diff --git a/src/main/resources/stubs/math/const.gobra b/src/main/resources/stubs/math/const.gobra new file mode 100644 index 000000000..0108789c1 --- /dev/null +++ b/src/main/resources/stubs/math/const.gobra @@ -0,0 +1,31 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in https://golang.org/LICENSE + +// Signatures for the public declarations in file +// https://github.com/golang/go/blob/master/src/math/const.go + +package math + +// Integer limit values. +// +// NOTE (Gobra): the architecture-dependent limits `MaxInt`, `MinInt`, and +// `MaxUint` are deliberately NOT provided. Their values depend on the size of +// the platform `int`/`uint` (32 or 64 bits), which the verifier does not fix, +// so a proof relying on them would be unsound on some target. Use the +// limits of a concrete sized type (e.g. `MaxInt64`, `MinInt32`, `MaxUint8`) +// instead. +const ( + MaxInt8 = 1<<7 - 1 // 127 + MinInt8 = -1 << 7 // -128 + MaxInt16 = 1<<15 - 1 // 32767 + MinInt16 = -1 << 15 // -32768 + MaxInt32 = 1<<31 - 1 // 2147483647 + MinInt32 = -1 << 31 // -2147483648 + MaxInt64 = 1<<63 - 1 // 9223372036854775807 + MinInt64 = -1 << 63 // -9223372036854775808 + MaxUint8 = 1<<8 - 1 // 255 + MaxUint16 = 1<<16 - 1 // 65535 + MaxUint32 = 1<<32 - 1 // 4294967295 + MaxUint64 = 1<<64 - 1 // 18446744073709551615 +) diff --git a/src/main/scala/viper/gobra/Gobra.scala b/src/main/scala/viper/gobra/Gobra.scala index af1ce9536..ed6dae717 100644 --- a/src/main/scala/viper/gobra/Gobra.scala +++ b/src/main/scala/viper/gobra/Gobra.scala @@ -15,7 +15,7 @@ import com.typesafe.scalalogging.StrictLogging import org.slf4j.LoggerFactory import scalaz.Scalaz.futureInstance import viper.gobra.ast.internal.Program -import viper.gobra.ast.internal.transform.{CGEdgesTerminationTransform, ConstantPropagation, InternalTransform, OverflowChecksTransform} +import viper.gobra.ast.internal.transform.{CGEdgesTerminationTransform, ConstantPropagation, InternalTransform} import viper.gobra.backend.BackendVerifier import viper.gobra.frontend.PackageResolver.{AbstractPackage, RegularPackage} import viper.gobra.frontend.Parser.ParseResult @@ -50,6 +50,8 @@ object GoVerifier { trait GoVerifier extends StrictLogging { + protected val timeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss") + def name: String = { this.getClass.getSimpleName } @@ -86,7 +88,6 @@ trait GoVerifier extends StrictLogging { } }) - val timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss"); config.packageInfoInputMap.keys.foreach(pkgInfo => { val pkgId = pkgInfo.id logger.info(s"Verifying package $pkgId [${LocalTime.now().format(timeFormatter)}]") @@ -254,6 +255,7 @@ class Gobra extends GoVerifier with GoIdeVerifier { // that all imported packages have been parsed successfully (this is only checked during type-checking) private def performParsing(config: Config, pkgInfo: PackageInfo)(implicit executor: GobraExecutionContext): EitherT[Vector[VerifierError], Future, Map[AbstractPackage, ParseResult]] = { if (config.shouldParse) { + logger.info(s"Phase 1/6: parsing [${LocalTime.now().format(timeFormatter)}]") val startMs = System.currentTimeMillis() val res = Parser.parse(config, pkgInfo) logger.debug { @@ -268,6 +270,7 @@ class Gobra extends GoVerifier with GoIdeVerifier { private def performTypeChecking(config: Config, pkgInfo: PackageInfo, parseResults: Map[AbstractPackage, ParseResult])(implicit executor: GobraExecutionContext): EitherT[Vector[VerifierError], Future, TypeInfo] = { if (config.shouldTypeCheck) { + logger.info(s"Phase 2/6: type-checking [${LocalTime.now().format(timeFormatter)}]") Info.check(config, RegularPackage(pkgInfo.id), parseResults) } else { EitherT.left(Vector.empty) @@ -276,6 +279,7 @@ class Gobra extends GoVerifier with GoIdeVerifier { private def performDesugaring(config: Config, typeInfo: TypeInfo)(implicit executor: GobraExecutionContext): EitherT[Vector[VerifierError], Future, Program] = { if (config.shouldDesugar) { + logger.info(s"Phase 3/6: desugaring [${LocalTime.now().format(timeFormatter)}]") val startMs = System.currentTimeMillis() val res = EitherT.right[Vector[VerifierError], Future, Program](Desugar.desugar(config, typeInfo)(executor)) logger.debug { @@ -289,18 +293,12 @@ class Gobra extends GoVerifier with GoIdeVerifier { } /** - * Applies transformations to programs in the internal language. Currently, only adds overflow checks but it can - * be easily extended to perform more transformations + * Applies transformations to programs in the internal language. */ private def performInternalTransformations(config: Config, pkgInfo: PackageInfo, program: Program)(implicit executor: GobraExecutionContext): EitherT[Vector[VerifierError], Future, Program] = { - // constant propagation does not cause duplication of verification errors caused - // by overflow checks (if enabled) because all overflows in constant declarations - // can be found by the well-formedness checks. + logger.info(s"Phase 4/6: internal transformations [${LocalTime.now().format(timeFormatter)}]") val startMs = System.currentTimeMillis() - var transformations: Vector[InternalTransform] = Vector(CGEdgesTerminationTransform, ConstantPropagation) - if (config.checkOverflows) { - transformations :+= OverflowChecksTransform - } + val transformations: Vector[InternalTransform] = Vector(CGEdgesTerminationTransform, ConstantPropagation) val result = transformations.foldLeft(program)((prog, transf) => transf.transform(prog)) logger.debug { val durationS = f"${(System.currentTimeMillis() - startMs) / 1000f}%.1f" @@ -312,6 +310,7 @@ class Gobra extends GoVerifier with GoIdeVerifier { private def performViperEncoding(config: Config, pkgInfo: PackageInfo, program: Program)(implicit executor: GobraExecutionContext): EitherT[Vector[VerifierError], Future, BackendVerifier.Task] = { if (config.shouldViperEncode) { + logger.info(s"Phase 5/6: Viper encoding [${LocalTime.now().format(timeFormatter)}]") val startMs = System.currentTimeMillis() val res = EitherT.fromEither[Future, Vector[VerifierError], BackendVerifier.Task](Future.successful(Translator.translate(program, pkgInfo)(config))) logger.debug { @@ -328,6 +327,7 @@ class Gobra extends GoVerifier with GoIdeVerifier { if (config.noVerify) { Future(VerifierResult.Success)(executor) } else { + logger.info(s"Phase 6/6: backend verification [${LocalTime.now().format(timeFormatter)}]") verifyAst(config, pkgInfo, ast, backtrack)(executor) } } diff --git a/src/main/scala/viper/gobra/ast/internal/Program.scala b/src/main/scala/viper/gobra/ast/internal/Program.scala index f75ad8fe4..34ee5806f 100644 --- a/src/main/scala/viper/gobra/ast/internal/Program.scala +++ b/src/main/scala/viper/gobra/ast/internal/Program.scala @@ -19,7 +19,7 @@ import viper.gobra.reporting.Source.Parser import viper.gobra.theory.Addressability import viper.gobra.translator.Names import viper.gobra.util.{BackendAnnotation, Decimal, GoString, NumBase, TypeBounds, Violation} -import viper.gobra.util.TypeBounds.{IntegerKind, UnboundedInteger, UntypedConstInteger} +import viper.gobra.util.TypeBounds.{IntegerKind, UnboundedInteger} import viper.gobra.util.Violation.violation import scala.collection.SortedSet @@ -996,8 +996,8 @@ case class MapKeys(exp : Expr, expUnderlyingType: Type)(val info : Source.Parser case class MapValues(exp : Expr, expUnderlyingType: Type)(val info : Source.Parser.Info) extends Expr { override val typ : Type = expUnderlyingType match { - case t: MathMapT => SetT(t.keys, Addressability.mathDataStructureElement) - case t: MapT => SetT(t.keys, Addressability.rValue) + case t: MathMapT => SetT(t.values, Addressability.mathDataStructureElement) + case t: MapT => SetT(t.values, Addressability.rValue) case _ => violation(s"unexpected type ${exp.typ}") } } @@ -1105,14 +1105,15 @@ sealed abstract class BinaryIntExpr(override val operator: String) extends Binar // "kinds IntegerKind(integer) and IntegerKind(int) cannot be merged". case (IntT(_, kind1), IntT(_, kind2)) => IntT(Addressability.Exclusive, TypeBounds.mergeLenient(kind1, kind2)) - // A binary expression may have one operand of a defined type T and another operand that is an unbounded - // integer or an untyped integer constant. + // A binary expression may have one operand of a defined type T and another operand that is an integer + // (of any kind: a conversion like `AS(v)` yields the underlying bounded kind, an untyped constant may + // have been assigned a concrete kind by the type-checker, and internally synthesized nodes are unbounded). // If T's underlying type is an integer type, then the result of the expression should be of type T. // Here, the underlying type of a defined type is not checked, as the information is not available at this point. // However, this should not pose a problem assuming that the original program has been type-checked before the // translation to the internal language. - case (x, IntT(_, UnboundedInteger | UntypedConstInteger)) if x.isInstanceOf[DefinedT] => x.withAddressability(Addressability.Exclusive) - case (IntT(_, UnboundedInteger | UntypedConstInteger), y) if y.isInstanceOf[DefinedT] => y.withAddressability(Addressability.Exclusive) + case (x: DefinedT, _: IntT) => x.withAddressability(Addressability.Exclusive) + case (_: IntT, y: DefinedT) => y.withAddressability(Addressability.Exclusive) case (x, y) if x.equalsWithoutMod(y) => x.withAddressability(Addressability.Exclusive) case (l, r) => violation(s"cannot merge types $l and $r") @@ -1153,7 +1154,13 @@ case class ShiftLeft(left: Expr, right: Expr)(val info: Source.Parser.Info) exte case class ShiftRight(left: Expr, right: Expr)(val info: Source.Parser.Info) extends BinaryIntExpr(">>") { override val typ: Type = left.typ } -case class BitNeg(op: Expr)(val info: Source.Parser.Info) extends IntOperation +case class BitNeg(op: Expr)(val info: Source.Parser.Info) extends IntOperation { + // ^x has its operand's type. The inherited unbounded-Int typ would misreport the + // complement of a bounded operand: the encoding produces a domain-typed value for it, + // and enclosing operations decide based on this typ whether the value still needs the + // domain-to-Int projection. + override def typ: Type = op.typ.withAddressability(Addressability.rValue) +} /* * Convert 'expr' to non-interface type 'newType'. If 'newType' is diff --git a/src/main/scala/viper/gobra/ast/internal/transform/OverflowChecksTransform.scala b/src/main/scala/viper/gobra/ast/internal/transform/OverflowChecksTransform.scala deleted file mode 100644 index 246e1d66f..000000000 --- a/src/main/scala/viper/gobra/ast/internal/transform/OverflowChecksTransform.scala +++ /dev/null @@ -1,202 +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-2020 ETH Zurich. - -package viper.gobra.ast.internal.transform - -import viper.gobra.ast.internal._ -import viper.gobra.reporting.Source -import viper.gobra.reporting.Source.OverflowCheckAnnotation -import viper.gobra.reporting.Source.Parser.{Internal, Single} -import viper.gobra.util.TypeBounds.BoundedIntegerKind -import viper.gobra.util.Violation.violation - -/** - * Adds overflow checks to programs written in Gobra's internal language - */ -object OverflowChecksTransform extends InternalTransform { - override def name(): String = "add_integer_overflow_checks" - - override def transform(p: Program): Program = transformMembers(memberTrans)(p) - - private def memberTrans(member: Member): Member = member match { - // adds overflow checks per statement that contains subexpressions of bounded integer type and adds assume - /// statements at the beginning of a function or method body assuming that the value of an argument (of - // bounded integer type) respects the bounds. - case f@Function(name, args, results, pres, posts, terminationMeasure, annotations, body) => - Function(name, args, results, pres, posts, terminationMeasure, annotations, body map computeNewBody)(f.info) - - // same as functions - case m@Method(receiver, name, args, results, pres, posts, terminationMeasure, annotations, body) => - Method(receiver, name, args, results, pres, posts, terminationMeasure, annotations, body map computeNewBody)(m.info) - - // Adds pre-conditions stating the bounds of each argument and a post-condition to check if the body expression - // overflows - case f@PureFunction(name, args, results, pres, posts, terminationMeasure, annotations, body, isOpaque) => body match { - case Some(expr) => - val newPost = posts ++ getPureBlockPosts(expr, results) - PureFunction(name, args, results, pres, newPost, terminationMeasure, annotations, body, isOpaque)(f.info) - case None => f - } - - // Same as pure functions - case m@PureMethod(receiver, name, args, results, pres, posts, terminationMeasure, annotations, body, isOpaque) => body match { - case Some(expr) => - val newPost = posts ++ getPureBlockPosts(expr, results) - PureMethod(receiver, name, args, results, pres, newPost, terminationMeasure, annotations, body, isOpaque)(m.info) - case None => m - } - - /* As discussed on the Gobra meeting (27/10/2020), overflow checks should not be added to predicates, assertions - * and any other purely logical (i.e. non-executable code) statements and expressions. This seems to be the approach taken - * by other verification tools such as FramaC, as noted by Wytse - */ - - case x => x - } - - /** - * Adds overflow checks to the body of a method. - */ - private def computeNewBody(body: MethodBody): MethodBody = { - MethodBody( - body.decls, - MethodBodySeqn(body.seqn.stmts map stmtTransform)(body.seqn.info), - body.postprocessing map stmtTransform, - )(body.info) - } - - /** - * Computes the post-conditions to be added to pure functions and methods to check for overflows, i.e. - * that the expression result is within the bounds of its type - */ - private def getPureBlockPosts(body: Expr, results: Vector[Parameter.Out]): Vector[Assertion] = { - // relies on the current assumption that pure functions and methods must have exactly one result argument - if (results.length != 1) violation("Pure functions and methods must have exactly one result argument") - Vector(assertionExprInBounds(body, results(0).typ)(createAnnotatedInfo(body.info))) - } - - private def stmtTransform(stmt: Stmt): Stmt = stmt match { - case b@Block(decls, stmts) => Block(decls, stmts map stmtTransform)(b.info) - - case s@Seqn(stmts) => Seqn(stmts map stmtTransform)(s.info) - - case i@If(cond, thn, els) => - val condInfo = createAnnotatedInfo(cond.info) - val assertCond = Assert(assertionExprInBounds(cond, cond.typ)(condInfo))(condInfo) - val ifStmt = If(cond, stmtTransform(thn), stmtTransform(els))(i.info) - Seqn(Vector(assertCond, ifStmt))(i.info) - - case w@While(cond, invs, terminationMeasure, body) => - val condInfo = createAnnotatedInfo(cond.info) - val assertCond = Assert(assertionExprInBounds(cond, cond.typ)(condInfo))(condInfo) - val whileStmt = While(cond, invs, terminationMeasure,stmtTransform(body))(w.info) - Seqn(Vector(assertCond, whileStmt))(w.info) - - case ass@SingleAss(l, r) => - val info = createAnnotatedInfo(r.info) - val assertBounds = Assert(assertionExprInBounds(r, l.op.typ)(info))(info) - Seqn(Vector(assertBounds, ass))(l.op.info) - - case f@FunctionCall(_, _, args) => - Seqn(genOverflowChecksExprs(args) :+ f)(f.info) - - case m@MethodCall(_, recv, _, args) => - Seqn(genOverflowChecksExprs(recv +: args) :+ m)(m.info) - - case m@New(_, expr) => - Seqn(genOverflowChecksExprs(Vector(expr)) :+ m)(m.info) - - case f@GoFunctionCall(_, args) => - Seqn(genOverflowChecksExprs(args) :+ f)(f.info) - - case m@GoMethodCall(recv, _, args) => - Seqn(genOverflowChecksExprs(recv +: args) :+ m)(m.info) - - case d@Defer(FunctionCall(_, _, args)) => Seqn(genOverflowChecksExprs(args) :+ d)(d.info) - case d@Defer(MethodCall(_, recv, _, args)) => Seqn(genOverflowChecksExprs(recv +: args) :+ d)(d.info) - case d@Defer(_: Fold | _: Unfold | _: PredExprFold | _: PredExprUnfold) => d - - case m@Send(_, expr, _, _, _) => - Seqn(genOverflowChecksExprs(Vector(expr)) :+ m)(m.info) - - case m@MakeSlice(_, _, arg1, optArg2) => - Seqn(genOverflowChecksExprs(arg1 +: optArg2.toVector) :+ m)(m.info) - - case m@MakeChannel(_, _, optArg, _, _) => - Seqn(genOverflowChecksExprs(optArg.toVector) :+ m)(m.info) - - case m@MakeMap(_, _, optArg) => - Seqn(genOverflowChecksExprs(optArg.toVector) :+ m)(m.info) - - case m@SafeMapLookup(_, _, IndexedExp(base, idx, _)) => - Seqn(genOverflowChecksExprs(Vector(base, idx)) :+ m)(m.info) - - case c@Critical(inv, invIsInv, openInvs, body) => - Critical(inv, invIsInv, openInvs, stmtTransform(body))(c.info) - - // explicitly matches remaining statements to detect non-exhaustive pattern matching if a new statement is added - case x@(_: Inhale | _: Exhale | _: Assert | _: Refute | _: Assume | _: AssignSuchThat - | _: Return | _: Fold | _: Unfold | _: PredExprFold | _: PredExprUnfold | _: Outline - | _: SafeTypeAssertion | _: SafeReceive | _: Label | _: Initialization | _: PatternMatchStmt) => x - - case _ => violation("Unexpected case reached.") - } - - private def genOverflowChecksExprs(exprs: Vector[Expr]): Vector[Assert] = - exprs map (expr => { - val info = createAnnotatedInfo(expr.info) - Assert(assertionExprInBounds(expr, expr.typ)(info))(info) - }) - - // Checks if expr and its subexpressions are within bounds given by their type - private def assertionExprInBounds(expr: Expr, typ: Type)(info: Source.Parser.Info): Assertion = { - val trueLit: Expr = BoolLit(b = true)(info) - - def genAssertionExpr(expr: Expr, typ: Type): Expr = { - typ match { - case IntT(_, kind) if kind.isInstanceOf[BoundedIntegerKind] => - val boundedKind = kind.asInstanceOf[BoundedIntegerKind] - And( - AtMostCmp(IntLit(boundedKind.lower)(info), expr)(info), - AtMostCmp(expr, IntLit(boundedKind.upper)(info))(info))(info) - - case _ => trueLit - } - } - - val intSubExprsWithType: Set[(Expr, Type)] = Expr.getSubExpressions(expr) - .filter(_.typ.isInstanceOf[IntT]) - .map(e => if (e == expr) (expr, typ) else (e, e.typ)) - - // values assumed to be within bounds, i.e. variables, fields from structs, dereferences of pointers and indexed expressions - // this stops Gobra from throwing overflow errors in field accesses and pointer dereferences because Gobra was not able to prove that - // they were within bounds even though that is guaranteed by the expression's type - val valuesWithinBounds = intSubExprsWithType.filter(elem => elem._1 match { - case _: Var | _: FieldRef | _: IndexedExp | _: Deref => true - case _ => false - }) - - val computeAssertions = (exprsWithType: Set[(Expr, Type)]) => - exprsWithType - .map{elem => genAssertionExpr(elem._1, elem._2)} - .foldRight(trueLit)((x,y) => And(x,y)(info)) - - // assumptions for the values that are considered within bounds - val assumptions = computeAssertions(valuesWithinBounds) - - // Assertions that need to be verified assuming the expressions in `assumptions` - val obligations = ExprAssertion(computeAssertions(intSubExprsWithType))(info) - Implication(assumptions, obligations)(info) - } - - private def createAnnotatedInfo(info: Source.Parser.Info): Source.Parser.Info = - info match { - case s: Single => s.createAnnotatedInfo(OverflowCheckAnnotation) - // the following is temporary hack that will be discarded when we merge the new support for overflow checking - case i@ Internal => i - case i => violation(s"l.op.info ($i) is expected to be a Single") - } -} diff --git a/src/main/scala/viper/gobra/ast/internal/utility/IntKindAlignment.scala b/src/main/scala/viper/gobra/ast/internal/utility/IntKindAlignment.scala new file mode 100644 index 000000000..cec7a74df --- /dev/null +++ b/src/main/scala/viper/gobra/ast/internal/utility/IntKindAlignment.scala @@ -0,0 +1,200 @@ +// 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-2020 ETH Zurich. + +package viper.gobra.ast.internal.utility + +import viper.gobra.ast.{internal => in} +import viper.gobra.theory.Addressability +import viper.gobra.util.TypeBounds + +/** + * Helpers to keep integer-typed binary operands of the same `IntegerKind`. Some internal AST + * nodes hardcode `IntT(UnboundedInteger)` regardless of operand type (notably `in.Length` and + * `in.Capacity`), while the frontend may infer a bounded kind for sibling literals. The + * encoding requires both operands of equality / comparison nodes to be of the same kind so + * that no Viper consistency violation arises when the encoded LHS and RHS land in different + * Viper sorts. + * + * The alignment always demotes a bounded operand to `UnboundedInteger` via an `in.Conversion` + * (the bounded → unbounded direction is total: the corresponding bridge function `from` has + * no precondition). Promoting the unbounded operand would require an in-range check that may + * not hold in general (e.g. `len(s)` is non-negative but unbounded above in the encoding). + */ +object IntKindAlignment { + + private val unboundedT: in.Type = + in.IntT(Addressability.rValue, TypeBounds.UnboundedInteger) + + /** + * If `l` and `r` are both integer-typed but with different IntegerKinds, align them. + * + * Preferred path: if one operand is an `in.IntLit` whose value fits in the *other* operand's + * kind, retype the literal in place (no `in.Conversion` introduced). This keeps the + * encoding free of `wrap` roundtrips that otherwise lose precision. + * + * Fallback: demote the bounded operand to `UnboundedInteger` via an `in.Conversion` (the + * `from` bridge function has no precondition, so this is total). + * + * If both operands are bounded but of different kinds (e.g. `int8 == int16`), no alignment + * is performed — that is a type-checker concern, not an alignment concern. + */ + def alignIntKinds(l: in.Expr, r: in.Expr): (in.Expr, in.Expr) = (l.typ, r.typ) match { + case (in.IntT(_, lk), in.IntT(_, rk)) if lk != rk => + // 1) Retype an IntLit to match the other side, when its value fits. + (l, r) match { + case (lit: in.IntLit, _) if fitsInKind(lit.v, rk) => + return (in.IntLit(lit.v, rk, lit.base)(lit.info), r) + case (_, lit: in.IntLit) if fitsInKind(lit.v, lk) => + return (l, in.IntLit(lit.v, lk, lit.base)(lit.info)) + case _ => + } + // 2) A constant expression (e.g. `-1`, i.e. Sub(0, 1)) of mathematical/untyped kind + // next to a bounded sibling: fold it to a literal of the bounded kind, when it fits. + // Demoting the bounded side instead would leak a Viper `Int` into a context whose + // surrounding sort (e.g. a pure function's bounded return type) is a domain type. + if (isMathematical(lk) && rk.isInstanceOf[TypeBounds.BoundedIntegerKind]) + foldConst(l) match { + case Some(v) if fitsInKind(v, rk) => return (in.IntLit(v, rk)(l.info), r) + case _ => + } + if (isMathematical(rk) && lk.isInstanceOf[TypeBounds.BoundedIntegerKind]) + foldConst(r) match { + case Some(v) if fitsInKind(v, lk) => return (l, in.IntLit(v, lk)(r.info)) + case _ => + } + // 3) Otherwise, demote the bounded operand to integer. + if (isMathematical(lk)) + (l, in.Conversion(unboundedT, r)(r.info)) + else if (isMathematical(rk)) + (in.Conversion(unboundedT, l)(l.info), r) + else + (l, r) + + // Ghost collections of ints whose element kinds disagree (e.g. `seq[1..4] == seq[int]{1,2,3}`, + // where the range sequence has mathematical-integer elements while the literal has bounded + // `int` elements). The mathematical side is promoted to the bounded element kind — see + // [[coerceToElemKind]]. + case (lt, rt) if elemKindMismatch(lt, rt) => + (elemIntKind(lt), elemIntKind(rt)) match { + case (Some(lk: TypeBounds.BoundedIntegerKind), Some(_)) => (l, coerceToElemKind(r, lk)) + case (Some(_), Some(rk: TypeBounds.BoundedIntegerKind)) => (coerceToElemKind(l, rk), r) + case _ => (l, r) + } + + case _ => (l, r) + } + + /** True for the kinds whose Viper encoding is a plain `Int` (as opposed to a bounded domain). */ + private def isMathematical(k: TypeBounds.IntegerKind): Boolean = + k == TypeBounds.UnboundedInteger || k == TypeBounds.UntypedConstInteger + + /** Statically evaluates simple constant integer expressions (literals combined with +, -, *). */ + private def foldConst(e: in.Expr): Option[BigInt] = e match { + case lit: in.IntLit => Some(lit.v) + case in.Add(l, r) => for { a <- foldConst(l); b <- foldConst(r) } yield a + b + case in.Sub(l, r) => for { a <- foldConst(l); b <- foldConst(r) } yield a - b + case in.Mul(l, r) => for { a <- foldConst(l); b <- foldConst(r) } yield a * b + case _ => None + } + + /** The integer element/member kind of a ghost collection or option type, if any. */ + private def elemIntKind(t: in.Type): Option[TypeBounds.IntegerKind] = t match { + case in.SequenceT(in.IntT(_, k), _) => Some(k) + case in.SetT(in.IntT(_, k), _) => Some(k) + case in.MultisetT(in.IntT(_, k), _) => Some(k) + case in.OptionT(in.IntT(_, k), _) => Some(k) + case _ => None + } + + /** True if both types are the same ghost collection / option over ints but with different elem + * kinds, where exactly one side is mathematical (`integer` / untyped). */ + private def elemKindMismatch(lt: in.Type, rt: in.Type): Boolean = { + def unbounded(k: TypeBounds.IntegerKind): Boolean = + k == TypeBounds.UnboundedInteger || k == TypeBounds.UntypedConstInteger + ((lt, rt) match { + case (_: in.SequenceT, _: in.SequenceT) => true + case (_: in.SetT, _: in.SetT) => true + case (_: in.MultisetT, _: in.MultisetT) => true + case (_: in.OptionT, _: in.OptionT) => true + case _ => false + }) && ((elemIntKind(lt), elemIntKind(rt)) match { + case (Some(lk), Some(rk)) => lk != rk && (unbounded(lk) ^ unbounded(rk)) + case _ => false + }) + } + + /** + * Coerces a ghost collection expression with mathematical-integer elements to the bounded + * element kind `k`. Sequence-typed expressions are wrapped with an `in.Conversion` to + * `seq[k]`, which the encoding translates to a per-kind `Seq[Int] -> Seq[Bounded_k]` + * mapping function. Set-/multiset-typed expressions have no direct mapping function; + * instead, the coercion is pushed through the structure (conversions from sequences and + * set operations) until it reaches sequence level. + */ + def coerceToElemKind(e: in.Expr, k: TypeBounds.BoundedIntegerKind): in.Expr = { + def boundedElemT: in.Type = in.IntT(Addressability.mathDataStructureElement, k) + e.typ match { + case in.IntT(_, ek) if ek != k => // element-level coercion (used for recursion helpers) + in.Conversion(in.IntT(Addressability.rValue, k), e)(e.info) + case _: in.SequenceT => + in.Conversion(in.SequenceT(boundedElemT, e.typ.addressability), e)(e.info) + case t: in.SetT => e match { + case in.SetConversion(s) => in.SetConversion(coerceToElemKind(s, k))(e.info) + case in.Union(a, b, _) => in.Union(coerceToElemKind(a, k), coerceToElemKind(b, k), in.SetT(boundedElemT, t.addressability))(e.info) + case in.Intersection(a, b, _) => in.Intersection(coerceToElemKind(a, k), coerceToElemKind(b, k), in.SetT(boundedElemT, t.addressability))(e.info) + case in.SetMinus(a, b, _) => in.SetMinus(coerceToElemKind(a, k), coerceToElemKind(b, k), in.SetT(boundedElemT, t.addressability))(e.info) + case _ => e // no general Set[Int] -> Set[Bounded_k] mapping; leave unchanged + } + case t: in.MultisetT => e match { + case in.MultisetConversion(s) => in.MultisetConversion(coerceToElemKind(s, k))(e.info) + case in.Union(a, b, _) => in.Union(coerceToElemKind(a, k), coerceToElemKind(b, k), in.MultisetT(boundedElemT, t.addressability))(e.info) + case in.Intersection(a, b, _) => in.Intersection(coerceToElemKind(a, k), coerceToElemKind(b, k), in.MultisetT(boundedElemT, t.addressability))(e.info) + case in.SetMinus(a, b, _) => in.SetMinus(coerceToElemKind(a, k), coerceToElemKind(b, k), in.MultisetT(boundedElemT, t.addressability))(e.info) + case _ => e + } + // Options: push the element coercion into the constructor — `some(v)` -> `some(int(v))`, + // `none[integer]` -> `none[int]`. There is no general Option[Int] -> Option[Bounded_k] + // mapping function, so a plain option variable is left unchanged. + case _: in.OptionT => e match { + case in.OptionSome(x) => in.OptionSome(coerceToElemKind(x, k))(e.info) + case _: in.OptionNone => in.OptionNone(boundedElemT)(e.info) + case _ => e + } + case _ => e + } + } + + /** True if `v` is representable in `kind`. UnboundedInteger admits any value. */ + private def fitsInKind(v: BigInt, kind: TypeBounds.IntegerKind): Boolean = kind match { + case TypeBounds.UnboundedInteger => true + case bk: TypeBounds.BoundedIntegerKind => v >= bk.lower && v <= bk.upper + case _ => true + } + + /** + * If `e` has a bounded integer type, wrap it with an `in.Conversion` to integer. Otherwise + * return `e` unchanged. Useful for slots that require a Viper Int (sequence indices, slice + * indices, perm-constructor numerator/denominator, etc.) but that may receive a bounded + * integer value because the frontend infers a concrete kind for sibling literals. + */ + def asUnboundedInt(e: in.Expr): in.Expr = asUnboundedInt(e, e.typ) + + /** + * Like [[asUnboundedInt(e:viper\.gobra\.ast\.internal\.Expr)*]], but decides based on a + * caller-resolved type. Use this variant when `e`'s type may be a defined type whose + * *underlying* type is a bounded integer (e.g. `type Type uint8`): the caller resolves the + * underlying type (`underlyingType(e.typ)(ctx)` in encodings, `underlyingType` in the + * desugarer) since this utility has no access to type-declaration lookups. + */ + def asUnboundedInt(e: in.Expr, resolvedTyp: in.Type): in.Expr = resolvedTyp match { + case in.IntT(_, k) if k != TypeBounds.UnboundedInteger => + // Bounded literals are NOT retyped to plain integer literals here: the Conversion's + // `from(to(c))` roundtrip keeps the ground `to(c)` bridge-axiom anchors that Z3's + // nonlinear reasoning demonstrably relies on (see BoundedIntEncoding.asInt). + in.Conversion(unboundedT, e)(e.info) + case _ => e + } +} diff --git a/src/main/scala/viper/gobra/frontend/Config.scala b/src/main/scala/viper/gobra/frontend/Config.scala index f8fc30fda..af5a7eeab 100644 --- a/src/main/scala/viper/gobra/frontend/Config.scala +++ b/src/main/scala/viper/gobra/frontend/Config.scala @@ -52,6 +52,10 @@ object ConfigDefaults { // as they have the same size. This flag allows users to pick the size of int's and uints's: 32 if true, // 64 bit otherwise. val DefaultInt32bit: Boolean = false + // When enabled, all integer types (including bounded types like int, int8, uint16, ...) are encoded + // as Viper's mathematical (unbounded) Int. This restores Gobra's integer encoding prior to the + // introduction of the sound bounded-integer semantics. It is mutually exclusive with checkOverflows. + val DefaultUnboundedIntegers: Boolean = false // the following option is currently not controllable via CLI as it is meaningless without a constantly // running JVM. It is targeted in particular to Gobra Server and Gobra IDE val DefaultCacheParserAndTypeChecker: Boolean = false @@ -212,6 +216,9 @@ case class Config( // as they have the same size. This flag allows users to pick the size of int's and uints's: 32 if true, // 64 bit otherwise. int32bit: Boolean = ConfigDefaults.DefaultInt32bit, + // When enabled, all integer types (including bounded types) are encoded as Viper's + // unbounded Int, restoring Gobra's integer encoding prior to the sound bounded-int semantics. + unboundedIntegers: Boolean = ConfigDefaults.DefaultUnboundedIntegers, // the following option is currently not controllable via CLI as it is meaningless without a constantly // running JVM. It is targeted in particular to Gobra Server and Gobra IDE cacheParserAndTypeChecker: Boolean = ConfigDefaults.DefaultCacheParserAndTypeChecker, @@ -272,6 +279,7 @@ case class Config( cacheFile = cacheFile orElse input.cacheFile.value, checkOverflows = checkOverflows || input.checkOverflows.value.contains(true), int32bit = int32bit || input.int32bit.value.contains(true), + unboundedIntegers = unboundedIntegers || input.unboundedIntegers.value.contains(true), checkConsistency = checkConsistency || input.checkConsistency.value.contains(true), cacheParserAndTypeChecker = cacheParserAndTypeChecker, onlyFilesWithHeader = onlyFilesWithHeader || input.onlyFilesWithHeader.value.contains(true), @@ -325,6 +333,7 @@ case class Config( "checkOverflows" -> checkOverflows, "checkConsistency" -> checkConsistency, "int32bit" -> int32bit, + "unboundedIntegers" -> unboundedIntegers, "onlyFilesWithHeader" -> onlyFilesWithHeader, "gobraDirectory" -> gobraDirectory.map(_.toString).getOrElse("(none)"), "assumeInjectivityOnInhale" -> assumeInjectivityOnInhale, @@ -402,6 +411,7 @@ case class BaseConfig(gobraDirectory: Option[Path] = ConfigDefaults.DefaultGobra checkOverflows: Boolean = ConfigDefaults.DefaultCheckOverflows, checkConsistency: Boolean = ConfigDefaults.DefaultCheckConsistency, int32bit: Boolean = ConfigDefaults.DefaultInt32bit, + unboundedIntegers: Boolean = ConfigDefaults.DefaultUnboundedIntegers, cacheParserAndTypeChecker: Boolean = ConfigDefaults.DefaultCacheParserAndTypeChecker, onlyFilesWithHeader: Boolean = ConfigDefaults.DefaultOnlyFilesWithHeader, assumeInjectivityOnInhale: Boolean = ConfigDefaults.DefaultAssumeInjectivityOnInhale, @@ -485,6 +495,7 @@ case class InputConfig( checkOverflows: InputConfigOption[Boolean] = InputConfigOption("checkOverflows", None), cacheFile: InputConfigOption[Path] = InputConfigOption("cacheFile", None), int32bit: InputConfigOption[Boolean] = InputConfigOption("int32bit", None), + unboundedIntegers: InputConfigOption[Boolean] = InputConfigOption("unboundedIntegers", None), onlyFilesWithHeader: InputConfigOption[Boolean] = InputConfigOption("onlyFilesWithHeader", None), checkConsistency: InputConfigOption[Boolean] = InputConfigOption("checkConsistency", None), assumeInjectivityOnInhale: InputConfigOption[Boolean] = InputConfigOption("assumeInjectivityOnInhale", None), @@ -546,6 +557,7 @@ case class InputConfig( checkOverflows = checkOverflows orElse other.checkOverflows, cacheFile = cacheFile orElse other.cacheFile, int32bit = int32bit orElse other.int32bit, + unboundedIntegers = unboundedIntegers orElse other.unboundedIntegers, onlyFilesWithHeader = onlyFilesWithHeader orElse other.onlyFilesWithHeader, checkConsistency = checkConsistency orElse other.checkConsistency, assumeInjectivityOnInhale = assumeInjectivityOnInhale orElse other.assumeInjectivityOnInhale, @@ -644,6 +656,7 @@ case class InputConfig( checkOverflows = checkOverflows orElse other.checkOverflows, cacheFile = cacheFile orElse other.cacheFile, int32bit = int32bit orElse other.int32bit, + unboundedIntegers = unboundedIntegers orElse other.unboundedIntegers, onlyFilesWithHeader = onlyFilesWithHeader orElse other.onlyFilesWithHeader, checkConsistency = checkConsistency orElse other.checkConsistency, assumeInjectivityOnInhale = assumeInjectivityOnInhale orElse other.assumeInjectivityOnInhale, @@ -755,6 +768,13 @@ case class InputConfig( } else { Right(()) }, + // `--unboundedIntegers` encodes every integer as Viper's unbounded Int, so there are no bounds to + // overflow. Checking overflows in that mode is meaningless, hence the two flags are mutually exclusive. + if (unboundedIntegers.value.contains(true) && checkOverflows.value.contains(true)) { + Left(Vector(ConfigError("--unboundedIntegers cannot be combined with --overflow."))) + } else { + Right(()) + }, // file validations validateFilesExist(cutInput), @@ -945,6 +965,7 @@ case class InputConfig( checkOverflows = checkOverflows.value.getOrElse(ConfigDefaults.DefaultCheckOverflows), checkConsistency = checkConsistency.value.getOrElse(ConfigDefaults.DefaultCheckConsistency), int32bit = int32bit.value.getOrElse(ConfigDefaults.DefaultInt32bit), + unboundedIntegers = unboundedIntegers.value.getOrElse(ConfigDefaults.DefaultUnboundedIntegers), cacheParserAndTypeChecker = false, // caching does not make sense when using the CLI. Thus, we simply set it to `false` onlyFilesWithHeader = onlyFilesWithHeader.value.getOrElse(ConfigDefaults.DefaultOnlyFilesWithHeader), assumeInjectivityOnInhale = assumeInjectivityOnInhale.value.getOrElse(ConfigDefaults.DefaultAssumeInjectivityOnInhale), @@ -1070,6 +1091,7 @@ trait RawConfig { shouldVerify = baseConfig.shouldVerify, shouldChop = baseConfig.shouldChop, int32bit = baseConfig.int32bit, + unboundedIntegers = baseConfig.unboundedIntegers, cacheParserAndTypeChecker = baseConfig.cacheParserAndTypeChecker, onlyFilesWithHeader = baseConfig.onlyFilesWithHeader, assumeInjectivityOnInhale = baseConfig.assumeInjectivityOnInhale, @@ -1568,6 +1590,15 @@ class ScallopGobraConfig(arguments: Seq[String], isInputOptional: Boolean = fals noshort = false ) + val unboundedIntegers: ScallopOption[Boolean] = opt[Boolean]( + name = "unboundedIntegers", + descr = "Encode all integer types (including bounded types like int, int8, uint16, ...) as Viper's " + + "unbounded Int. This restores Gobra's integer encoding prior to the sound bounded-integer semantics. " + + "Mutually exclusive with --overflow.", + default = Some(ConfigDefaults.DefaultUnboundedIntegers), + noshort = true + ) + val onlyFilesWithHeader: ScallopOption[Boolean] = opt[Boolean]( name = "onlyFilesWithHeader", descr = s"When enabled, Gobra only looks at files that contain the header comment '${Config.prettyPrintedHeader}'", @@ -1763,6 +1794,7 @@ class ScallopGobraConfig(arguments: Seq[String], isInputOptional: Boolean = fals checkOverflows = toInputConfigOption(checkOverflows), cacheFile = InputConfigOption(cacheFile.name, cacheFile.toOption.map(_.toPath)), int32bit = toInputConfigOption(int32Bit), + unboundedIntegers = toInputConfigOption(unboundedIntegers), onlyFilesWithHeader = toInputConfigOption(onlyFilesWithHeader), checkConsistency = toInputConfigOption(checkConsistency), assumeInjectivityOnInhale = toInputConfigOption(assumeInjectivityOnInhale), diff --git a/src/main/scala/viper/gobra/frontend/Desugar.scala b/src/main/scala/viper/gobra/frontend/Desugar.scala index 5929e2ab4..de0d44a48 100644 --- a/src/main/scala/viper/gobra/frontend/Desugar.scala +++ b/src/main/scala/viper/gobra/frontend/Desugar.scala @@ -563,7 +563,8 @@ object Desugar extends LazyLogging { in.StringLit(constValue.get)(src) case x if underlyingType(x).isInstanceOf[in.IntT] && x.addressability == Addressability.Exclusive => val constValue = sc.context.intConstantEvaluation(sc.exp) - in.IntLit(constValue.get)(src) + val kind = underlyingType(x).asInstanceOf[in.IntT].kind + in.IntLit(constValue.get, kind)(src) case in.PermissionT(Addressability.Exclusive) => val constValue = sc.context.permConstantEvaluation(sc.exp) in.PermLit(constValue.get._1, constValue.get._2)(src) @@ -2469,6 +2470,31 @@ object Desugar extends LazyLogging { } } + def alignIntKinds(l: in.Expr, r: in.Expr): (in.Expr, in.Expr) = + viper.gobra.ast.internal.utility.IntKindAlignment.alignIntKinds(l, r) + + /** + * If `e`'s type is an integer with a different `IntegerKind` than the expected element + * type `elemT`, retype an `in.IntLit` (when the value fits) or insert an `in.Conversion`. + * Used at sites that expect `e` to match a container's element type. + */ + def alignElementKind(e: in.Expr, elemT: in.Type): in.Expr = (e.typ, elemT) match { + case (in.IntT(_, ek), in.IntT(_, tk)) if ek != tk => + e match { + case lit: in.IntLit if isInRangeForKind(lit.v, tk) => + in.IntLit(lit.v, tk, lit.base)(lit.info) + case _ => + in.Conversion(elemT.withAddressability(Addressability.rValue), e)(e.info) + } + case _ => e + } + + private def isInRangeForKind(v: BigInt, kind: viper.gobra.util.TypeBounds.IntegerKind): Boolean = kind match { + case viper.gobra.util.TypeBounds.UnboundedInteger => true + case bk: viper.gobra.util.TypeBounds.BoundedIntegerKind => v >= bk.lower && v <= bk.upper + case _ => true + } + def implicitConversion(from: in.Type, to: in.Type, exp: in.Expr): in.Expr = { val fromUt = underlyingType(from) @@ -2476,8 +2502,27 @@ object Desugar extends LazyLogging { if (toUt.isInstanceOf[in.InterfaceT] && !fromUt.isInstanceOf[in.InterfaceT]) { in.ToInterface(exp, toUt)(exp.info) - } else { - exp + } else (fromUt, toUt) match { + // With domain-encoded bounded integers, distinct integer kinds map to distinct + // Viper sorts (e.g. Bounded_int vs Int). An implicit assignment between operands + // of different IntegerKind needs an explicit Conversion so the encoder can route + // through the appropriate `to`/`wrap` bridge function. Without this, the encoder + // sees a sort mismatch (e.g. Tuple2[Int] passed where Tuple2[Bounded_int] is + // expected) and Viper consistency-checks fail. + case (in.IntT(_, fromK), in.IntT(_, toK)) if fromK != toK => + in.Conversion(to, exp)(exp.info) + // Option types with mismatched integer element kinds (e.g. `opt = some(34)`, where + // `some(34)` is typed option[untyped] but the target is option[int]): push the + // alignment into the option constructor — there is no Viper-level mapping function + // between Option[Int] and Option[Bounded_k]. + case (in.OptionT(in.IntT(_, fromK), _), in.OptionT(toElem@ in.IntT(_, toK), _)) if fromK != toK => + exp match { + case in.OptionSome(x) => in.OptionSome(alignElementKind(x, toElem))(exp.info) + case _: in.OptionNone => in.OptionNone(toElem.withAddressability(Addressability.rValue))(exp.info) + case _ => exp + } + case _ => + exp } } @@ -2559,8 +2604,25 @@ object Desugar extends LazyLogging { private def indexedExprD(base : PExpression, index : PExpression)(ctx : FunctionContext, info : TypeInfo)(src : Meta) : Writer[in.IndexedExp] = { for { dbase <- exprD(ctx, info)(base) - dindex <- exprD(ctx, info)(index) + dindexRaw <- exprD(ctx, info)(index) baseUnderlyingType = underlyingType(dbase.typ) + // For seq/slice/array/string indexes the encoding expects a Viper Int (not a bounded + // integer domain value). Demote a bounded-int index to integer via Conversion, which + // routes through the (precondition-free) `from` bridge function. For maps, retype the + // index to match the map's declared key type. + dindex = baseUnderlyingType match { + case _: in.SliceT | _: in.ArrayT | _: in.SequenceT | _: in.StringT => + // Resolve the index's *underlying* type: the index may have a defined type whose + // underlying type is a bounded integer (e.g. `type Type uint8`). + underlyingType(dindexRaw.typ) match { + case in.IntT(_, k) if k != viper.gobra.util.TypeBounds.UnboundedInteger => + in.Conversion(in.IntT(Addressability.rValue, viper.gobra.util.TypeBounds.UnboundedInteger), dindexRaw)(dindexRaw.info) + case _ => dindexRaw + } + case t: in.MapT => alignElementKind(dindexRaw, t.keys) + case t: in.MathMapT => alignElementKind(dindexRaw, t.keys) + case _ => dindexRaw + } } yield in.IndexedExp(dbase, dindex, baseUnderlyingType)(src) } @@ -2591,6 +2653,22 @@ object Desugar extends LazyLogging { case None => } + // Constant-fold pure integer expressions whose inferred type is a bounded integer kind + // AND whose statically-evaluated value fits in that kind. This handles patterns like + // `PSub(PIntLit(0), PIntLit(128))` (Gobra's representation of unary `-128`) in an int8 + // context: the value -128 fits even though the inner literal 128 does not. Without + // folding, the encoding wraps 128 with the lossy `wrap` function (its postcondition is + // conditional on the input being in range), losing the constant -128. + info.typ(expr) match { + case Type.IntT(k: viper.gobra.util.TypeBounds.BoundedIntegerKind) => + info.intConstantEvaluation(expr) match { + case Some(v) if v >= k.lower && v <= k.upper => + return unit(in.IntLit(v, k)(src)) + case _ => + } + case _ => + } + expr match { case NoGhost(noGhost) => noGhost match { case n: PNamedOperand => info.resolve(n) match { @@ -2676,8 +2754,9 @@ object Desugar extends LazyLogging { } yield in.EqCmp(l, r)(src) } else { for { - l <- exprAndTypeAsExpr(ctx, info)(left) - r <- exprAndTypeAsExpr(ctx, info)(right) + l0 <- exprAndTypeAsExpr(ctx, info)(left) + r0 <- exprAndTypeAsExpr(ctx, info)(right) + (l, r) = alignIntKinds(l0, r0) } yield in.EqCmp(l, r)(src) } @@ -2692,8 +2771,9 @@ object Desugar extends LazyLogging { } yield in.UneqCmp(l, r)(src) } else { for { - l <- exprAndTypeAsExpr(ctx, info)(left) - r <- exprAndTypeAsExpr(ctx, info)(right) + l0 <- exprAndTypeAsExpr(ctx, info)(left) + r0 <- exprAndTypeAsExpr(ctx, info)(right) + (l, r) = alignIntKinds(l0, r0) } yield in.UneqCmp(l, r)(src) } @@ -2704,7 +2784,7 @@ object Desugar extends LazyLogging { // E.g. the right-hand side of perm(1/2) == 1/2 is treated as a permission. for { l <- permissionD(ctx, info)(left); r <- permissionD(ctx, info)(right) } yield in.GhostEqCmp(l, r)(src) } else { - for { l <- exprD(ctx, info)(left); r <- exprD(ctx, info)(right) } yield in.GhostEqCmp(l, r)(src) + for { l0 <- exprD(ctx, info)(left); r0 <- exprD(ctx, info)(right); (l, r) = alignIntKinds(l0, r0) } yield in.GhostEqCmp(l, r)(src) } case PGhostUnequals(left, right) => @@ -2714,35 +2794,35 @@ object Desugar extends LazyLogging { // E.g. the right-hand side of perm(1/2) == 1/2 is treated as a permission. for { l <- permissionD(ctx, info)(left); r <- permissionD(ctx, info)(right) } yield in.GhostUneqCmp(l, r)(src) } else { - for { l <- exprD(ctx, info)(left); r <- exprD(ctx, info)(right) } yield in.GhostUneqCmp(l, r)(src) + for { l0 <- exprD(ctx, info)(left); r0 <- exprD(ctx, info)(right); (l, r) = alignIntKinds(l0, r0) } yield in.GhostUneqCmp(l, r)(src) } case PLess(left, right) => if (info.typ(left) == PermissionT || info.typ(right) == PermissionT) { for {l <- permissionD(ctx, info)(left); r <- permissionD(ctx, info)(right)} yield in.PermLtCmp(l, r)(src) } else { - for {l <- go(left); r <- go(right)} yield in.LessCmp(l, r)(src) + for {l0 <- go(left); r0 <- go(right); (l, r) = alignIntKinds(l0, r0)} yield in.LessCmp(l, r)(src) } case PAtMost(left, right) => if (info.typ(left) == PermissionT || info.typ(right) == PermissionT) { for {l <- permissionD(ctx, info)(left); r <- permissionD(ctx, info)(right)} yield in.PermLeCmp(l, r)(src) } else { - for {l <- go(left); r <- go(right)} yield in.AtMostCmp(l, r)(src) + for {l0 <- go(left); r0 <- go(right); (l, r) = alignIntKinds(l0, r0)} yield in.AtMostCmp(l, r)(src) } case PGreater(left, right) => if (info.typ(left) == PermissionT || info.typ(right) == PermissionT) { for {l <- permissionD(ctx, info)(left); r <- permissionD(ctx, info)(right)} yield in.PermGtCmp(l, r)(src) } else { - for {l <- go(left); r <- go(right)} yield in.GreaterCmp(l, r)(src) + for {l0 <- go(left); r0 <- go(right); (l, r) = alignIntKinds(l0, r0)} yield in.GreaterCmp(l, r)(src) } case PAtLeast(left, right) => if (info.typ(left) == PermissionT || info.typ(right) == PermissionT) { for {l <- permissionD(ctx, info)(left); r <- permissionD(ctx, info)(right)} yield in.PermGeCmp(l, r)(src) } else { - for {l <- go(left); r <- go(right)} yield in.AtLeastCmp(l, r)(src) + for {l0 <- go(left); r0 <- go(right); (l, r) = alignIntKinds(l0, r0)} yield in.AtLeastCmp(l, r)(src) } case PAnd(left, right) => @@ -2809,12 +2889,12 @@ object Desugar extends LazyLogging { // both operands are statically evaluable so that `var d int = 1 << 2` becomes // `d := 4` in Viper, making `assert(d == 4)` provable. info.intConstantEvaluation(e) match { - case Some(v) => unit(in.IntLit(v)(src)) + case Some(v) => unit(in.IntLit(v, inferredIntKind(info)(e))(src)) case None => for {l <- go(e.left); r <- go(e.right)} yield in.ShiftLeft(l, r)(src) } case e: PShiftRight => info.intConstantEvaluation(e) match { - case Some(v) => unit(in.IntLit(v)(src)) + case Some(v) => unit(in.IntLit(v, inferredIntKind(info)(e))(src)) case None => for {l <- go(e.left); r <- go(e.right)} yield in.ShiftRight(l, r)(src) } case PBitNegation(exp) => for {e <- go(exp)} yield in.BitNeg(e)(src) @@ -2835,9 +2915,14 @@ object Desugar extends LazyLogging { case PSliceExp(base, low, high, cap) => for { dbase <- go(base) - dlow <- option(low map go) - dhigh <- option(high map go) - dcap <- option(cap map go) + dlowRaw <- option(low map go) + dhighRaw <- option(high map go) + dcapRaw <- option(cap map go) + // Slice / sequence bounds are passed to the encoding as Viper Int, so demote any + // bounded-integer kind that the frontend may have inferred for the bound expression. + dlow = dlowRaw.map(viper.gobra.ast.internal.utility.IntKindAlignment.asUnboundedInt) + dhigh = dhighRaw.map(viper.gobra.ast.internal.utility.IntKindAlignment.asUnboundedInt) + dcap = dcapRaw.map(viper.gobra.ast.internal.utility.IntKindAlignment.asUnboundedInt) } yield underlyingType(dbase.typ) match { case _: in.SequenceT => (dlow, dhigh) match { case (None, None) => dbase @@ -3074,13 +3159,23 @@ object Desugar extends LazyLogging { } + /** Returns the IntegerKind inferred for an expression, or UnboundedInteger if it isn't an integer type. */ + def inferredIntKind(info: TypeInfo)(e: PExpression): viper.gobra.util.TypeBounds.IntegerKind = info.typ(e) match { + case Type.IntT(k) => k + case _ => viper.gobra.util.TypeBounds.UnboundedInteger + } + def litD(ctx: FunctionContext, info: TypeInfo)(lit: PLiteral): Writer[in.Expr] = { val src: Meta = meta(lit, info) def single[E <: in.Expr](gen: Meta => E): Writer[in.Expr] = unit[in.Expr](gen(src)) lit match { - case PIntLit(v, base) => single(in.IntLit(v, base = base)) + case lit @ PIntLit(v, base) => + // Propagate the frontend-inferred integer kind (e.g. int8 in `0 < x` where x: int8) + // so the internal IntLit's typ matches its sibling's. Without this, BoundedIntEncoding + // sees mixed-kind operands (Int vs Bounded_int8) and Silicon rejects the Viper output. + single(in.IntLit(v, inferredIntKind(info)(lit), base)) case PBoolLit(b) => single(in.BoolLit(b)) case PStringLit(s) => single(in.StringLit(s)) case nil: PNilLit => single(in.NilLit(typeD(info.nilType(nil).getOrElse(Type.ActualPointerT(Type.BooleanT)), Addressability.literal)(src))) // if no type is found, then use *bool @@ -3294,7 +3389,7 @@ object Desugar extends LazyLogging { sequence( lit.elems map { case PKeyedElement(Some(key), value) => for { - entryKey <- key match { + entryKeyRaw <- key match { case v: PCompositeVal => compositeValD(ctx, info)(v, keys) case k: PIdentifierKey => info.regular(k.id) match { case _: st.Variable => unit(varD(ctx, info)(k.id)) @@ -3302,7 +3397,13 @@ object Desugar extends LazyLogging { case _ => violation(s"unexpected key $key") } } - entryVal <- compositeValD(ctx, info)(value, values) + // Identifier keys (variables/constants) bypass compositeValD's implicit + // conversion, so their integer kind may not match the declared key type. + entryKey = alignElementKind(entryKeyRaw, keys) + // Align the value's integer kind with the declared value type as well: an + // untyped literal value (e.g. `5` in `dict[string]int{"k": 5}`) may otherwise + // keep a kind whose Viper sort differs from the map's value sort. + entryVal <- compositeValD(ctx, info)(value, values).map(alignElementKind(_, values)) } yield (entryKey, entryVal) case _ => violation("unexpected pattern, missing key in map literal") @@ -4509,17 +4610,24 @@ object Desugar extends LazyLogging { case PBefore(op) => for {o <- go(op)} yield in.LabeledOld(in.LabelProxy("before")(src), o)(src) case PConditional(cond, thn, els) => for { wcond <- go(cond) - wthn <- go(thn) - wels <- go(els) + wthnRaw <- go(thn) + welsRaw <- go(els) + (wthn, wels) = viper.gobra.ast.internal.utility.IntKindAlignment.alignIntKinds(wthnRaw, welsRaw) } yield in.Conditional(wcond, wthn, wels, typ)(src) case PLet(ass, op) => val dOp = pureExprD(ctx, info)(op) unit((ass.left zip ass.right).foldRight(dOp)((lr, letop) => { val right = pureExprD(ctx, info)(lr._2) + // The binder is typed with the variable's frontend type, not right.typ: the two can + // differ in integer kind (e.g. `let x := len(b)` — the variable is a bounded `int` + // while the internal length expression is unbounded), and the body's references to + // the variable are resolved at the frontend type. The encoding aligns the bound + // right-hand side with the binder's sort (see AssertionEncoding.alignLetBinding). + val leftTyp = typeD(info.typ(lr._1), Addressability.exclusiveVariable)(src) val left = in.LocalVar( nm.variable(lr._1.name, info.scope(lr._1), info), - right.typ.withAddressability(Addressability.exclusiveVariable) + leftTyp )(src) in.PureLet(left, right, letop)(src) })) @@ -4553,8 +4661,17 @@ object Desugar extends LazyLogging { } yield in.Rel(dExp, dLit.asInstanceOf[in.IntLit])(src) case PElem(left, right) => for { - dleft <- go(left) + dleftRaw <- go(left) dright <- go(right) + // Align the element kind with the container's element type to avoid mixed-kind + // arguments at the encoding level (e.g. `0 elem set[int]{...}` where 0 is untyped). + dleft = underlyingType(dright.typ) match { + case t: in.SequenceT => alignElementKind(dleftRaw, t.t) + case t: in.SetT => alignElementKind(dleftRaw, t.t) + case t: in.MultisetT => alignElementKind(dleftRaw, t.t) + case t: in.MapT => alignElementKind(dleftRaw, t.keys) + case _ => dleftRaw + } } yield underlyingType(dright.typ) match { case _: in.SequenceT | _: in.SetT => in.Contains(dleft, dright)(src) case _: in.MultisetT => in.LessCmp(in.IntLit(0)(src), in.Contains(dleft, dright)(src))(src) @@ -4563,14 +4680,36 @@ object Desugar extends LazyLogging { } case PMultiplicity(left, right) => for { - dleft <- go(left) + dleftRaw <- go(left) dright <- go(right) + // Align the searched element's kind with the collection's element type (like PElem); + // e.g. in `42 # m` with `m: mset[int]`, the literal must become a bounded int. + dleft = underlyingType(dright.typ) match { + case t: in.SequenceT => alignElementKind(dleftRaw, t.t) + case t: in.MultisetT => alignElementKind(dleftRaw, t.t) + case t: in.SetT => alignElementKind(dleftRaw, t.t) + case _ => dleftRaw + } } yield in.Multiplicity(dleft, dright)(src) case PRangeSequence(low, high) => for { dlow <- go(low) dhigh <- go(high) - } yield in.RangeSequence(dlow, dhigh)(src) + // The bounds must be mathematical integers (Viper's RangeSeq is over Int) … + rng = in.RangeSequence( + viper.gobra.ast.internal.utility.IntKindAlignment.asUnboundedInt(dlow), + viper.gobra.ast.internal.utility.IntKindAlignment.asUnboundedInt(dhigh))(src) + } yield typ match { + // … but the *element* kind follows the frontend-inferred type: `seq[1..4]` in an + // `int` context has bounded elements, so wrap with a Conversion that the sequence + // encoding turns into the per-kind `Seq[Int] -> Seq[Bounded_k]` mapping function. + // Without this, `seq[1..4] ++ seq[int]{1}` etc. mixes Viper sorts. + case in.SequenceT(in.IntT(_, k: viper.gobra.util.TypeBounds.BoundedIntegerKind), _) => + in.Conversion( + in.SequenceT(in.IntT(Addressability.mathDataStructureElement, k), Addressability.rValue), + rng)(src) + case _ => rng + } case PSequenceAppend(left, right) => for { dleft <- go(left) @@ -4581,8 +4720,20 @@ object Desugar extends LazyLogging { case (dcol, clause) => for { dcolExp <- dcol baseUnderlyingType = underlyingType(dcolExp.typ) - dleft <- go(clause.left) - dright <- go(clause.right) + dleftRaw <- go(clause.left) + drightRaw <- go(clause.right) + // Align the key/index and the value with the collection's declared types: + // sequence indices must be mathematical integers (Viper Int), while values, map + // keys, and map values must match the (possibly bounded) element kind. + (dleft, dright) = baseUnderlyingType match { + case t: in.SequenceT => + (viper.gobra.ast.internal.utility.IntKindAlignment.asUnboundedInt(dleftRaw), alignElementKind(drightRaw, t.t)) + case t: in.MathMapT => + (alignElementKind(dleftRaw, t.keys), alignElementKind(drightRaw, t.values)) + case t: in.MapT => + (alignElementKind(dleftRaw, t.keys), alignElementKind(drightRaw, t.values)) + case _ => (dleftRaw, drightRaw) + } } yield in.GhostCollectionUpdate(dcol.res, dleft, dright, baseUnderlyingType)(src) } @@ -4647,10 +4798,13 @@ object Desugar extends LazyLogging { } yield in.OptionGet(dop)(src) case m: PMatchExp => + // The match expression is encoded as a chain of Viper conditionals, so every case + // body (and the default) must land in the same Viper sort as the overall type — + // align integer kinds (e.g. an untyped literal body vs. a bounded `int` match type). val defaultD: Writer[Option[in.Expr]] = if (m.hasDefault) { for { e <- exprD(ctx, info)(m.defaultClauses.head.exp) - } yield Some(e) + } yield Some(alignElementKind(e, typ)) } else { unit(None) } @@ -4658,7 +4812,7 @@ object Desugar extends LazyLogging { def caseD(c: PMatchExpCase): Writer[in.PatternMatchCaseExp] = for { p <- matchPatternD(ctx, info)(c.pattern) e <- exprD(ctx, info)(c.exp) - } yield in.PatternMatchCaseExp(p, e)(src) + } yield in.PatternMatchCaseExp(p, alignElementKind(e, typ))(src) for { e <- exprD(ctx, info)(m.exp) @@ -4719,6 +4873,11 @@ object Desugar extends LazyLogging { } yield (newVars, newTriggers, newBody) } + // A quantifier variable is bound at its declared type. For a bounded integer kind this means + // the variable ranges over exactly the values of that type (`forall x uint8 :: x >= 0` holds); + // the encoding lowers such variables to `Int`-sorted Viper variables with an explicit range + // guard so that quantified permissions keep linear, injective receivers (see the bounded + // bound-variable lowering in AssertionEncoding). def boundVariableD(x: PBoundVariable) : in.BoundVar = in.BoundVar(idName(x.id, info), typeD(info.symbType(x.typ), Addressability.boundVariable)(meta(x, info)))(meta(x, info)) @@ -4810,9 +4969,11 @@ object Desugar extends LazyLogging { dOp <- assertionD(ctx, info)(op) lets = (ass.left zip ass.right).foldRight(dOp)((lr, letop) => { val right = pureExprD(ctx, info)(lr._2) + // binder typed at the frontend type — see the PureLet case for the rationale + val leftTyp = typeD(info.typ(lr._1), Addressability.exclusiveVariable)(src) val left = in.LocalVar( nm.variable(lr._1.name, info.scope(lr._1), info), - right.typ.withAddressability(Addressability.exclusiveVariable) + leftTyp )(src) in.Let(left, right, letop)(src) }) @@ -5025,7 +5186,10 @@ object Desugar extends LazyLogging { if (info.typ(num) == PermissionT) Some(for { vp <- permissionD(ctx, info)(num); vd <- goE(den) } yield in.PermConstructorFromPerm(vp, vd)(src)) else - Some(for { vn <- goE(num); vd <- goE(den) } yield in.PermConstructorFromInt(vn, vd)(src)) + Some(for { + vn <- goE(num).map(viper.gobra.ast.internal.utility.IntKindAlignment.asUnboundedInt) + vd <- goE(den).map(viper.gobra.ast.internal.utility.IntKindAlignment.asUnboundedInt) + } yield in.PermConstructorFromInt(vn, vd)(src)) case _ => None } case PFullPerm() => Some(unit(in.FullPerm(src))) diff --git a/src/main/scala/viper/gobra/frontend/PackageResolver.scala b/src/main/scala/viper/gobra/frontend/PackageResolver.scala index fe597f63f..1d1a3b3e7 100644 --- a/src/main/scala/viper/gobra/frontend/PackageResolver.scala +++ b/src/main/scala/viper/gobra/frontend/PackageResolver.scala @@ -57,7 +57,15 @@ object PackageResolver { case Left(_) => NoPackage(imp) case Right(inputResource) => try { - RegularPackage(Source.uniquePath(inputResource.path, config.projectRoot).toString) + // Normalize both paths before deriving the package id: the same directory + // reached through different import spellings (e.g. "encoding/binary" via an + // include path vs "verification/dependencies/encoding/binary" via the project + // root) must map to the same package. Without normalization the two spellings + // produce distinct ids ("./x/y" vs "x/y"), the package is parsed, checked, and + // encoded twice, and the resulting Viper program contains duplicate identifiers. + val normalizedPath = inputResource.path.toAbsolutePath.normalize() + val normalizedRoot = config.projectRoot.toAbsolutePath.normalize() + RegularPackage(Source.uniquePath(normalizedPath, normalizedRoot).toString) } catch { case _: Throwable => NoPackage(imp) } } } diff --git a/src/main/scala/viper/gobra/translator/Names.scala b/src/main/scala/viper/gobra/translator/Names.scala index f1d22e249..bc7fa9aae 100644 --- a/src/main/scala/viper/gobra/translator/Names.scala +++ b/src/main/scala/viper/gobra/translator/Names.scala @@ -8,6 +8,7 @@ package viper.gobra.translator import viper.gobra.ast.{internal => in} import viper.gobra.theory.Addressability +import viper.gobra.util.TypeBounds.{BoundedIntegerKind, IntegerKind} import viper.gobra.util.Violation import viper.silver.{ast => vpr} @@ -181,7 +182,47 @@ object Names { // built-in members def builtInMember: String = "built_in" - // ints + // bounded integer domains: one domain per IntegerKind + // Use a "Bounded_" prefix to avoid clashing with Viper's built-in Int sort when + // the kind name (e.g. "int") would otherwise produce a sort named "int~_int", + // which Silicon conflates with "Int~_Int" (Viper's built-in mathematical integer). + def boundedIntDomain(k: IntegerKind): String = s"Bounded_${k.name}" + // bounded integer functions (abstract, with contracts) + def boundedIntFrom(k: IntegerKind): String = s"${k.name}$$from" + def boundedIntInv(k: IntegerKind): String = s"${k.name}$$inv" + def boundedIntTo(k: IntegerKind): String = s"${k.name}$$to" + def boundedIntAdd(k: IntegerKind): String = s"${k.name}$$add" + def boundedIntSub(k: IntegerKind): String = s"${k.name}$$sub" + def boundedIntMul(k: IntegerKind): String = s"${k.name}$$mul" + def boundedIntDiv(k: IntegerKind): String = s"${k.name}$$div" + def boundedIntMod(k: IntegerKind): String = s"${k.name}$$mod" + def boundedIntBand(k: IntegerKind): String = s"${k.name}$$band" + def boundedIntBor(k: IntegerKind): String = s"${k.name}$$bor" + def boundedIntBxor(k: IntegerKind): String = s"${k.name}$$bxor" + def boundedIntBclear(k: IntegerKind): String = s"${k.name}$$bclear" + def boundedIntBneg(k: IntegerKind): String = s"${k.name}$$bneg" + def boundedIntShl(k: IntegerKind): String = s"${k.name}$$shl" + def boundedIntShr(k: IntegerKind): String = s"${k.name}$$shr" + // unbounded → bounded conversion (also reused for bounded → bounded via from-composition) + def integerToBounded(to: BoundedIntegerKind): String = s"integer$$to_${to.name}" + + private val boundedIntHelperSuffixes = + Set("add", "sub", "mul", "div", "mod", "band", "bor", "bxor", "bclear", "bneg", "shl", "shr") + + /** + * True iff `name` is one of the bounded-integer arithmetic/bitwise/shift helper functions + * (`$add`, ...) or an `integer$to_` conversion. These are Viper functions, but + * semantically they are arithmetic — in particular, they must not appear in quantifier + * triggers, just like `+` or `*`. + */ + def isBoundedIntArithHelper(name: String): Boolean = { + val i = name.lastIndexOf('$') + (i >= 0 && boundedIntHelperSuffixes.contains(name.substring(i + 1))) || + name.startsWith("integer$to_") + } + + // kept for legacy uses (bitwise ops on unbounded integers are now a type error; only kept for + // shift / bitwise functions on bounded integer types which use the bounded naming above) def bitwiseAnd: String = "intBitwiseAnd" def bitwiseOr: String = "intBitwiseOr" def bitwiseXor: String = "intBitwiseXor" diff --git a/src/main/scala/viper/gobra/translator/Translator.scala b/src/main/scala/viper/gobra/translator/Translator.scala index 4bbded4d7..a92eb0744 100644 --- a/src/main/scala/viper/gobra/translator/Translator.scala +++ b/src/main/scala/viper/gobra/translator/Translator.scala @@ -32,6 +32,23 @@ object Translator { ConsistencyError(err.readableMessage, pos) }).toVector + // The bounded-integer arithmetic helpers are Viper functions, but semantically they are + // arithmetic: automatic trigger inference must not pick terms containing them (just like + // terms containing `+`), otherwise quantifiers over spec arithmetic like + // `forall i, j :: a[i][j] == i + j` end up with helper-application triggers that no ground + // term ever matches (constant arithmetic is folded). Installing the predicate on silver's + // global default trigger generation is idempotent and harmless under --unboundedIntegers, + // where the helper names never occur. + // Both hooks are needed: `isPossibleTrigger` stops helper applications from being picked as + // candidate trigger terms themselves, `isForbiddenInTrigger` makes them get factored out of + // larger candidate terms (like `+` would be). + viper.silver.ast.utility.Triggers.DefaultTriggerGeneration.setCustomIsPossibleTrigger { + case app: vpr.FuncApp if Names.isBoundedIntArithHelper(app.funcname) => false + } + viper.silver.ast.utility.Triggers.DefaultTriggerGeneration.setCustomIsForbiddenInTrigger { + case app: vpr.FuncApp if Names.isBoundedIntArithHelper(app.funcname) => true + } + def translate(program: Program, pkgInfo: PackageInfo)(config: Config): Either[Vector[VerifierError], BackendVerifier.Task] = { val translationConfig = new DfltTranslatorConfig()(config) val programTranslator = new ProgramsImpl() diff --git a/src/main/scala/viper/gobra/translator/context/Context.scala b/src/main/scala/viper/gobra/translator/context/Context.scala index 224a5c613..81091ec17 100644 --- a/src/main/scala/viper/gobra/translator/context/Context.scala +++ b/src/main/scala/viper/gobra/translator/context/Context.scala @@ -57,6 +57,9 @@ trait Context { def defaultEncoding: DefaultEncoding + /** When enabled, all integer types (including bounded ones) are encoded as Viper's unbounded Int. */ + def unboundedIntegers: Boolean + def typ(x: in.Type): vpr.Type = typeEncoding.typ(this)(x) def variable(x: in.BodyVar): vpr.LocalVarDecl = typeEncoding.variable(this)(x) diff --git a/src/main/scala/viper/gobra/translator/context/ContextImpl.scala b/src/main/scala/viper/gobra/translator/context/ContextImpl.scala index 1bfa6abc0..6b154ce08 100644 --- a/src/main/scala/viper/gobra/translator/context/ContextImpl.scala +++ b/src/main/scala/viper/gobra/translator/context/ContextImpl.scala @@ -39,6 +39,7 @@ case class ContextImpl( typeEncoding: TypeEncoding, defaultEncoding: DefaultEncoding, table: LookupTable, + unboundedIntegers: Boolean = false, initialFreshCounterValue: Int = 0 ) extends Context { @@ -60,6 +61,7 @@ case class ContextImpl( conf.typeEncoding, conf.defaultEncoding, table, + conf.unboundedIntegers, ) } diff --git a/src/main/scala/viper/gobra/translator/context/DfltTranslatorConfig.scala b/src/main/scala/viper/gobra/translator/context/DfltTranslatorConfig.scala index e379ce708..3e2c2bfa7 100644 --- a/src/main/scala/viper/gobra/translator/context/DfltTranslatorConfig.scala +++ b/src/main/scala/viper/gobra/translator/context/DfltTranslatorConfig.scala @@ -54,7 +54,19 @@ class DfltTranslatorConfig( val seqToMultiset : SeqToMultiset = new SeqToMultisetImpl(seqMultiplicity) val optionToSeq : OptionToSeq = new OptionToSeqImpl(option) - val slice : Slices = new SlicesImpl(array) + + val unboundedIntegers: Boolean = config.unboundedIntegers + + // Go guarantees that len/cap of arrays, slices, and strings fit in `int`; under bounded + // integer semantics the container libraries axiomatize this upper bound. + private val intUpperBound: Option[BigInt] = + if (config.unboundedIntegers) None else config.typeBounds.Int match { + case k: viper.gobra.util.TypeBounds.BoundedIntegerKind => Some(k.upper) + case _ => None + } + array.intUpperBound = intUpperBound + + val slice : Slices = new SlicesImpl(array, intUpperBound) val arrayEncoding: ArrayEncoding = new ArrayEncoding() @@ -66,13 +78,13 @@ class DfltTranslatorConfig( val typeEncoding: TypeEncoding = new FinalTypeEncoding( new SafeTypeEncodingCombiner(Vector( - new BoolEncoding, new IntEncoding, new PermissionEncoding, + new BoolEncoding, new BoundedIntEncoding(config.checkOverflows), new IntEncoding, new PermissionEncoding, new PointerEncoding, new StructEncoding, arrayEncoding, new ClosureEncoding(config), new InterfaceEncoding, new SequenceEncoding, new SetEncoding, new OptionEncoding, new DomainEncoding, new AdtEncoding, - new SliceEncoding(arrayEncoding), new PredEncoding, new ChannelEncoding, new StringEncoding, + new SliceEncoding(arrayEncoding), new PredEncoding, new ChannelEncoding(config.typeBounds.Int), new StringEncoding(intUpperBound), new MapEncoding, new MathematicalMapEncoding, new FloatEncoding, new AssertionEncoding, new CallEncoding, new MemoryEncoding, new ControlEncoding, - new TerminationEncoding, new BuiltInEncoding, new OutlineEncoding, new CriticalEncoding, new DeferEncoding, + new TerminationEncoding, new BuiltInEncoding(config.typeBounds.Int), new OutlineEncoding, new CriticalEncoding, new DeferEncoding, new GlobalEncoding, new Comments, ), Vector( methodEncoding, pureMethodEncoding, predicateEncoding, globalVarEncoding, triggerExprEncoding diff --git a/src/main/scala/viper/gobra/translator/context/TranslatorConfig.scala b/src/main/scala/viper/gobra/translator/context/TranslatorConfig.scala index f77dcab2e..8e2a665a4 100644 --- a/src/main/scala/viper/gobra/translator/context/TranslatorConfig.scala +++ b/src/main/scala/viper/gobra/translator/context/TranslatorConfig.scala @@ -51,4 +51,8 @@ trait TranslatorConfig { def typeEncoding: TypeEncoding def defaultEncoding: DefaultEncoding + + // options + /** When enabled, all integer types are encoded as Viper's unbounded Int (see Config.unboundedIntegers). */ + def unboundedIntegers: Boolean } diff --git a/src/main/scala/viper/gobra/translator/encodings/BoundedIntEncoding.scala b/src/main/scala/viper/gobra/translator/encodings/BoundedIntEncoding.scala new file mode 100644 index 000000000..a84675a73 --- /dev/null +++ b/src/main/scala/viper/gobra/translator/encodings/BoundedIntEncoding.scala @@ -0,0 +1,732 @@ +// 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-2020 ETH Zurich. + +package viper.gobra.translator.encodings + +import org.bitbucket.inkytonik.kiama.==> +import viper.gobra.ast.{internal => in} +import viper.gobra.reporting.BackTranslator.RichErrorMessage +import viper.gobra.reporting.{OverflowError, ShiftPreconditionError, Source} +import viper.gobra.theory.Addressability.{Exclusive, Shared} +import viper.gobra.translator.Names +import viper.gobra.translator.encodings.combinators.LeafTypeEncoding +import viper.gobra.translator.context.Context +import viper.gobra.translator.util.ViperWriter.CodeWriter +import viper.gobra.util.TypeBounds.BoundedIntegerKind +import viper.silver.{ast => vpr} +import viper.silver.plugin.standard.termination +import viper.silver.verifier.{errors => err} + +import scala.collection.mutable + +/** + * Encoding for bounded integer types (int8, uint8, int16, uint16, int32, uint32, int64, uint64, byte, rune, uintptr). + * + * Each bounded integer kind is encoded as its own opaque Viper domain type (e.g. `domain Bounded_int8 {}`). + * A pair of domain functions `int8$from` and `int8$to`, characterised by two domain axioms, bridge + * between the domain type and mathematical integers (`Int`): + * - `int8$from(x: Bounded_int8): Int` — axiom: -128 <= from(x) <= 127 (trigger `{ from(x) }`) + * - `int8$to(x: Int): Bounded_int8` — axiom: inRange(n) ==> from(to(n)) == n (trigger `{ to(n) }`) + * + * IMPORTANT (matching loops): the abstract arithmetic/bitwise/shift helper functions operate on + * `Int` arguments — never on the domain type. This is deliberate. If those helpers took + * domain-typed parameters and referred to `from(x)`/`from(y)` of their (universally quantified) + * parameters in their postconditions, Silicon would synthesise quantified axioms reachable from + * `from`-application triggers. Because `from` appears on essentially every bounded term, Z3's + * e-matching would keep instantiating those axioms against the `to`/`from` bridge axioms forever + * — a classic matching loop, which manifested as Gobra hanging on the StatsCollector tests. + * + * By keeping the helpers over `Int` and applying `from`/`to` only at concrete (ground) call sites, + * the only quantified axioms over the domain are `from`'s range axiom, `from`'s injectivity axiom + * (whose body creates no terms at all), and `to`'s inverse axiom — none of which generates a term + * matching another's trigger. A bounded binary operation + * `l r` of kind `k` is therefore encoded as `k$to( k$op( k$from(l), k$from(r) ) )`, where the + * helper `k$op` carries the range contract (and, when `checkOverflows`, the range precondition + * that makes overflow a verification error) and `k$to` merely lifts the in-range `Int` back into + * the domain. Without `checkOverflows`, the result value is known exactly only under the + * no-overflow condition (`rangeOk ==> result == computed`): proofs about possibly-overflowing + * arithmetic are meant to break unless the specs exclude the overflow. + */ +class BoundedIntEncoding(checkOverflows: Boolean) extends LeafTypeEncoding { + + import viper.gobra.translator.util.TypePatterns._ + import viper.gobra.translator.util.ViperWriter.CodeLevel._ + + // ===== Per-kind generated functions ===== + + private case class KindFunctions( + from: vpr.DomainFunc, // (x: domType): Int — DOMAIN function; characterised by domain axioms + inv: vpr.DomainFunc, // (n: Int): domType — Skolem inverse witnessing from's injectivity + to: vpr.DomainFunc, // (x: Int): domType — DOMAIN function; characterised by domain axioms + rangeAxiom: vpr.DomainAxiom, // forall x: domType :: lower <= from(x) && from(x) <= upper + invAxiom: vpr.DomainAxiom, // forall x: domType :: { from(x) } inv(from(x)) == x + toFromAxiom: vpr.DomainAxiom, // forall n: Int :: { to(n) } inRange(n) ==> from(to(n)) == n + invFromAxiom: vpr.DomainAxiom, // forall n: Int :: { inv(n) } inRange(n) ==> from(inv(n)) == n + add: vpr.Function, // (x y: Int): Int + sub: vpr.Function, // (x y: Int): Int + mul: vpr.Function, // (x y: Int): Int + div: vpr.Function, // (x y: Int): Int + mod: vpr.Function, // (x y: Int): Int + band: vpr.Function, // (x y: Int): Int + bor: vpr.Function, // (x y: Int): Int + bxor: vpr.Function, // (x y: Int): Int + bclear: vpr.Function, // (x y: Int): Int + bneg: vpr.Function, // (x: Int): Int + shl: vpr.Function, // (x: Int, shift: Int): Int + shr: vpr.Function // (x: Int, shift: Int): Int + ) { + /** Top-level Viper functions emitted for this kind (not the domain members). */ + def topLevelFns: Seq[vpr.Function] = + Seq(add, sub, mul, div, mod, band, bor, bxor, bclear, bneg, shl, shr) + } + + private val kindCache: mutable.Map[BoundedIntegerKind, KindFunctions] = mutable.Map.empty + private val intToBoundedCache: mutable.Map[BoundedIntegerKind, vpr.Function] = mutable.Map.empty + + private def funcsOf(k: BoundedIntegerKind): KindFunctions = + kindCache.getOrElseUpdate(k, buildKindFunctions(k)) + + // ===== Type translation ===== + + override def typ(ctx: Context): in.Type ==> vpr.Type = { + case ctx.BoundedInt(k) / Exclusive => + // Touch the cache so finalize emits the domain — otherwise a kind referenced only via + // its type (e.g. a `byte` function parameter that's never used in arithmetic) would + // produce a Viper file mentioning `Bounded_byte` without declaring the domain. + funcsOf(k) + domainType(k) + case ctx.BoundedInt(_) / Shared => vpr.Ref + } + + // ===== Assignment: normalise RHS to the domain type of the LHS ===== + // Needed because Gobra's internal AST allows unbounded-integer RHS expressions (e.g. IntLit + // with kind=UnboundedInteger) to be assigned to bounded-integer variables without explicit + // conversion. The default encoding would produce a Viper type mismatch (Int → DomainType). + + override def assignment(ctx: Context): (in.Assignee, in.Expr, in.Node) ==> CodeWriter[vpr.Stmt] = + default(super.assignment(ctx)) { + case (in.Assignee((v: in.BodyVar) :: t / Exclusive), rhs, src) + if ctx.BoundedInt.unapply(t).isDefined && !ctx.BoundedInt.unapply(rhs.typ).isDefined => + val k = ctx.BoundedInt.unapply(t).get + val (pos, info, errT) = src.vprMeta + for { vRhs <- ctx.expression(rhs) } yield + vpr.LocalVarAssign(variable(ctx)(v).localVar, asDomain(ctx)(k, rhs, vRhs))(pos, info, errT) + + case (in.Assignee((loc: in.Location) :: t / Shared), rhs, src) + if ctx.BoundedInt.unapply(t).isDefined && !ctx.BoundedInt.unapply(rhs.typ).isDefined => + val k = ctx.BoundedInt.unapply(t).get + val (pos, info, errT) = src.vprMeta + for { + vRhs <- ctx.expression(rhs) + vLoc <- ctx.expression(loc).map(_.asInstanceOf[vpr.FieldAccess]) + } yield vpr.FieldAssign(vLoc, asDomain(ctx)(k, rhs, vRhs))(pos, info, errT) + } + + // ===== Equal: compare the `from`-images so domain values are compared mathematically ===== + + /** True iff `e` has some integer type (bounded, `integer`, or untyped constant). */ + private def isIntegerTyped(ctx: Context)(e: in.Expr): Boolean = + ctx.BoundedInt.unapply(e.typ).isDefined || ctx.UnboundedInt.unapply(e.typ) + + /** + * True iff BOTH expressions are integer-typed and at least one is bounded. Claiming a + * comparison/equality on a bounded operand alone is too greedy: `x == itf` compares a + * bounded int against an interface value (boxing equality), which belongs to the + * interface encoding — claiming it here made the combiner report the node as supported + * by more than one encoding. + */ + private def hasBoundedOperand(ctx: Context)(l: in.Expr, r: in.Expr): Boolean = + (ctx.BoundedInt.unapply(l.typ).isDefined || ctx.BoundedInt.unapply(r.typ).isDefined) && + isIntegerTyped(ctx)(l) && isIntegerTyped(ctx)(r) + + // A bounded operand can appear on EITHER side (e.g. `0 == c.BufferSize()`, or mixed + // bounded/unbounded siblings the desugarer did not align). Both operands are projected + // to Int, applying `from` to the bounded one(s). + override def equal(ctx: Context): (in.Expr, in.Expr, in.Node) ==> CodeWriter[vpr.Exp] = + default(super.equal(ctx)) { + case (lhs, rhs, src) if hasBoundedOperand(ctx)(lhs, rhs) => + val (pos, info, errT) = src.vprMeta + for { + vLhs <- ctx.expression(lhs) + vRhs <- ctx.expression(rhs) + } yield vpr.EqCmp(asInt(ctx)(lhs, vLhs), asInt(ctx)(rhs, vRhs))(pos, info, errT): vpr.Exp + } + + // ===== Expression encoding ===== + + override def expression(ctx: Context): in.Expr ==> CodeWriter[vpr.Exp] = { + + def goE(x: in.Expr): CodeWriter[vpr.Exp] = ctx.expression(x) + + // Encodes `left right` of kind `k` as `to(helper(from(left), from(right)))`. The helper + // operates on Int and carries the range contract; `to` lifts the in-range result back into the + // domain. The overflow error (if any) is attributed to the helper application. + def handleBoundedBinOp(k: BoundedIntegerKind, helper: vpr.Function)(left: in.Expr, right: in.Expr, src: in.Node): CodeWriter[vpr.Exp] = { + val (pos, info, errT) = src.vprMeta + for { + vl <- goE(left) + vr <- goE(right) + app = vpr.FuncApp(helper, Seq(asInt(ctx)(left, vl), asInt(ctx)(right, vr)))(pos, info, errT) + _ <- if (checkOverflows) errorT { + case e @ err.PreconditionInAppFalse(Source(info), _, _) if e.causedBy(app) => + OverflowError(info) + } else unit(()) + } yield toApp(k, app, pos, info, errT) + } + + default(super.expression(ctx)) { + + // Default value: to(0) — the domain value corresponding to integer 0 + case (e: in.DfltVal) :: ctx.BoundedInt(k) / Exclusive => + val (pos, info, errT) = e.vprMeta + unit(vpr.DomainFuncApp(funcsOf(k).to, Seq(zero), Map.empty)(pos, info, errT)) + + // Integer literal of bounded type: to(lit.v) + case (lit: in.IntLit) :: ctx.BoundedInt(k) => + val (pos, info, errT) = lit.vprMeta + unit(vpr.DomainFuncApp(funcsOf(k).to, Seq(vpr.IntLit(lit.v)()), Map.empty)(pos, info, errT)) + + // Arithmetic + case e @ in.Add(l, r) :: ctx.BoundedInt(k) => handleBoundedBinOp(k, funcsOf(k).add)(l, r, e) + case e @ in.Sub(l, r) :: ctx.BoundedInt(k) => handleBoundedBinOp(k, funcsOf(k).sub)(l, r, e) + case e @ in.Mul(l, r) :: ctx.BoundedInt(k) => handleBoundedBinOp(k, funcsOf(k).mul)(l, r, e) + case e @ in.Div(l, r) :: ctx.BoundedInt(k) => handleBoundedBinOp(k, funcsOf(k).div)(l, r, e) + case e @ in.Mod(l, r) :: ctx.BoundedInt(k) => handleBoundedBinOp(k, funcsOf(k).mod)(l, r, e) + + // Bitwise binary — no overflow possible (helper postcondition guarantees range) + case e @ in.BitAnd(l, r) :: ctx.BoundedInt(k) => handleBoundedBinOp(k, funcsOf(k).band)(l, r, e) + case e @ in.BitOr(l, r) :: ctx.BoundedInt(k) => handleBoundedBinOp(k, funcsOf(k).bor)(l, r, e) + case e @ in.BitXor(l, r) :: ctx.BoundedInt(k) => handleBoundedBinOp(k, funcsOf(k).bxor)(l, r, e) + case e @ in.BitClear(l, r) :: ctx.BoundedInt(k) => handleBoundedBinOp(k, funcsOf(k).bclear)(l, r, e) + + // Bitwise unary NOT — encode as to(bneg(from(op))). BitNeg.typ equals the operand's type, + // so enclosing operations correctly treat the result as a domain value. + case e @ in.BitNeg(op :: ctx.BoundedInt(k)) => + val (pos, info, errT) = e.vprMeta + for { ve <- goE(op) } yield + toApp(k, vpr.FuncApp(funcsOf(k).bneg, Seq(fromApp(k, ve)))(pos, info, errT), pos, info, errT) + + // Shifts — value operand is projected to Int via from; shift amount is Int. + case e @ in.ShiftLeft(l, r) :: ctx.BoundedInt(k) => handleShift(ctx, k, funcsOf(k).shl)(l, r, e) + case e @ in.ShiftRight(l, r) :: ctx.BoundedInt(k) => handleShift(ctx, k, funcsOf(k).shr)(l, r, e) + + // Comparisons — MemoryEncoding is guarded to skip comparisons with a bounded-int + // operand on either side, so we must handle them here. Both operands are projected + // to Int (`from` is applied to the bounded one(s)); a bounded operand can appear on + // either side (e.g. `0 < c.BufferSize()`). + case e @ in.LessCmp(l, r) if hasBoundedOperand(ctx)(l, r) => + val (pos, info, errT) = e.vprMeta + for { vl <- goE(l); vr <- goE(r) } + yield vpr.LtCmp(asInt(ctx)(l, vl), asInt(ctx)(r, vr))(pos, info, errT): vpr.Exp + + case e @ in.AtMostCmp(l, r) if hasBoundedOperand(ctx)(l, r) => + val (pos, info, errT) = e.vprMeta + for { vl <- goE(l); vr <- goE(r) } + yield vpr.LeCmp(asInt(ctx)(l, vl), asInt(ctx)(r, vr))(pos, info, errT): vpr.Exp + + case e @ in.GreaterCmp(l, r) if hasBoundedOperand(ctx)(l, r) => + val (pos, info, errT) = e.vprMeta + for { vl <- goE(l); vr <- goE(r) } + yield vpr.GtCmp(asInt(ctx)(l, vl), asInt(ctx)(r, vr))(pos, info, errT): vpr.Exp + + case e @ in.AtLeastCmp(l, r) if hasBoundedOperand(ctx)(l, r) => + val (pos, info, errT) = e.vprMeta + for { vl <- goE(l); vr <- goE(r) } + yield vpr.GeCmp(asInt(ctx)(l, vl), asInt(ctx)(r, vr))(pos, info, errT): vpr.Exp + + // Type conversions + + // (1) bounded → different bounded kind: intToBounded_k2(from_k1(x)). The Int→bounded + // function's range precondition is the (narrowing) overflow check. + case conv @ in.Conversion(_, expr :: ctx.BoundedInt(k1)) + if ctx.BoundedInt.unapply(conv.typ).exists(_ != k1) => + val k2 = ctx.BoundedInt.unapply(conv.typ).get + val fn = getIntToBoundedFunc(k2) + val (pos, info, errT) = conv.vprMeta + for { + ve <- goE(expr) + app = vpr.FuncApp(fn, Seq(fromApp(k1, ve)))(pos, info, errT) + _ <- if (checkOverflows) errorT { + case e @ err.PreconditionInAppFalse(Source(info), _, _) if e.causedBy(app) => + OverflowError(info) + } else unit(()) + } yield app + + // (2) bounded → unbounded: extract the Int value via from + case conv @ in.Conversion(_, expr :: ctx.BoundedInt(k)) + if ctx.UnboundedInt.unapply(conv.typ) => + for { ve <- goE(expr) } yield { + val (pos, info, errT) = conv.vprMeta + vpr.DomainFuncApp(funcsOf(k).from.name, Seq(ve), Map.empty)(pos, info, vpr.Int, Names.boundedIntDomain(k), errT) + } + + // (3) unbounded → bounded + case conv @ in.Conversion(_, expr) + if ctx.UnboundedInt.unapply(expr.typ) && ctx.BoundedInt.unapply(conv.typ).isDefined => + val k2 = ctx.BoundedInt.unapply(conv.typ).get + val fn = getIntToBoundedFunc(k2) + val (pos, info, errT) = conv.vprMeta + for { + ve <- goE(expr) + app = vpr.FuncApp(fn, Seq(ve))(pos, info, errT) + _ <- if (checkOverflows) errorT { + case e @ err.PreconditionInAppFalse(Source(info), _, _) if e.causedBy(app) => + OverflowError(info) + } else unit(()) + } yield app + } + } + + // ===== Finalize: emit domains and all generated functions ===== + + override def finalize(addMemberFn: vpr.Member => Unit): Unit = { + // Emit one domain per kind, holding `from`, `to`, and the bridging axioms. + // + // The bijection between the domain and [lower, upper] is axiomatised as: + // 1. range: forall x :: { from(x) } lower <= from(x) <= upper + // 2. injectivity: forall x :: { from(x) } inv(from(x)) == x (Skolem-inverse form) + // 3. right-inverse: forall n :: { to(n) } inRange(n) ==> from(to(n)) == n + // 4. surjectivity: forall n :: { inv(n) } inRange(n) ==> from(inv(n)) == n + // We deliberately do NOT emit the left-inverse direction `to(from(x)) == x` triggered + // on `{from(x)}`: together with (3) it sets up a matching loop in Z3 — each + // instantiation of `to(from(x))` matches the to-trigger and introduces + // `from(to(from(x)))`, which matches the from-trigger and introduces + // `to(from(to(from(x))))`, ad infinitum. The left inverse is nevertheless derivable + // by e-matching from (2) + (3): a ground term `to(from(x))` instantiates (3) to give + // `from(to(from(x))) == from(x)`, and (2) then yields `to(from(x)) == x` — without + // creating further terms. + for ((k, fns) <- kindCache) { + addMemberFn(vpr.Domain( + name = Names.boundedIntDomain(k), + functions = Seq(fns.from, fns.inv, fns.to), + axioms = Seq(fns.rangeAxiom, fns.invAxiom, fns.toFromAxiom, fns.invFromAxiom) + )()) + } + for ((_, fns) <- kindCache) fns.topLevelFns.foreach(addMemberFn) + for ((_, fn) <- intToBoundedCache) addMemberFn(fn) + // Emit a well-founded order domain for each kind so termination measures over + // bounded-int values type-check. Mirrors the shape of `IntWellFoundedOrder` from + // Silver's `import `, but the underlying order is the Int order + // on `from(x)`, and `bounded` is unconditional (every domain value satisfies + // `lower <= from(x)` by the range axiom). + for (k <- kindCache.keys) addMemberFn(buildWellFoundedOrderDomain(k)) + } + + /** Build a `WellFoundedOrder` domain with `decreasing`/`bounded` axioms. */ + private def buildWellFoundedOrderDomain(k: BoundedIntegerKind): vpr.Domain = { + val domTyp = domainType(k) + val wfDomName = Names.boundedIntDomain(k) + "WellFoundedOrder" + + def wfApp(name: String, args: Seq[vpr.Exp]): vpr.DomainFuncApp = + vpr.DomainFuncApp( + funcname = name, + args = args, + typVarMap = Map(vpr.TypeVar("T") -> domTyp) + )(vpr.NoPosition, vpr.NoInfo, typ = vpr.Bool, domainName = "WellFoundedOrder", vpr.NoTrafos) + + val xDecl = vpr.LocalVarDecl("x", domTyp)() + val yDecl = vpr.LocalVarDecl("y", domTyp)() + val x = xDecl.localVar + val y = yDecl.localVar + val fx = fromApp(k, x) + val fy = fromApp(k, y) + + // forall x, y: D :: { decreasing(x, y) } from(x) < from(y) ==> decreasing(x, y) + val decAxiom = vpr.NamedDomainAxiom( + name = s"${k.name}$$dec_ax", + exp = vpr.Forall( + Seq(xDecl, yDecl), + Seq(vpr.Trigger(Seq(wfApp("decreasing", Seq(x, y))))()), + vpr.Implies(vpr.LtCmp(fx, fy)(), wfApp("decreasing", Seq(x, y)))() + )() + )(domainName = wfDomName) + + // forall x: D :: { bounded(x) } bounded(x) + // (Every domain value's `from`-image is >= `lower` by the range axiom, so the + // standard `lower <= from(x) ==> bounded(x)` simplifies to an unconditional + // `bounded(x)`. Termination is sound because the Int order on `[lower, upper]` + // is well-founded.) + val boundedAxiom = vpr.NamedDomainAxiom( + name = s"${k.name}$$bounded_ax", + exp = vpr.Forall( + Seq(xDecl), + Seq(vpr.Trigger(Seq(wfApp("bounded", Seq(x))))()), + wfApp("bounded", Seq(x)) + )() + )(domainName = wfDomName) + + vpr.Domain( + name = wfDomName, + functions = Seq.empty, + axioms = Seq(decAxiom, boundedAxiom) + )() + } + + // ===== Viper AST helpers ===== + + private val zero: vpr.IntLit = vpr.IntLit(0)() + private val decreases: vpr.Exp = termination.DecreasesWildcard(None)() + + private def domainType(k: BoundedIntegerKind): vpr.DomainType = + vpr.DomainType(Names.boundedIntDomain(k), Map.empty)(Seq.empty) + + /** Floor of the square root of a non-negative BigInt (Newton's method). */ + private def sqrtFloor(n: BigInt): BigInt = { + require(n >= 0) + if (n < 2) n else { + var x = BigInt(1) << ((n.bitLength + 1) / 2) + var y = (x + n / x) / 2 + while (y < x) { x = y; y = (x + n / x) / 2 } + x + } + } + + /** lower <= e && e <= upper */ + private def inRange(k: BoundedIntegerKind, e: vpr.Exp): vpr.Exp = + vpr.And( + vpr.LeCmp(vpr.IntLit(k.lower)(), e)(), + vpr.LeCmp(e, vpr.IntLit(k.upper)())() + )() + + /** Apply the `from` domain function of kind k to a domain-typed Viper expression. */ + private def fromApp(k: BoundedIntegerKind, v: vpr.Exp): vpr.Exp = + vpr.DomainFuncApp(funcsOf(k).from, Seq(v), Map.empty)() + + /** Apply the `to` domain function of kind k to an Int-typed Viper expression. */ + private def toApp(k: BoundedIntegerKind, v: vpr.Exp, pos: vpr.Position, info: vpr.Info, errT: vpr.ErrorTrafo): vpr.Exp = + vpr.DomainFuncApp(funcsOf(k).to, Seq(v), Map.empty)(pos, info, errT) + + /** Encodes a shift `value amount` of kind `k` as `to(shiftHelper(from(value), amount))`. */ + private def handleShift(ctx: Context, k: BoundedIntegerKind, helper: vpr.Function)(left: in.Expr, right: in.Expr, src: in.Node): CodeWriter[vpr.Exp] = { + val (pos, info, errT) = src.vprMeta + for { + vl <- ctx.expression(left) + vr <- ctx.expression(right) + app = vpr.FuncApp(helper, Seq(asInt(ctx)(left, vl), asInt(ctx)(right, vr)))(pos, info, errT) + _ <- errorT { + case e2 @ err.PreconditionInAppFalse(Source(info2), _, _) if e2.causedBy(app) => + ShiftPreconditionError(info2) + } + } yield toApp(k, app, pos, info, errT) + } + + /** + * Normalise a Viper expression to the domain type of kind `k`. If the source Gobra + * expression is already a bounded integer, its Viper translation is already a domain value — + * return it unchanged. Otherwise the Viper value is an `Int` (produced by IntEncoding) and is + * converted with `to`. + * + * This is needed because Gobra's internal AST mixes unbounded-integer `IntLit`s with bounded + * targets (e.g. an explicit `return 0` where the result is bounded) without inserting explicit + * conversions. + */ + private def asDomain(ctx: Context)(k: BoundedIntegerKind, expr: in.Expr, v: vpr.Exp): vpr.Exp = + ctx.BoundedInt.unapply(expr.typ) match { + case Some(_) => v + case None => vpr.DomainFuncApp(funcsOf(k).to, Seq(v), Map.empty)() + } + + /** + * If `expr` has a bounded integer type, wrap `v` with `from`; otherwise return `v` unchanged. + * Used to normalise operands of mixed (bounded/unbounded) comparisons, equalities, and + * arithmetic helper applications. + * + * Bounded literals deliberately stay as `from(to(c))` rather than being folded to `c`: + * the ground `to(c)` terms instantiate the bridge axiom and act as congruence anchors + * linking the domain values to their integer images. Folding them away was measured to + * send otherwise-fast nonlinear queries (division/multiplication chains) from seconds + * into multi-minute Z3 timeouts (blank-identifier1: 12s -> 600s+ on identical Silicon). + */ + private def asInt(ctx: Context)(expr: in.Expr, v: vpr.Exp): vpr.Exp = + ctx.BoundedInt.unapply(expr.typ) match { + case Some(k) => fromApp(k, v) + case None => v + } + + // ===== Build per-kind functions ===== + + private def buildKindFunctions(k: BoundedIntegerKind): KindFunctions = { + val domTyp = domainType(k) + val domName = Names.boundedIntDomain(k) + + // Build `from` first — all other function contracts reference it. + // IMPORTANT: use a local `fromE` helper rather than funcsOf(k).from to avoid recursion. + val fromFn = { + val xDecl = vpr.LocalVarDecl("x", domTyp)() + vpr.DomainFunc( + name = Names.boundedIntFrom(k), + formalArgs = Seq(xDecl), + typ = vpr.Int + )(domainName = domName) + } + + def fromE(e: vpr.Exp): vpr.Exp = vpr.DomainFuncApp(fromFn, Seq(e), Map.empty)() + + // Axiom: forall x: domType :: lower <= from(x) && from(x) <= upper. + // This replaces the old `from` postcondition: every domain value's image under `from` + // lies inside the kind's range. + val rangeAxiom = { + val xDecl = vpr.LocalVarDecl("x", domTyp)() + val fx = fromE(xDecl.localVar) + val body = inRange(k, fx) + vpr.NamedDomainAxiom( + name = s"${k.name}$$from_in_range", + exp = vpr.Forall(Seq(xDecl), Seq(vpr.Trigger(Seq(fx))()), body)() + )(domainName = domName) + } + + // Injectivity of `from` via a Skolem inverse: forall x: D :: { from(x) } inv(from(x)) == x. + // + // `from` must be injective: equality of domain values has to coincide with equality of + // their integer images wherever Viper compares domain values natively — map domains + // (`k in domain(m)`), set membership, sequence/tuple/ADT equality, etc. The encoder's + // `equal` override only covers *direct* equalities on bounded-int expressions; without + // injectivity, `n !in domain(m) && 0 in domain(m)` fails to entail `from(n) != 0` and + // e.g. map-based caches become unverifiable (same_package/pkg_init/fib regression). + // + // The Skolem-inverse form is preferred over the classic pair-trigger axiom + // `forall x, y :: { from(x), from(y) } from(x) == from(y) ==> x == y`: it needs only ONE + // instantiation per ground `from`-term (the pair trigger needs one per PAIR of them), + // yet gives the same power via congruence: from(a) == from(b) implies + // inv(from(a)) == inv(from(b)), i.e. a == b. `inv` also serves as the range-restricted + // inverse of `from` in lowered quantifiers (see AssertionEncoding and `invFromAxiom`): + // the `inv(from(x))` terms this axiom creates are exactly the anchors that let lowered + // triggers like `{ m[inv(v)] }` e-match ground `m[i]` terms via congruence. + val invFn = vpr.DomainFunc( + name = Names.boundedIntInv(k), + formalArgs = Seq(vpr.LocalVarDecl("n", vpr.Int)()), + typ = domTyp + )(domainName = domName) + val invAxiom = { + val xDecl = vpr.LocalVarDecl("x", domTyp)() + val fx = fromE(xDecl.localVar) + val invFx = vpr.DomainFuncApp(invFn, Seq(fx), Map.empty)() + vpr.NamedDomainAxiom( + name = s"${k.name}$$from_injective", + exp = vpr.Forall(Seq(xDecl), Seq(vpr.Trigger(Seq(fx))()), vpr.EqCmp(invFx, xDecl.localVar)())() + )(domainName = domName) + } + + // `to` is a total domain function (no precondition). Its defining property is + // expressed by the domain axiom `toFromAxiom` below (under the in-range condition). + // + // We deliberately do NOT also emit a `to(from(x)) == x` axiom — the pair of + // directions would form a matching loop in Z3 (see comment in `finalize`). + // That direction is instead recovered by `invAxiom` + `toFromAxiom`. + val toFn = vpr.DomainFunc( + name = Names.boundedIntTo(k), + formalArgs = Seq(vpr.LocalVarDecl("x", vpr.Int)()), + typ = domTyp + )(domainName = domName) + + def toE(e: vpr.Exp): vpr.Exp = vpr.DomainFuncApp(toFn, Seq(e), Map.empty)() + + // Axiom: forall n: Int :: { to(n) } inRange(n) ==> from(to(n)) == n. + // Replaces `to`'s old function postcondition. The trigger fires only at concrete + // call sites (e.g. `to(0)` for default values, `to(literal)` for literals) and + // does not introduce new domain values — bounded the way the encoder uses `to`. + val toFromAxiom = { + val nDecl = vpr.LocalVarDecl("n", vpr.Int)() + val n = nDecl.localVar + val tn = toE(n) + val body = vpr.Implies(inRange(k, n), vpr.EqCmp(fromE(tn), n)())() + vpr.NamedDomainAxiom( + name = s"${k.name}$$from_to_inverse", + exp = vpr.Forall(Seq(nDecl), Seq(vpr.Trigger(Seq(tn))()), body)() + )(domainName = domName) + } + + // Axiom: forall n: Int :: { inv(n) } inRange(n) ==> from(inv(n)) == n. + // Makes `inv` a two-sided inverse on the kind's range (surjectivity of `from` onto it). + // Quantifiers over bounded variables are lowered to Int variables whose domain-sorted + // occurrences are rewritten to `inv(v)` (see AssertionEncoding): proving facts *about* + // `inv(v)` for an arbitrary in-range `v` (e.g. a call precondition `b.Start <= inv(v)` + // inside a quantified postcondition) needs the projection of `inv(v)` to be `v` itself. + // Triggering on `inv(n)` keeps the axiom inert except where such terms already exist; + // it creates only `from(inv(n))` terms, whose own axioms add nothing beyond congruent + // terms — no matching loop. + val invFromAxiom = { + val nDecl = vpr.LocalVarDecl("n", vpr.Int)() + val n = nDecl.localVar + val invN = vpr.DomainFuncApp(invFn, Seq(n), Map.empty)() + val body = vpr.Implies(inRange(k, n), vpr.EqCmp(fromE(invN), n)())() + vpr.NamedDomainAxiom( + name = s"${k.name}$$inv_from_inverse", + exp = vpr.Forall(Seq(nDecl), Seq(vpr.Trigger(Seq(invN))()), body)() + )(domainName = domName) + } + + // ----- Int-valued helper functions (arguments are Int, NOT the domain type) ----- + // Keeping the arguments over Int is what prevents the e-matching matching loop: no synthesised + // axiom mentions `from` of a quantified variable. The result range is asserted unconditionally; + // the value equality is conditional on no overflow when --overflow is off. + + // The result value is known exactly only when the mathematical result lies within the + // kind's range (`rangeOk ==> result == computed`); a possibly-overflowing operation is + // deliberately opaque (only `inRange(result)` is known). Callers must either prove the + // absence of overflow via their specs or run with --overflow, which turns `rangeOk` + // into a precondition whose violation is reported as an integer overflow error. + def binaryArithFunc(name: String, compute: (vpr.Exp, vpr.Exp) => vpr.Exp, + extraPres: Seq[vpr.Exp] = Seq.empty, + extraPosts: Seq[vpr.Exp] = Seq.empty): vpr.Function = { + val xDecl = vpr.LocalVarDecl("x", vpr.Int)() + val yDecl = vpr.LocalVarDecl("y", vpr.Int)() + val computed = compute(xDecl.localVar, yDecl.localVar) + val result = vpr.Result(vpr.Int)() + val rangeOk = inRange(k, computed) + + val pres = if (checkOverflows) extraPres ++ Seq(rangeOk, decreases) + else extraPres :+ decreases + + val posts = (if (checkOverflows) Seq(inRange(k, result), vpr.EqCmp(result, computed)()) + else Seq(inRange(k, result), vpr.Implies(rangeOk, vpr.EqCmp(result, computed)())())) ++ extraPosts + + vpr.Function(name, Seq(xDecl, yDecl), vpr.Int, pres, posts, None)() + } + + val addFn = binaryArithFunc(Names.boundedIntAdd(k), (x, y) => vpr.Add(x, y)()) + val subFn = binaryArithFunc(Names.boundedIntSub(k), (x, y) => vpr.Sub(x, y)()) + + // "sqrt box" lemma for multiplication: whenever |x| and |y| are at most + // floor(sqrt(upper)), the product provably fits the range, so the exact-value + // equation holds unconditionally. This is a sound consequence of the conditional + // post (|x|,|y| <= B ==> |x*y| <= B^2 <= upper <= |lower|), but making it a + // separate post lets Z3 obtain exactness for small operands LINEARLY — without + // the nonlinear `lower <= x*y <= upper` derivation against the huge range + // constants, on which Z3's nlsat is fragile (the stats_collector Area methods + // diverged for minutes on exactly that step). + val mulFn = { + val sqrtBox: BigInt = sqrtFloor(k.upper) + def inBox(e: vpr.Exp): vpr.Exp = vpr.And( + vpr.LeCmp(vpr.IntLit(-sqrtBox)(), e)(), + vpr.LeCmp(e, vpr.IntLit(sqrtBox)())() + )() + val xRef = vpr.LocalVar("x", vpr.Int)() + val yRef = vpr.LocalVar("y", vpr.Int)() + val res = vpr.Result(vpr.Int)() + val bothInBox = vpr.And(inBox(xRef), inBox(yRef))() + val boxPost = vpr.Implies(bothInBox, vpr.EqCmp(res, vpr.Mul(xRef, yRef)())())() + // Sign lemmas (sound consequences of exactness inside the box): they let Z3 derive + // the sign of a product linearly — `w > 0 && h > 0 ==> w*h > 0` is itself a + // nonlinear step that Z3 otherwise fails on or grinds over. + val zero = vpr.IntLit(0)() + val nonNegPost = vpr.Implies( + vpr.And(bothInBox, vpr.And(vpr.GeCmp(xRef, zero)(), vpr.GeCmp(yRef, zero)())())(), + vpr.GeCmp(res, zero)() + )() + val posPost = vpr.Implies( + vpr.And(bothInBox, vpr.And(vpr.GtCmp(xRef, zero)(), vpr.GtCmp(yRef, zero)())())(), + vpr.GtCmp(res, zero)() + )() + binaryArithFunc(Names.boundedIntMul(k), (x, y) => vpr.Mul(x, y)(), extraPosts = Seq(boxPost, nonNegPost, posPost)) + } + + // div: Go truncation-towards-zero semantics; divisor must be non-zero. + val divFn = { + val xDecl = vpr.LocalVarDecl("x", vpr.Int)() + val yDecl = vpr.LocalVarDecl("y", vpr.Int)() + val x = xDecl.localVar + val y = yDecl.localVar + val result = vpr.Result(vpr.Int)() + val yNonZero = vpr.NeCmp(y, zero)() + val truncDiv = vpr.CondExp( + vpr.LeCmp(zero, x)(), + vpr.Div(x, y)(), + vpr.Minus(vpr.Div(vpr.Minus(x)(), y)())() + )() + val rangeOk = inRange(k, truncDiv) + val pres = if (checkOverflows) Seq(yNonZero, rangeOk, decreases) + else Seq(yNonZero, decreases) + val posts = if (checkOverflows) Seq(inRange(k, result), vpr.EqCmp(result, truncDiv)()) + else Seq(inRange(k, result), vpr.Implies(rangeOk, vpr.EqCmp(result, truncDiv)())()) + vpr.Function(Names.boundedIntDiv(k), Seq(xDecl, yDecl), vpr.Int, pres, posts, None)() + } + + // mod: Go truncation-towards-zero semantics; divisor must be non-zero. + val modFn = { + val xDecl = vpr.LocalVarDecl("x", vpr.Int)() + val yDecl = vpr.LocalVarDecl("y", vpr.Int)() + val x = xDecl.localVar + val y = yDecl.localVar + val result = vpr.Result(vpr.Int)() + val yNonZero = vpr.NeCmp(y, zero)() + val absY = vpr.CondExp(vpr.LeCmp(zero, y)(), y, vpr.Minus(y)())() + val truncMod = vpr.CondExp( + vpr.Or(vpr.LeCmp(zero, x)(), vpr.EqCmp(vpr.Mod(x, y)(), zero)())(), + vpr.Mod(x, y)(), + vpr.Sub(vpr.Mod(x, y)(), absY)() + )() + val rangeOk = inRange(k, truncMod) + val pres = if (checkOverflows) Seq(yNonZero, rangeOk, decreases) + else Seq(yNonZero, decreases) + val posts = if (checkOverflows) Seq(inRange(k, result), vpr.EqCmp(result, truncMod)()) + else Seq(inRange(k, result), vpr.Implies(rangeOk, vpr.EqCmp(result, truncMod)())()) + vpr.Function(Names.boundedIntMod(k), Seq(xDecl, yDecl), vpr.Int, pres, posts, None)() + } + + // Bitwise binary: abstract; result is in range. Bitwise operations never overflow. + def bitwiseBinaryFunc(name: String): vpr.Function = { + val xDecl = vpr.LocalVarDecl("x", vpr.Int)() + val yDecl = vpr.LocalVarDecl("y", vpr.Int)() + val result = vpr.Result(vpr.Int)() + vpr.Function(name, Seq(xDecl, yDecl), vpr.Int, + pres = Seq(decreases), + posts = Seq(inRange(k, result)), + body = None)() + } + + val bandFn = bitwiseBinaryFunc(Names.boundedIntBand(k)) + val borFn = bitwiseBinaryFunc(Names.boundedIntBor(k)) + val bxorFn = bitwiseBinaryFunc(Names.boundedIntBxor(k)) + val bclearFn = bitwiseBinaryFunc(Names.boundedIntBclear(k)) + + // bneg: unary NOT; takes Int (caller applies from to the operand), returns Int + val bnegFn = { + val xDecl = vpr.LocalVarDecl("x", vpr.Int)() + val result = vpr.Result(vpr.Int)() + vpr.Function(Names.boundedIntBneg(k), Seq(xDecl), vpr.Int, + pres = Seq(decreases), + posts = Seq(inRange(k, result)), + body = None)() + } + + // shifts: (x: Int, shift: Int): Int; shift amount must be non-negative. + def shiftFn(name: String): vpr.Function = { + val xDecl = vpr.LocalVarDecl("x", vpr.Int)(info = vpr.Synthesized) + val shiftDecl = vpr.LocalVarDecl("shift", vpr.Int)(info = vpr.Synthesized) + val result = vpr.Result(vpr.Int)() + vpr.Function(name, Seq(xDecl, shiftDecl), vpr.Int, + pres = Seq(vpr.GeCmp(shiftDecl.localVar, zero)(), decreases), + posts = Seq(inRange(k, result)), + body = None)() + } + + val shlFn = shiftFn(Names.boundedIntShl(k)) + val shrFn = shiftFn(Names.boundedIntShr(k)) + + KindFunctions(fromFn, invFn, toFn, rangeAxiom, invAxiom, toFromAxiom, invFromAxiom, addFn, subFn, mulFn, divFn, modFn, + bandFn, borFn, bxorFn, bclearFn, bnegFn, shlFn, shrFn) + } + + // ===== Conversion functions ===== + + private def getIntToBoundedFunc(to: BoundedIntegerKind): vpr.Function = + intToBoundedCache.getOrElseUpdate(to, buildIntToBoundedFunc(to)) + + /** unbounded (Int) → bounded: result is toKind domain. */ + private def buildIntToBoundedFunc(toKind: BoundedIntegerKind): vpr.Function = { + val toDomTyp = domainType(toKind) + val xDecl = vpr.LocalVarDecl("x", vpr.Int)() + val x = xDecl.localVar + val result = vpr.Result(toDomTyp)() + val fResult = fromApp(toKind, result) + + val pres = if (checkOverflows) Seq(inRange(toKind, x), decreases) else Seq(decreases) + val posts = if (checkOverflows) Seq(vpr.EqCmp(fResult, x)()) + else Seq(vpr.Implies(inRange(toKind, x), vpr.EqCmp(fResult, x)())()) + vpr.Function(Names.integerToBounded(toKind), Seq(xDecl), toDomTyp, pres, posts, None)() + } +} diff --git a/src/main/scala/viper/gobra/translator/encodings/FloatEncoding.scala b/src/main/scala/viper/gobra/translator/encodings/FloatEncoding.scala index 76ee45196..3cc79ae2f 100644 --- a/src/main/scala/viper/gobra/translator/encodings/FloatEncoding.scala +++ b/src/main/scala/viper/gobra/translator/encodings/FloatEncoding.scala @@ -13,6 +13,8 @@ import viper.gobra.translator.encodings.combinators.LeafTypeEncoding import viper.gobra.translator.context.Context import viper.gobra.translator.util.ViperWriter.CodeLevel.unit import viper.gobra.translator.util.ViperWriter.CodeWriter +import viper.gobra.ast.internal.utility.IntKindAlignment +import viper.gobra.translator.Names import viper.silver.{ast => vpr} class FloatEncoding extends LeafTypeEncoding { @@ -49,6 +51,20 @@ class FloatEncoding extends LeafTypeEncoding { * [ (x: floatX) / (y: floatX) ] -> divFloatX([ x ], [ y ]) * [ floatX(x: int) ] -> fromIntToX([ x ]) */ + /** Projects a bounded-integer operand to its mathematical image (a no-op otherwise). */ + private def asMathInt(ctx: Context)(e: in.Expr): in.Expr = + IntKindAlignment.asUnboundedInt(e, underlyingType(e.typ)(ctx)) + + /** Lifts an Int-typed Viper result into the domain of `typ`, if `typ` is a bounded kind. */ + private def asDomainInt(ctx: Context)(typ: in.Type, e: vpr.Exp): vpr.Exp = + ctx.BoundedInt.unapply(typ) match { + case Some(k) => + vpr.DomainFuncApp( + Names.boundedIntTo(k), Seq(e), Map.empty + )(e.pos, e.info, vpr.DomainType(Names.boundedIntDomain(k), Map.empty)(Seq.empty), Names.boundedIntDomain(k), e.errT) + case None => e + } + override def expression(ctx: Context): in.Expr ==> CodeWriter[vpr.Exp] = { def goE(x: in.Expr): CodeWriter[vpr.Exp] = ctx.expression(x) @@ -74,14 +90,17 @@ class FloatEncoding extends LeafTypeEncoding { for { lE <- goE(l); rE <- goE(r) } yield withSrc(vpr.FuncApp(divFloat32, Seq(lE, rE)), div) case div @ in.Div(l :: ctx.Float64(), r :: ctx.Float64()) => for { lE <- goE(l); rE <- goE(r) } yield withSrc(vpr.FuncApp(divFloat64, Seq(lE, rE)), div) + // The int<->float conversion functions operate on Viper Ints, so a bounded-kind operand + // must be projected to its integer image on the way in, and a bounded-kind result lifted + // back into its domain on the way out. case conv@in.Conversion(in.Float32T(_), expr :: ctx.Int()) => - for { e <- goE(expr) } yield withSrc(vpr.FuncApp(fromIntTo32, Seq(e)), conv) + for { e <- goE(asMathInt(ctx)(expr)) } yield withSrc(vpr.FuncApp(fromIntTo32, Seq(e)), conv) case conv@in.Conversion(in.Float64T(_), expr :: ctx.Int()) => - for { e <- goE(expr) } yield withSrc(vpr.FuncApp(fromIntTo64, Seq(e)), conv) + for { e <- goE(asMathInt(ctx)(expr)) } yield withSrc(vpr.FuncApp(fromIntTo64, Seq(e)), conv) case conv@in.Conversion(in.IntT(_, _), expr :: ctx.Float32()) => - for { e <- goE(expr) } yield withSrc(vpr.FuncApp(from32ToInt, Seq(e)), conv) + for { e <- goE(expr) } yield asDomainInt(ctx)(conv.typ, withSrc(vpr.FuncApp(from32ToInt, Seq(e)), conv)) case conv@in.Conversion(in.IntT(_, _), expr :: ctx.Float64()) => - for { e <- goE(expr) } yield withSrc(vpr.FuncApp(from64ToInt, Seq(e)), conv) + for { e <- goE(expr) } yield asDomainInt(ctx)(conv.typ, withSrc(vpr.FuncApp(from64ToInt, Seq(e)), conv)) } } diff --git a/src/main/scala/viper/gobra/translator/encodings/IntEncoding.scala b/src/main/scala/viper/gobra/translator/encodings/IntEncoding.scala index 2f7748897..4855b9c71 100644 --- a/src/main/scala/viper/gobra/translator/encodings/IntEncoding.scala +++ b/src/main/scala/viper/gobra/translator/encodings/IntEncoding.scala @@ -36,9 +36,10 @@ class IntEncoding extends LeafTypeEncoding { /** * Translates a type into a Viper type. + * Only handles unbounded integers; bounded integers are handled by BoundedIntEncoding. */ override def typ(ctx: Context): in.Type ==> vpr.Type = { - case ctx.Int() / m => + case ctx.UnboundedInt() / m => m match { case Exclusive => vpr.Int case Shared => vpr.Ref @@ -68,29 +69,30 @@ class IntEncoding extends LeafTypeEncoding { } default(super.expression(ctx)){ - case (e: in.DfltVal) :: ctx.Int() / Exclusive => unit(withSrc(vpr.IntLit(0), e)) - case lit: in.IntLit => unit(withSrc(vpr.IntLit(lit.v), lit)) + case (e: in.DfltVal) :: ctx.UnboundedInt() / Exclusive => unit(withSrc(vpr.IntLit(0), e)) + case lit: in.IntLit if ctx.UnboundedInt.unapply(lit.typ) => unit(withSrc(vpr.IntLit(lit.v), lit)) - case e@ in.Add(l, r) :: ctx.Int() => for {vl <- goE(l); vr <- goE(r)} yield withSrc(vpr.Add(vl, vr), e) - case e@ in.Sub(l, r) :: ctx.Int() => for {vl <- goE(l); vr <- goE(r)} yield withSrc(vpr.Sub(vl, vr), e) - case e@ in.Mul(l, r) :: ctx.Int() => for {vl <- goE(l); vr <- goE(r)} yield withSrc(vpr.Mul(vl, vr), e) - case e@ in.Mod(l, r) :: ctx.Int() => + case e@ in.Add(l, r) :: ctx.UnboundedInt() => for {vl <- goE(l); vr <- goE(r)} yield withSrc(vpr.Add(vl, vr), e) + case e@ in.Sub(l, r) :: ctx.UnboundedInt() => for {vl <- goE(l); vr <- goE(r)} yield withSrc(vpr.Sub(vl, vr), e) + case e@ in.Mul(l, r) :: ctx.UnboundedInt() => for {vl <- goE(l); vr <- goE(r)} yield withSrc(vpr.Mul(vl, vr), e) + case e@ in.Mod(l, r) :: ctx.UnboundedInt() => // We currently implement our own modulo algorithm to mimic what Go does. The default modulo implementation in // Viper does not match Go's semantics. Check https://github.com/viperproject/gobra/issues/858 and // https://github.com/viperproject/silver/issues/297 for {vl <- goE(l); vr <- goE(r)} yield withSrc(vpr.FuncApp(goIntMod, Seq(vl, vr)), e) - case e@ in.Div(l, r) :: ctx.Int() => + case e@ in.Div(l, r) :: ctx.UnboundedInt() => // We currently implement our own division algorithm to mimic what Go does. The default division implementation in // Viper does not match Go's semantics. Check https://github.com/viperproject/gobra/issues/858 and // https://github.com/viperproject/silver/issues/297 for {vl <- goE(l); vr <- goE(r)} yield withSrc(vpr.FuncApp(goIntDiv, Seq(vl, vr)), e) - case e@ in.BitAnd(l, r) :: ctx.Int() => for {vl <- goE(l); vr <- goE(r)} yield withSrc(vpr.FuncApp(bitwiseAnd, Seq(vl, vr)), e) - case e@ in.BitOr(l, r) :: ctx.Int() => for {vl <- goE(l); vr <- goE(r)} yield withSrc(vpr.FuncApp(bitwiseOr, Seq(vl, vr)), e) - case e@ in.BitXor(l, r) :: ctx.Int() => for {vl <- goE(l); vr <- goE(r)} yield withSrc(vpr.FuncApp(bitwiseXor, Seq(vl, vr)), e) - case e@ in.BitClear(l, r) :: ctx.Int() => for {vl <- goE(l); vr <- goE(r)} yield withSrc(vpr.FuncApp(bitClear, Seq(vl, vr)), e) - case e@ in.ShiftLeft(l, r) :: ctx.Int() => withSrc(handleShift(shiftLeft)(l, r), e) - case e@ in.ShiftRight(l, r) :: ctx.Int() => withSrc(handleShift(shiftRight)(l, r), e) - case e@ in.BitNeg(exp) :: ctx.Int() => for {ve <- goE(exp)} yield withSrc(vpr.FuncApp(bitwiseNegation, Seq(ve)), e) + case e@ in.BitAnd(l, r) :: ctx.UnboundedInt() => for {vl <- goE(l); vr <- goE(r)} yield withSrc(vpr.FuncApp(bitwiseAnd, Seq(vl, vr)), e) + case e@ in.BitOr(l, r) :: ctx.UnboundedInt() => for {vl <- goE(l); vr <- goE(r)} yield withSrc(vpr.FuncApp(bitwiseOr, Seq(vl, vr)), e) + case e@ in.BitXor(l, r) :: ctx.UnboundedInt() => for {vl <- goE(l); vr <- goE(r)} yield withSrc(vpr.FuncApp(bitwiseXor, Seq(vl, vr)), e) + case e@ in.BitClear(l, r) :: ctx.UnboundedInt() => for {vl <- goE(l); vr <- goE(r)} yield withSrc(vpr.FuncApp(bitClear, Seq(vl, vr)), e) + case e@ in.ShiftLeft(l, r) :: ctx.UnboundedInt() => withSrc(handleShift(shiftLeft)(l, r), e) + case e@ in.ShiftRight(l, r) :: ctx.UnboundedInt() => withSrc(handleShift(shiftRight)(l, r), e) + // BitNeg always has UnboundedInteger result type; for bounded operands, BoundedIntEncoding handles it + case e@ in.BitNeg(exp :: ctx.UnboundedInt()) => for {ve <- goE(exp)} yield withSrc(vpr.FuncApp(bitwiseNegation, Seq(ve)), e) } } diff --git a/src/main/scala/viper/gobra/translator/encodings/StringEncoding.scala b/src/main/scala/viper/gobra/translator/encodings/StringEncoding.scala index dc1e35bbf..c0ea57b3a 100644 --- a/src/main/scala/viper/gobra/translator/encodings/StringEncoding.scala +++ b/src/main/scala/viper/gobra/translator/encodings/StringEncoding.scala @@ -25,7 +25,13 @@ import viper.silver.plugin.standard.termination import scala.annotation.unused -class StringEncoding extends LeafTypeEncoding { +/** + * @param intUpperBound under bounded integer semantics, the configured `int` kind's maximum; + * string lengths are then axiomatized to be at most this bound (Go + * guarantees `len` of a string fits in `int`). `None` under + * `--unboundedIntegers`. + */ +class StringEncoding(intUpperBound: Option[BigInt] = None) extends LeafTypeEncoding { import viper.gobra.translator.util.TypePatterns._ @@ -85,7 +91,8 @@ class StringEncoding extends LeafTypeEncoding { val (pos, info, errT) = e.vprMeta for { baseExp <- goE(base) - indexExp <- goE(index) + // string indices are Viper Ints; project bounded-int indices via `from` + indexExp <- goE(viper.gobra.ast.internal.utility.IntKindAlignment.asUnboundedInt(index, underlyingType(index.typ)(ctx))) } yield stringIndex(baseExp, indexExp)(ctx)(pos, info, errT) } } @@ -270,18 +277,23 @@ class StringEncoding extends LeafTypeEncoding { } /** - * Every string has a non-negative length: + * Every string has a non-negative length that fits in `int` (the bound is omitted under + * `--unboundedIntegers`): * axiom { - * forall x string :: { strLen(str) } 0 <= strLen(x) + * forall x string :: { strLen(str) } 0 <= strLen(x) && strLen(x) <= MaxInt * } */ val lenAxiom = vpr.AnonymousDomainAxiom { val qtfVar = vpr.LocalVarDecl("str", stringType)() val lenApp = vpr.DomainFuncApp(lenFunc, Seq(qtfVar.localVar), Map.empty)() + val nonNeg: vpr.Exp = vpr.LeCmp(vpr.IntLit(0)(), lenApp)() + val bounded = intUpperBound.foldLeft(nonNeg) { (acc, bound) => + vpr.And(acc, vpr.LeCmp(lenApp, vpr.IntLit(bound)())())() + } vpr.Forall( variables = Seq(qtfVar), triggers = Seq(vpr.Trigger(Seq(lenApp))()), - exp = vpr.LeCmp(vpr.IntLit(0)(), lenApp)() + exp = bounded )() }(domainName = domainName) @@ -393,7 +405,10 @@ class StringEncoding extends LeafTypeEncoding { val info = Source.Parser.Internal val param1T = in.StringT(Addressability.Exclusive) val param1 = in.Parameter.In("s", param1T)(info) - val param2T = in.IntT(Addressability.Exclusive, TypeBounds.DefaultInt) + // Index is mathematical (the encoding's bound variables for string positions are + // unbounded `integer`). Declaring the formal as bounded `int` would produce a Viper + // sort mismatch when the call site passes a vpr.Int. + val param2T = in.IntT(Addressability.Exclusive, TypeBounds.UnboundedInteger) val param2 = in.Parameter.In("i", param2T)(info) val resT = in.IntT(Addressability.Exclusive, TypeBounds.Byte) val res = in.Parameter.Out("res", resT)(info) diff --git a/src/main/scala/viper/gobra/translator/encodings/adts/AdtEncoding.scala b/src/main/scala/viper/gobra/translator/encodings/adts/AdtEncoding.scala index 03ab482c8..095a6ac35 100644 --- a/src/main/scala/viper/gobra/translator/encodings/adts/AdtEncoding.scala +++ b/src/main/scala/viper/gobra/translator/encodings/adts/AdtEncoding.scala @@ -524,10 +524,10 @@ class AdtEncoding extends LeafTypeEncoding { unit(vpr.TrueLit()(pos,info,errT)) case in.MatchValue(exp) => - for { - e1 <- ctx.expression(exp) - e2 <- ctx.expression(expr) - } yield vpr.EqCmp(e1, e2)(pos, info, errT) + // Use the equality dispatch instead of a raw vpr.EqCmp: the pattern literal and the + // scrutinee may have different integer kinds (e.g. untyped literal `5` vs. a bounded + // `int` ADT field), in which case BoundedIntEncoding must project both to Int. + ctx.equal(exp, expr)(pattern) case in.MatchAdt(clause, patternArgs) => val destructorOverExp = clause.fields.map(f => in.AdtDestructor(expr, f)(expr.info)) diff --git a/src/main/scala/viper/gobra/translator/encodings/arrays/ArrayEncoding.scala b/src/main/scala/viper/gobra/translator/encodings/arrays/ArrayEncoding.scala index 2ca272282..ae5db274b 100644 --- a/src/main/scala/viper/gobra/translator/encodings/arrays/ArrayEncoding.scala +++ b/src/main/scala/viper/gobra/translator/encodings/arrays/ArrayEncoding.scala @@ -7,6 +7,7 @@ package viper.gobra.translator.encodings.arrays import org.bitbucket.inkytonik.kiama.==> +import viper.gobra.ast.internal.utility.IntKindAlignment import viper.gobra.ast.{internal => in} import viper.gobra.reporting.{LoadError, InsufficientPermissionError, Source} import viper.gobra.theory.Addressability @@ -151,13 +152,14 @@ class ArrayEncoding extends TypeEncoding with SharedArrayEmbedding { case (loc@ in.IndexedExp(base :: ctx.Array(len, t), idx, _)) :: _ / Exclusive => for { vBase <- ctx.expression(base) - vIdx <- ctx.expression(idx) + // array indices are Viper Ints; project bounded-int indices via `from` + vIdx <- ctx.expression(IntKindAlignment.asUnboundedInt(idx, underlyingType(idx.typ)(ctx))) } yield ex.get(vBase, vIdx, cptParam(len, t)(ctx))(loc)(ctx) case (upd: in.ArrayUpdate) :: ctx.Array(len, t) => for { vBase <- ctx.expression(upd.base) - vIdx <- ctx.expression(upd.left) + vIdx <- ctx.expression(IntKindAlignment.asUnboundedInt(upd.left, underlyingType(upd.left.typ)(ctx))) vVal <- ctx.expression(upd.right) } yield ex.update(vBase, vIdx, vVal, cptParam(len, t)(ctx))(upd)(ctx) @@ -245,13 +247,13 @@ class ArrayEncoding extends TypeEncoding with SharedArrayEmbedding { case (loc@ in.IndexedExp(base :: ctx.Array(len, t), idx, _)) :: _ / Shared => for { vBase <- ctx.reference(base.asInstanceOf[in.Location]) - vIdx <- ctx.expression(idx) + vIdx <- ctx.expression(IntKindAlignment.asUnboundedInt(idx, underlyingType(idx.typ)(ctx))) } yield sh.get(vBase, vIdx, cptParam(len, t)(ctx))(loc)(ctx) case loc@in.IndexedExp(base :: ctx.*(in.ArrayT(len, t, _)), idx, ptrT) => val derefBase = in.Deref(base, ptrT)(base.info) for { vBase <- ctx.reference(derefBase.asInstanceOf[in.Location]) - vIdx <- ctx.expression(idx) + vIdx <- ctx.expression(IntKindAlignment.asUnboundedInt(idx, underlyingType(idx.typ)(ctx))) } yield sh.get(vBase, vIdx, cptParam(len, t)(ctx))(loc)(ctx) } diff --git a/src/main/scala/viper/gobra/translator/encodings/channels/ChannelEncoding.scala b/src/main/scala/viper/gobra/translator/encodings/channels/ChannelEncoding.scala index d13147fa5..28c1ecf22 100644 --- a/src/main/scala/viper/gobra/translator/encodings/channels/ChannelEncoding.scala +++ b/src/main/scala/viper/gobra/translator/encodings/channels/ChannelEncoding.scala @@ -16,7 +16,12 @@ import viper.gobra.translator.context.Context import viper.gobra.translator.util.ViperWriter.CodeWriter import viper.silver.{ast => vpr} -class ChannelEncoding extends LeafTypeEncoding { +/** + * @param goIntKind the IntegerKind the frontend uses for Go's `int` type. The synthesized + * `BufferSize` call must declare this kind so its internal type matches the + * generated built-in member's (bounded) Viper return sort. + */ +class ChannelEncoding(goIntKind: viper.gobra.util.TypeBounds.IntegerKind) extends LeafTypeEncoding { import viper.gobra.translator.util.TypePatterns._ import viper.gobra.translator.util.ViperWriter.CodeLevel._ @@ -67,7 +72,7 @@ class ChannelEncoding extends LeafTypeEncoding { ) // exhale [c].RecvGivenPerm()() - recvGivenPermInst = getChannelInvariantAccess(channel, recvGivenPerm, Vector.empty, Vector.empty)(exp.info) + recvGivenPermInst = getChannelInvariantAccess(channel, recvGivenPerm, Vector.empty, Vector.empty)(exp.info)(ctx) vprRecvGivenPermInst <- ctx.assertion(recvGivenPermInst) _ <- exhale(vprRecvGivenPermInst, (info, _) => ChannelReceiveError(info) dueTo InsufficientPermissionFromTagError(s"${channel.info.tag}.RecvGivenPerm()()") @@ -84,7 +89,7 @@ class ChannelEncoding extends LeafTypeEncoding { // inhale res != Dflt[T] ==> [c].RecvGotPerm()(res) isNotZero = in.UneqCmp(res, in.DfltVal(res.typ)(exp.info))(exp.info) - recvGotPermInst = getChannelInvariantAccess(channel, recvGotPerm, Vector(res), Vector(typeParam))(exp.info) + recvGotPermInst = getChannelInvariantAccess(channel, recvGotPerm, Vector(res), Vector(typeParam))(exp.info)(ctx) notZeroImpl = in.Implication(isNotZero, recvGotPermInst)(exp.info) vprNotZeroImpl <- ctx.assertion(notZeroImpl) vprInhaleNotZeroImpl = vpr.Inhale(vprNotZeroImpl)(pos, info, errT) @@ -139,7 +144,9 @@ class ChannelEncoding extends LeafTypeEncoding { // var a [ chan T ] _ <- local(vprA) - vprBufferSize <- ctx.expression(bufferSizeArg) + // project a (possibly bounded-int) buffer size to a mathematical integer for the + // raw Viper comparison below + vprBufferSize <- ctx.expression(viper.gobra.ast.internal.utility.IntKindAlignment.asUnboundedInt(bufferSizeArg, underlyingType(bufferSizeArg.typ)(ctx))) // assert 0 <= [bufferSize] vprIsBufferSizePositive = vpr.LeCmp(vpr.IntLit(0)(pos, info, errT), vprBufferSize)(pos, info, errT) @@ -155,7 +162,7 @@ class ChannelEncoding extends LeafTypeEncoding { _ <- write(vprIsChannelInhale) // inhale [a].BufferSize() == [bufferSize] - bufferSizeCall = in.PureMethodCall(a, bufferSizeMProxy, Vector(), in.IntT(Addressability.outParameter), false)(makeStmt.info) + bufferSizeCall = in.PureMethodCall(a, bufferSizeMProxy, Vector(), in.IntT(Addressability.outParameter, goIntKind), false)(makeStmt.info) bufferSizeEq = in.EqCmp(bufferSizeCall, bufferSizeArg)(makeStmt.info) vprBufferSizeEq <- ctx.expression(bufferSizeEq) vprBufferSizeInhale = vpr.Inhale(vprBufferSizeEq)(pos, info, errT) @@ -179,14 +186,14 @@ class ChannelEncoding extends LeafTypeEncoding { ) // exhale [c].SendGivenPerm()([m]) - sendGivenPermInst = getChannelInvariantAccess(channel, sendGivenPerm, Vector(message), Vector(typeParam))(stmt.info) + sendGivenPermInst = getChannelInvariantAccess(channel, sendGivenPerm, Vector(message), Vector(typeParam))(stmt.info)(ctx) vprSendGivenPermInst <- ctx.assertion(sendGivenPermInst) _ <- exhale(vprSendGivenPermInst, (info, _) => ChannelSendError(info) dueTo InsufficientPermissionFromTagError(s"${channel.info.tag}.SendGivenPerm()(${message.info.tag})") ) // inhale [c].SendGotPerm()() - sendGotPermInst = getChannelInvariantAccess(channel, sendGotPerm, Vector.empty, Vector.empty)(stmt.info) + sendGotPermInst = getChannelInvariantAccess(channel, sendGotPerm, Vector.empty, Vector.empty)(stmt.info)(ctx) vprSendGotPermInst <- ctx.assertion(sendGotPermInst) vprInhaleSendGotPermInst = vpr.Inhale(vprSendGotPermInst)(pos, info, errT) } yield vprInhaleSendGotPermInst @@ -208,7 +215,7 @@ class ChannelEncoding extends LeafTypeEncoding { ) // exhale [c].RecvGivenPerm()() - recvGivenPermInst = getChannelInvariantAccess(channel, recvGivenPerm, Vector.empty, Vector.empty)(stmt.info) + recvGivenPermInst = getChannelInvariantAccess(channel, recvGivenPerm, Vector.empty, Vector.empty)(stmt.info)(ctx) vprRecvGivenPermInst <- ctx.assertion(recvGivenPermInst) _ <- exhale(vprRecvGivenPermInst, (info, _) => ChannelReceiveError(info) dueTo InsufficientPermissionFromTagError(s"${channel.info.tag}.RecvGivenPerm()()") @@ -227,7 +234,7 @@ class ChannelEncoding extends LeafTypeEncoding { _ <- write(vprInhaleRecvChannelFull) // inhale ok ==> [c].RecvGotPerm()(res) - recvGotPermInst = getChannelInvariantAccess(channel, recvGotPerm, Vector(res), Vector(typeParam))(stmt.info) + recvGotPermInst = getChannelInvariantAccess(channel, recvGotPerm, Vector(res), Vector(typeParam))(stmt.info)(ctx) okImpl = in.Implication(ok, recvGotPermInst)(stmt.info) vprOkImpl <- ctx.assertion(okImpl) vprInhaleOkImpl = vpr.Inhale(vprOkImpl)(pos, info, errT) @@ -259,10 +266,21 @@ class ChannelEncoding extends LeafTypeEncoding { /** * Constructs `[channel].invariant()([args])` */ - private def getChannelInvariantAccess(channel: in.Expr, invariant: in.MethodProxy, args: Vector[in.Expr], argTypes: Vector[in.Type])(src: Source.Parser.Info): in.Access = { + private def getChannelInvariantAccess(channel: in.Expr, invariant: in.MethodProxy, args: Vector[in.Expr], argTypes: Vector[in.Type])(src: Source.Parser.Info)(ctx: Context): in.Access = { require(args.length == argTypes.length) + // The message's integer kind may differ from the channel element type (a `chan int` is + // fed an untyped constant, or a defined type over `int`): the predicate-expression's + // `eval` expects the element type's Viper sort (a bounded domain), so insert an explicit + // Conversion at each such argument. Without it, e.g. `ch <- 42` passes a raw Int to an + // `eval_Pred(..., Bounded_int)` and Viper's consistency check fails. + val alignedArgs = args.zip(argTypes).map { case (arg, argT) => + (underlyingType(arg.typ)(ctx), underlyingType(argT)(ctx)) match { + case (in.IntT(_, ak), in.IntT(_, tk)) if ak != tk => in.Conversion(argT.withAddressability(Addressability.rValue), arg)(arg.info) + case _ => arg + } + } val permReturnT = in.PredT(argTypes, Addressability.outParameter) val permPred = in.PureMethodCall(channel, invariant, Vector(), permReturnT, false)(src) - in.Access(in.Accessible.PredExpr(in.PredExprInstance(permPred, args)(src)), in.FullPerm(src))(src) + in.Access(in.Accessible.PredExpr(in.PredExprInstance(permPred, alignedArgs)(src)), in.FullPerm(src))(src) } } diff --git a/src/main/scala/viper/gobra/translator/encodings/defaults/DefaultPureMethodEncoding.scala b/src/main/scala/viper/gobra/translator/encodings/defaults/DefaultPureMethodEncoding.scala index ca48ed103..8daec1492 100644 --- a/src/main/scala/viper/gobra/translator/encodings/defaults/DefaultPureMethodEncoding.scala +++ b/src/main/scala/viper/gobra/translator/encodings/defaults/DefaultPureMethodEncoding.scala @@ -11,13 +11,38 @@ import viper.gobra.ast.{internal => in} import viper.gobra.translator.encodings.combinators.Encoding import viper.gobra.translator.context.Context import viper.gobra.translator.util.VprInfo +import viper.gobra.translator.Names import viper.silver.{ast => vpr} class DefaultPureMethodEncoding extends Encoding { import viper.gobra.translator.util.ViperWriter._ + import viper.gobra.translator.util.TypePatterns._ import MemberLevel._ + /** + * Aligns the encoded body of a pure function/method with the declared result sort. A + * bounded-kind result may receive a body that encodes to plain Int (untyped-constant + * conditional branches, length expressions), and vice versa; the resulting vpr.Function + * would be ill-sorted. Mirrors AssertionEncoding.alignLetBinding. + */ + private def alignResult(ctx: Context)(resultTyp: in.Type, declared: vpr.Type, body: vpr.Exp): vpr.Exp = + (declared, body.typ) match { + case (dt: vpr.DomainType, vpr.Int) + if ctx.BoundedInt.unapply(resultTyp).exists(k => dt.domainName == Names.boundedIntDomain(k)) => + val k = ctx.BoundedInt.unapply(resultTyp).get + vpr.DomainFuncApp( + Names.boundedIntTo(k), Seq(body), Map.empty + )(body.pos, body.info, dt, Names.boundedIntDomain(k), body.errT) + case (vpr.Int, dt: vpr.DomainType) + if ctx.BoundedInt.unapply(resultTyp).exists(k => dt.domainName == Names.boundedIntDomain(k)) => + val k = ctx.BoundedInt.unapply(resultTyp).get + vpr.DomainFuncApp( + Names.boundedIntFrom(k), Seq(body), Map.empty + )(body.pos, body.info, vpr.Int, Names.boundedIntDomain(k), body.errT) + case _ => body + } + override def function(ctx: Context): in.Member ==> MemberWriter[vpr.Function] = { case x: in.PureMethod => pureMethodDefault(x)(ctx) case x: in.PureFunction => pureFunctionDefault(x)(ctx) @@ -47,14 +72,14 @@ class DefaultPureMethodEncoding extends Encoding { for { pres <- sequence((vRecvPres ++ vArgPres) ++ meth.pres.map(ctx.precondition)) - posts <- sequence(vResultPosts ++ meth.posts.map(ctx.postcondition(_).map(fixResultvar(_)))) + posts <- sequence(vResultPosts.map(_.map(fixResultvar)) ++ meth.posts.map(ctx.postcondition(_).map(fixResultvar(_)))) measures <- sequence(meth.terminationMeasures.map(e => pure(ctx.assertion(e))(ctx))) body <- option(meth.body map { b => pure( for { results <- ctx.expression(b) - } yield results + } yield alignResult(ctx)(meth.results.head.typ, resultType, results) )(ctx) }) @@ -93,14 +118,14 @@ class DefaultPureMethodEncoding extends Encoding { for { pres <- sequence(vArgPres ++ func.pres.map(ctx.precondition)) - posts <- sequence(vResultPosts ++ func.posts.map(ctx.postcondition(_).map(fixResultvar(_)))) + posts <- sequence(vResultPosts.map(_.map(fixResultvar)) ++ func.posts.map(ctx.postcondition(_).map(fixResultvar(_)))) measures <- sequence(func.terminationMeasures.map(e => pure(ctx.assertion(e))(ctx))) body <- option(func.body map { b => pure( for { results <- ctx.expression(b) - } yield results + } yield alignResult(ctx)(func.results.head.typ, resultType, results) )(ctx) }) diff --git a/src/main/scala/viper/gobra/translator/encodings/maps/MapEncoding.scala b/src/main/scala/viper/gobra/translator/encodings/maps/MapEncoding.scala index 398aa9414..1f372ace2 100644 --- a/src/main/scala/viper/gobra/translator/encodings/maps/MapEncoding.scala +++ b/src/main/scala/viper/gobra/translator/encodings/maps/MapEncoding.scala @@ -204,10 +204,12 @@ class MapEncoding extends LeafTypeEncoding { case makeStmt@in.MakeMap(target, t@in.MapT(keys, values, _), makeArg) => val (pos, info, errT) = makeStmt.vprMeta - // Runtime check asserting 0 <= [n] + // Runtime check asserting 0 <= [n]. The size argument is projected to a mathematical + // integer first: `make(map[int]int, 1)` types `1` as Go `int`, whose direct encoding + // is a `Bounded_int` domain value — not the Viper Int the raw comparison needs. val runtimeCheck = makeArg.toVector map { n => for { - nVpr <- goE(n) + nVpr <- goE(viper.gobra.ast.internal.utility.IntKindAlignment.asUnboundedInt(n, underlyingType(n.typ)(ctx))) runtimeCheckExp = vpr.LeCmp(vpr.IntLit(0)(pos, info, errT), nVpr)(pos, info, errT) } yield vpr.Exhale(runtimeCheckExp)(pos, info, errT) } diff --git a/src/main/scala/viper/gobra/translator/encodings/sequences/SequenceEncoding.scala b/src/main/scala/viper/gobra/translator/encodings/sequences/SequenceEncoding.scala index cd81a08c0..c872849b7 100644 --- a/src/main/scala/viper/gobra/translator/encodings/sequences/SequenceEncoding.scala +++ b/src/main/scala/viper/gobra/translator/encodings/sequences/SequenceEncoding.scala @@ -7,6 +7,7 @@ package viper.gobra.translator.encodings.sequences import org.bitbucket.inkytonik.kiama.==> +import viper.gobra.ast.internal.utility.IntKindAlignment import viper.gobra.ast.{internal => in} import viper.gobra.reporting.Source import viper.gobra.theory.Addressability.{Exclusive, Shared} @@ -16,7 +17,7 @@ import viper.gobra.translator.context.Context import viper.gobra.translator.util.FunctionGenerator import viper.gobra.translator.util.ViperUtil.synthesized import viper.gobra.translator.util.ViperWriter.CodeWriter -import viper.gobra.util.Violation +import viper.gobra.util.{TypeBounds, Violation} import viper.silver.plugin.standard.termination import viper.silver.{ast => vpr} @@ -27,6 +28,54 @@ class SequenceEncoding extends LeafTypeEncoding { override def finalize(addMemberFn: vpr.Member => Unit): Unit = { emptySeqFunc.finalize(addMemberFn) + seqToBoundedFunc.finalize(addMemberFn) + } + + /** + * Generates, per bounded integer kind k, a mapping function from mathematical-integer + * sequences to sequences of the bounded domain type: + * + * function k$seqToBounded(s: Seq[Int]): Seq[Bounded_k] + * ensures |result| == |s| + * ensures forall i: Int :: { result[i] } 0 <= i && i < |s| ==> result[i] == k$to(s[i]) + * + * Used to encode `in.Conversion`s between `seq[integer]` and `seq[k]`, which the desugarer + * inserts when a mathematical sequence (e.g. a range `seq[a..b]`, which Viper only supports + * over Int) meets a bounded-element context (e.g. `seq[1..4] == seq[int]{1,2,3}`). + */ + private val seqToBoundedFunc: FunctionGenerator[TypeBounds.BoundedIntegerKind] = new FunctionGenerator[TypeBounds.BoundedIntegerKind] { + override def genFunction(k: TypeBounds.BoundedIntegerKind)(ctx: Context): vpr.Function = { + val domT = vpr.DomainType(Names.boundedIntDomain(k), Map.empty[vpr.TypeVar, vpr.Type])(Seq.empty) + val sDecl = vpr.LocalVarDecl("s", vpr.SeqType(vpr.Int))() + val s = sDecl.localVar + val result = vpr.Result(vpr.SeqType(domT))() + val iDecl = vpr.LocalVarDecl("i", vpr.Int)() + val i = iDecl.localVar + val toElem = vpr.DomainFuncApp( + funcname = Names.boundedIntTo(k), + args = Seq(vpr.SeqIndex(s, i)()), + typVarMap = Map.empty + )(vpr.NoPosition, vpr.NoInfo, domT, Names.boundedIntDomain(k), vpr.NoTrafos) + + val lenPost = vpr.EqCmp(vpr.SeqLength(result)(), vpr.SeqLength(s)())() + val idxPost = vpr.Forall( + Seq(iDecl), + Seq(vpr.Trigger(Seq(vpr.SeqIndex(result, i)()))()), + vpr.Implies( + vpr.And(vpr.LeCmp(vpr.IntLit(0)(), i)(), vpr.LtCmp(i, vpr.SeqLength(s)())())(), + vpr.EqCmp(vpr.SeqIndex(result, i)(), toElem)() + )() + )() + + vpr.Function( + name = s"${k.name}$$seqToBounded", + formalArgs = Seq(sDecl), + typ = vpr.SeqType(domT), + pres = Seq(synthesized(termination.DecreasesWildcard(None))("This function is assumed to terminate")), + posts = Seq(lenPost, idxPost), + body = None + )() + } } /** @@ -84,17 +133,26 @@ class SequenceEncoding extends LeafTypeEncoding { val (pos, info, errT) = n.vprMeta for { vE <- goE(e) - vIdx <- goE(idx) + // sequence indices are Viper Ints; a bounded-int index (e.g. a Go `int` loop + // variable) must be projected via `from` + vIdx <- goE(IntKindAlignment.asUnboundedInt(idx, underlyingType(idx.typ)(ctx))) } yield vpr.SeqIndex(vE, vIdx)(pos, info, errT) case n@ in.GhostCollectionUpdate(base :: ctx.Seq(_), left, right, _) => val (pos, info, errT) = n.vprMeta for { vBase <- goE(base) - vLeft <- goE(left) + vLeft <- goE(IntKindAlignment.asUnboundedInt(left, underlyingType(left.typ)(ctx))) vRight <- goE(right) } yield vpr.SeqUpdate(vBase, vLeft, vRight)(pos, info, errT) + // seq[integer] -> seq[k] for a bounded kind k: apply the per-kind mapping function + // (see seqToBoundedFunc). Inserted by the desugarer for range sequences in a + // bounded-element context and by IntKindAlignment when aligning collection operands. + case n@ in.Conversion(in.SequenceT(in.IntT(_, k: TypeBounds.BoundedIntegerKind), _), expr :: ctx.Seq(in.IntT(_, TypeBounds.UnboundedInteger | TypeBounds.UntypedConstInteger)))=> + val (pos, info, errT) = n.vprMeta + for { vE <- goE(expr) } yield seqToBoundedFunc(Vector(vE), k)(pos, info, errT)(ctx) + case (e: in.DfltVal) :: ctx.Seq(t) / Exclusive => unit(withSrc(vpr.EmptySeq(ctx.typ(t)), e)) @@ -146,8 +204,8 @@ class SequenceEncoding extends LeafTypeEncoding { case n@ in.RangeSequence(low, high) => val (pos, info, errT) = n.vprMeta for { - lowT <- goE(low) - highT <- goE(high) + lowT <- goE(IntKindAlignment.asUnboundedInt(low, underlyingType(low.typ)(ctx))) + highT <- goE(IntKindAlignment.asUnboundedInt(high, underlyingType(high.typ)(ctx))) } yield vpr.RangeSeq(lowT, highT)(pos, info, errT) case n: in.SequenceAppend => @@ -161,14 +219,14 @@ class SequenceEncoding extends LeafTypeEncoding { val (pos, info, errT) = n.vprMeta for { leftT <- goE(n.left) - rightT <- goE(n.right) + rightT <- goE(IntKindAlignment.asUnboundedInt(n.right, underlyingType(n.right.typ)(ctx))) } yield vpr.SeqDrop(leftT, rightT)(pos, info, errT) case n: in.SequenceTake => val (pos, info, errT) = n.vprMeta for { leftT <- goE(n.left) - rightT <- goE(n.right) + rightT <- goE(IntKindAlignment.asUnboundedInt(n.right, underlyingType(n.right.typ)(ctx))) } yield vpr.SeqTake(leftT, rightT)(pos, info, errT) } } diff --git a/src/main/scala/viper/gobra/translator/encodings/slices/SliceEncoding.scala b/src/main/scala/viper/gobra/translator/encodings/slices/SliceEncoding.scala index f2fd03fdb..93c9c3c80 100644 --- a/src/main/scala/viper/gobra/translator/encodings/slices/SliceEncoding.scala +++ b/src/main/scala/viper/gobra/translator/encodings/slices/SliceEncoding.scala @@ -155,8 +155,11 @@ class SliceEncoding(arrayEmb : SharedArrayEmbedding) extends LeafTypeEncoding { _ <- local(vprSlice) capArg = optCapArg.getOrElse(lenArg) - vprLength <- ctx.expression(lenArg) - vprCapacity <- ctx.expression(capArg) + // Project the (possibly bounded-int) size arguments to mathematical integers before + // building raw Viper comparisons / quantifier bounds: `make([]T, 6)` types `6` as Go + // `int`, whose direct encoding is a `Bounded_int` domain value — not a Viper Int. + vprLength <- ctx.expression(viper.gobra.ast.internal.utility.IntKindAlignment.asUnboundedInt(lenArg, underlyingType(lenArg.typ)(ctx))) + vprCapacity <- ctx.expression(viper.gobra.ast.internal.utility.IntKindAlignment.asUnboundedInt(capArg, underlyingType(capArg.typ)(ctx))) // Perform additional runtime checks of conditions that must be true when make is invoked, otherwise the program panics (according to the go spec) // asserts 0 <= [len] && 0 <= [cap] && [len] <= [cap] @@ -179,12 +182,17 @@ class SliceEncoding(arrayEmb : SharedArrayEmbedding) extends LeafTypeEncoding { lenExpr = in.Length(slice)(makeStmt.info) capExpr = in.Capacity(slice)(makeStmt.info) + // capArg/lenArg can have a bounded integer kind (Go specifies `make`'s size + // parameter as `int`); align with the unbounded kind of in.Capacity/in.Length. + (alignedCapL, alignedCapR) = viper.gobra.ast.internal.utility.IntKindAlignment.alignIntKinds(capExpr, capArg) + (alignedLenL, alignedLenR) = viper.gobra.ast.internal.utility.IntKindAlignment.alignIntKinds(lenExpr, lenArg) + // inhale cap(a) == [cap] - eqCap <- ctx.equal(capExpr, capArg)(makeStmt) + eqCap <- ctx.equal(alignedCapL, alignedCapR)(makeStmt) _ <- write(vpr.Inhale(eqCap)(pos, info, errT)) // inhale len(a) == [len] - eqLen <- ctx.equal(lenExpr, lenArg)(makeStmt) + eqLen <- ctx.equal(alignedLenL, alignedLenR)(makeStmt) _ <- write(vpr.Inhale(eqLen)(pos, info, errT)) // inhale forall i: int :: {loc(a, i)} 0 <= i && i < [len] ==> [ a[i] == dfltVal(T) ] @@ -295,7 +303,8 @@ class SliceEncoding(arrayEmb : SharedArrayEmbedding) extends LeafTypeEncoding { override def reference(ctx : Context) : in.Location ==> CodeWriter[vpr.Exp] = default(super.reference(ctx)) { case (exp @ in.IndexedExp(base :: ctx.Slice(_), idx, _)) :: _ / Shared => for { baseT <- ctx.expression(base) - idxT <- ctx.expression(idx) + // slice indices are Viper Ints; project bounded-int indices via `from` + idxT <- ctx.expression(viper.gobra.ast.internal.utility.IntKindAlignment.asUnboundedInt(idx, underlyingType(idx.typ)(ctx))) } yield withSrc(ctx.slice.loc(baseT, idxT), exp) } diff --git a/src/main/scala/viper/gobra/translator/encodings/typeless/AssertionEncoding.scala b/src/main/scala/viper/gobra/translator/encodings/typeless/AssertionEncoding.scala index d5d7d4146..fea14e092 100644 --- a/src/main/scala/viper/gobra/translator/encodings/typeless/AssertionEncoding.scala +++ b/src/main/scala/viper/gobra/translator/encodings/typeless/AssertionEncoding.scala @@ -14,7 +14,10 @@ import viper.gobra.translator.encodings.combinators.Encoding import viper.gobra.translator.context.Context import viper.gobra.translator.util.ViperWriter.CodeWriter import viper.gobra.util.Violation +import viper.gobra.util.TypeBounds.BoundedIntegerKind +import viper.gobra.translator.Names import viper.gobra.translator.util.{ViperUtil => vu} +import viper.silver.ast.utility.ViperStrategy import viper.silver.{ast => vpr} import viper.silver.plugin.standard.{refute => vprrefute} import viper.silver.plugin.sif._ @@ -22,6 +25,7 @@ import viper.silver.plugin.sif._ class AssertionEncoding extends Encoding { import viper.gobra.translator.util.ViperWriter.{CodeLevel => cl} + import viper.gobra.translator.util.TypePatterns._ import cl._ override def expression(ctx: Context): in.Expr ==> CodeWriter[vpr.Exp] = { @@ -43,18 +47,26 @@ class AssertionEncoding extends Encoding { case n@ in.PureForall(vars, triggers, body) => val (pos, info, errT) = n.vprMeta for { - (newVars, newTriggers, newBody) <- quantifier(vars, triggers, body)(ctx) - newForall = vpr.Forall(newVars, newTriggers, newBody)(pos, info, errT).autoTrigger + (newVars, newTriggers, guard, newBody) <- quantifier(vars, triggers, body)(ctx) + guardedBody = guard.fold(newBody)(g => vpr.Implies(g, newBody)(pos, info, errT)) + newForall = vu.dropBoundedFromOnlyTriggers(vpr.Forall(newVars, newTriggers, guardedBody)(pos, info, errT).autoTrigger) } yield newForall.check match { case Seq() => newForall case errors => Violation.violation(s"invalid trigger pattern (${errors.head.readableMessage})") } + // Existential bound variables of bounded kinds stay at the domain sort: domain values are + // intrinsically in-range (no guard needed) and, unlike the Int-plus-range-guard lowering + // used for universals, the domain sort does not break witness finding — a pure arithmetic + // guard gives Z3 no term to instantiate ('exists n int :: true' would fail), whereas SMT + // sorts are non-empty and ground 'to'/'from' terms anchor instantiation. case n@ in.Exists(vars, triggers, body) => + val newVars = vars map ctx.variable val (pos, info, errT) = n.vprMeta for { - (newVars, newTriggers, newBody) <- quantifier(vars, triggers, body)(ctx) - newExists = vpr.Exists(newVars, newTriggers, newBody)(pos, info, errT).autoTrigger + newTriggers <- sequence(triggers map (trigger(_)(ctx))) + newBody <- ctx.expression(body) + newExists = vu.dropBoundedFromOnlyTriggers(vpr.Exists(newVars, newTriggers, newBody)(pos, info, errT).autoTrigger) } yield newExists.check match { case Seq() => newExists case errors => Violation.violation(s"invalid trigger pattern (${errors.head.readableMessage})") @@ -65,7 +77,7 @@ class AssertionEncoding extends Encoding { exp <- ctx.expression(let.in) l = ctx.variable(let.left) r <- ctx.expression(let.right) - } yield withSrc(vpr.Let(l, r, exp), let) + } yield withSrc(vpr.Let(l, alignLetBinding(ctx)(l, let.left.typ, let.right.typ, r), exp), let) case as: in.Asserting => for { @@ -94,19 +106,23 @@ class AssertionEncoding extends Encoding { exp <- ctx.assertion(op) r <- ctx.expression(right) l = ctx.variable(left) - } yield withSrc(vpr.Let(l, r, exp), n) + } yield withSrc(vpr.Let(l, alignLetBinding(ctx)(l, left.typ, right.typ, r), exp), n) case n@ in.MagicWand(l, r) => for {vl <- ctx.assertion(l); vr <- ctx.assertion(r)} yield withSrc(vpr.MagicWand(vl, vr), n) case n@ in.Implication(l, r) => for {vl <- ctx.expression(l); vr <- ctx.assertion(r)} yield withSrc(vpr.Implies(vl, vr), n) case n@ in.SepForall(vars, triggers, body) => - val newVars = vars map ctx.variable + val lowering = BoundedQuantLowering(ctx, vars) + val newVars = lowering.decls val (pos, info, errT) = n.vprMeta for { - newTriggers <- sequence(triggers map (trigger(_)(ctx))) - newBody <- pure(ctx.assertion(body))(ctx) + rawTriggers <- sequence(triggers map (trigger(_)(ctx))) + newTriggers = lowering.rewriteTriggers(rawTriggers) + rawBody <- pure(ctx.assertion(body))(ctx) + rewrittenBody = lowering.rewrite(rawBody) + newBody = lowering.guard.fold(rewrittenBody)(g => vpr.Implies(g, rewrittenBody)(pos, info, errT)) newForall = vpr.Forall(newVars, newTriggers, newBody)(pos, info, errT) desugaredForall = vpr.utility.QuantifiedPermissions.desugarSourceQuantifiedPermissionSyntax(newForall) - triggeredForall = desugaredForall.map(_.autoTrigger) + triggeredForall = desugaredForall.map(f => vu.dropBoundedFromOnlyTriggers(f.autoTrigger)) reducedForall = triggeredForall.reduce[vpr.Exp] { (a, b) => vpr.And(a, b)(pos, info, errT) } } yield reducedForall } @@ -127,6 +143,8 @@ class AssertionEncoding extends Encoding { // The existential carries `cond`'s source info so error messages show // just `P` rather than the whole `var x T :| P` statement. val (condPos, condInfo, condErrT) = cond.vprMeta + // The witness existential keeps the bound variable at its (possibly domain) sort — see + // the in.Exists case for why the Int-plus-guard lowering is not used for existentials. val boundVar = in.BoundVar(v.id + "_B", v.typ.withAddressability(Addressability.boundVariable))(v.info) val renaming: Map[in.LocalVar, in.Node] = Map(v -> boundVar) val renamedCond = cond.replace(renaming) @@ -134,7 +152,7 @@ class AssertionEncoding extends Encoding { val vprBoundVar = ctx.variable(boundVar) seqnUnits(Vector(for { vprBody <- ctx.expression(renamedCond) - existsExpr = vpr.Exists(Seq(vprBoundVar), Seq.empty, vprBody)(condPos, condInfo, condErrT).autoTrigger + existsExpr = vu.dropBoundedFromOnlyTriggers(vpr.Exists(Seq(vprBoundVar), Seq.empty, vprBody)(condPos, condInfo, condErrT).autoTrigger) condEnc <- ctx.assertion(condAss) _ <- assert(existsExpr, (info, _) => AssignSuchThatError(info) dueTo AssignSuchThatNoWitnessError(info) @@ -208,18 +226,131 @@ class AssertionEncoding extends Encoding { } yield vpr.Apply(w)(pos, info, errT) } + /** + * Aligns the encoded right-hand side of a let binding with the bound variable's encoded + * sort. A bounded-kind let variable is encoded at the Bounded_k domain sort, but its + * right-hand side may encode to a plain Int even when its internal type claims a bounded + * kind (e.g. `let lenR := len(b) in ...` — container lengths encode to the raw `slen` + * application). Without alignment the resulting vpr.Let is ill-sorted, which crashes the + * backend. The comparison is on the *encoded* Viper sorts, so it is robust to internal + * types whose encodings differ; mirrors the normalization BoundedIntEncoding applies to + * assignments. + */ + private def alignLetBinding(ctx: Context)(l: vpr.LocalVarDecl, leftTyp: in.Type, rightTyp: in.Type, r: vpr.Exp): vpr.Exp = + (l.typ, r.typ) match { + case (dt: vpr.DomainType, vpr.Int) + if ctx.BoundedInt.unapply(leftTyp).exists(k => dt.domainName == Names.boundedIntDomain(k)) => + val k = ctx.BoundedInt.unapply(leftTyp).get + vpr.DomainFuncApp( + Names.boundedIntTo(k), Seq(r), Map.empty + )(r.pos, r.info, dt, Names.boundedIntDomain(k), r.errT) + case (vpr.Int, dt: vpr.DomainType) + if ctx.BoundedInt.unapply(rightTyp).exists(k => dt.domainName == Names.boundedIntDomain(k)) => + val k = ctx.BoundedInt.unapply(rightTyp).get + vpr.DomainFuncApp( + Names.boundedIntFrom(k), Seq(r), Map.empty + )(r.pos, r.info, vpr.Int, Names.boundedIntDomain(k), r.errT) + case _ => r + } + def trigger(trigger: in.Trigger)(ctx: Context) : CodeWriter[vpr.Trigger] = { val (pos, info, errT) = trigger.vprMeta for { expr <- sequence(trigger.exprs map ctx.triggerExpr)} yield vpr.Trigger(expr)(pos, info, errT) } - def quantifier(vars: Vector[in.BoundVar], triggers: Vector[in.Trigger], body: in.Expr)(ctx: Context) : CodeWriter[(Seq[vpr.LocalVarDecl], Seq[vpr.Trigger], vpr.Exp)] = { - val newVars = vars map ctx.variable + def quantifier(vars: Vector[in.BoundVar], triggers: Vector[in.Trigger], body: in.Expr)(ctx: Context) : CodeWriter[(Seq[vpr.LocalVarDecl], Seq[vpr.Trigger], Option[vpr.Exp], vpr.Exp)] = { + val lowering = BoundedQuantLowering(ctx, vars) for { newTriggers <- sequence(triggers map (trigger(_)(ctx))) newBody <- ctx.expression(body) - } yield (newVars, newTriggers, newBody) + } yield (lowering.decls, lowering.rewriteTriggers(newTriggers), lowering.guard, lowering.rewrite(newBody)) + } + + /** + * Lowering for universally quantified variables of bounded integer kinds (existentials keep + * the domain sort — see the in.Exists case). + * + * A bound variable declared at a bounded integer kind `k` ranges over exactly the values of + * the corresponding `Bounded_k` domain (`forall x uint8 :: x >= 0` holds). Binding the Viper + * variable at the domain sort, however, would force every arithmetic or indexing use of the + * variable through `k$from(x)`, which destroys the linear injective receivers Silicon needs + * for quantified permissions (`acc(&s[x])` would become `sadd(offset, from(x))`, a shape on + * which Z3's inverse-function reasoning diverges). The variable is therefore bound at the + * `Int` sort, the kind's range is added as an explicit guard, and body and triggers are + * rewritten: + * - `k$from(x)` --> `x` (now Int-sorted) + * - any remaining domain-sorted use of `x` --> `k$inv(x)` + * This is equivalent to quantifying over the domain: by the bridge axioms `inv(from(x)) == x` + * and `from(to(n)) == n`, `from` restricts to a bijection between the domain values and + * `[lower, upper]` whose inverse on that range is `inv` (== `to` there). `inv` is used + * rather than `to` because its axiom triggers on `{ from(x) }`: every ground projected + * value `i` yields a known `inv(from(i)) == i`, so a lowered trigger like `{ m[inv(v)] }` + * e-matches the ground `m[i]` via congruence — with `to`, the corresponding link + * `to(from(i)) == i` is never established and quantifiers over map keys, set elements, + * etc. of bounded kinds silently fail to instantiate. + * + * Under `--unboundedIntegers`, `ctx.BoundedInt` matches nothing and the lowering is a no-op. + */ + private case class BoundedQuantLowering(ctx: Context, vars: Vector[in.BoundVar]) { + // maps the encoded variable name to the bounded kind, for bound variables of bounded kinds + private val lowered: Map[String, BoundedIntegerKind] = + vars.flatMap(x => ctx.BoundedInt.unapply(x.typ).map(k => ctx.variable(x).name -> k)).toMap + + private val isTrivial: Boolean = lowered.isEmpty + + /** The bound-variable declarations, with lowered variables declared at the Int sort. */ + def decls: Seq[vpr.LocalVarDecl] = vars.map { x => + val decl = ctx.variable(x) + if (lowered.contains(decl.name)) vpr.LocalVarDecl(decl.name, vpr.Int)(decl.pos, decl.info, decl.errT) + else decl + } + + /** Conjunction of the range guards `lower <= x && x <= upper` of all lowered variables. */ + def guard: Option[vpr.Exp] = { + val conjuncts = vars.flatMap { x => + val decl = ctx.variable(x) + lowered.get(decl.name).map { k => + val v = vpr.LocalVar(decl.name, vpr.Int)(decl.pos, decl.info, decl.errT) + vpr.And( + vpr.LeCmp(vpr.IntLit(k.lower)(decl.pos, decl.info, decl.errT), v)(decl.pos, decl.info, decl.errT), + vpr.LeCmp(v, vpr.IntLit(k.upper)(decl.pos, decl.info, decl.errT))(decl.pos, decl.info, decl.errT) + )(decl.pos, decl.info, decl.errT): vpr.Exp + } + } + conjuncts.reduceOption((a, b) => vpr.And(a, b)(a.pos, a.info, a.errT)) + } + + private def isFromOfLowered(app: vpr.DomainFuncApp): Boolean = app.args match { + case Seq(lv: vpr.LocalVar) => lowered.get(lv.name).exists(k => app.funcname == Names.boundedIntFrom(k)) + case _ => false + } + + /** Rewrites `from(x)` to Int-sorted `x` and remaining domain-sorted `x` to `to(x)`. */ + def rewrite[T <: vpr.Node](n: T): T = + if (isTrivial) n else ViperStrategy.Slim({ + case app: vpr.DomainFuncApp if isFromOfLowered(app) => + val lv = app.args.head.asInstanceOf[vpr.LocalVar] + vpr.LocalVar(lv.name, vpr.Int)(app.pos, app.info, app.errT) + case lv: vpr.LocalVar if lv.typ != vpr.Int && lowered.contains(lv.name) => + val k = lowered(lv.name) + vpr.DomainFuncApp( + Names.boundedIntInv(k), + Seq(vpr.LocalVar(lv.name, vpr.Int)(lv.pos, lv.info, lv.errT)), + Map.empty + )(lv.pos, lv.info, vpr.DomainType(Names.boundedIntDomain(k), Map.empty)(Seq.empty), Names.boundedIntDomain(k), lv.errT) + }).execute[T](n) + + /** + * Rewrites trigger expressions, dropping those that degenerate to a bare bound variable + * (`{ from(x) }` becomes `{ x }`, which is not a valid trigger term) and any trigger left + * without expressions. A quantifier that loses all triggers falls back to auto-triggering. + */ + def rewriteTriggers(ts: Seq[vpr.Trigger]): Seq[vpr.Trigger] = + if (isTrivial) ts else ts.flatMap { t => + val exps = t.exps.map(rewrite(_)).filterNot(_.isInstanceOf[vpr.LocalVar]) + if (exps.isEmpty) None else Some(vpr.Trigger(exps)(t.pos, t.info, t.errT)) + } } } diff --git a/src/main/scala/viper/gobra/translator/encodings/typeless/BuiltInEncoding.scala b/src/main/scala/viper/gobra/translator/encodings/typeless/BuiltInEncoding.scala index 114d90580..6a47e307b 100644 --- a/src/main/scala/viper/gobra/translator/encodings/typeless/BuiltInEncoding.scala +++ b/src/main/scala/viper/gobra/translator/encodings/typeless/BuiltInEncoding.scala @@ -18,6 +18,7 @@ import viper.gobra.translator.context.Context import viper.gobra.translator.util.ViperWriter.MemberWriter import viper.gobra.translator.util.PrimitiveGenerator import viper.gobra.util.Computation +import viper.gobra.util.TypeBounds import viper.gobra.util.Violation.violation import viper.silver.{ast => vpr} @@ -27,7 +28,13 @@ import scala.language.postfixOps /** * Encodes built-in members by translating them to 'regular' members and calling the corresponding encoding */ -class BuiltInEncoding extends Encoding { +/** + * @param goIntKind the IntegerKind the frontend uses for Go's `int` type + * (depends on the 32-/64-bit configuration). Built-in members with + * Go-visible `int` results must declare this kind so their generated + * Viper signatures match the frontend-typed call sites. + */ +class BuiltInEncoding(goIntKind: TypeBounds.IntegerKind) extends Encoding { // the implementation uses 4 distinct generators (instead of a single one) such that the exposed // methods (i.e. method, function, fpredicate, and mpredicate) can return the translated 'regular' member. @@ -211,7 +218,9 @@ class BuiltInEncoding extends Encoding { */ assert(recv.addressability == Addressability.inParameter) val recvParam = in.Parameter.In("c", recv)(src) - val kParam = in.Parameter.Out("k", in.IntT(Addressability.outParameter))(src) + // Go-visible `int` result: use the configured `int` kind so the generated Viper + // function's return sort matches the frontend type of `c.BufferSize()` call sites. + val kParam = in.Parameter.Out("k", in.IntT(Addressability.outParameter, goIntKind))(src) val isChannelInst = builtInMPredAccessible(BuiltInMemberTag.IsChannelMPredTag, recvParam, Vector())(src)(ctx) val pres: Vector[in.Assertion] = Vector( in.Access(isChannelInst, in.WildcardPerm(src))(src), @@ -264,7 +273,7 @@ class BuiltInEncoding extends Encoding { val aParam = in.Parameter.In("A", predTType)(src) val bParam = in.Parameter.In("B", predType)(src) val isChannelInst = builtInMPredAccessible(BuiltInMemberTag.IsChannelMPredTag, recvParam, Vector())(src)(ctx) - val bufferSizeType = in.IntT(Addressability.inParameter) + val bufferSizeType = in.IntT(Addressability.inParameter, goIntKind) val bufferSizeCall = builtInPureMethodCall(BuiltInMemberTag.BufferSizeMethodTag, recvParam, Vector(), bufferSizeType)(src)(ctx) val predTrueProxy = getOrGenerateFPredicate(BuiltInMemberTag.PredTrueFPredTag, Vector())(src)(ctx) val predTrueConstr = in.PredicateConstructor(predTrueProxy, predType, Vector())(src) // pred_true{} @@ -570,7 +579,9 @@ class BuiltInEncoding extends Encoding { val args = Vector(dstParam, srcParam, pParam) // results - val resParam = in.Parameter.Out("res", in.IntT(Addressability.outParameter))(src) + // Go-visible `int` result (number of copied elements): use the configured `int` kind + // so the generated Viper function's return sort matches the frontend type at call sites. + val resParam = in.Parameter.Out("res", in.IntT(Addressability.outParameter, goIntKind))(src) val results = Vector(resParam) // preconditions diff --git a/src/main/scala/viper/gobra/translator/encodings/typeless/MemoryEncoding.scala b/src/main/scala/viper/gobra/translator/encodings/typeless/MemoryEncoding.scala index b5149518a..3ce168954 100644 --- a/src/main/scala/viper/gobra/translator/encodings/typeless/MemoryEncoding.scala +++ b/src/main/scala/viper/gobra/translator/encodings/typeless/MemoryEncoding.scala @@ -10,21 +10,33 @@ import org.bitbucket.inkytonik.kiama.==> import viper.gobra.ast.{internal => in} import viper.gobra.translator.encodings.combinators.Encoding import viper.gobra.translator.context.Context +import viper.gobra.translator.util.TypePatterns._ import viper.gobra.translator.util.ViperWriter.CodeWriter import viper.silver.{ast => vpr} class MemoryEncoding extends Encoding { + /** True iff neither expression has a bounded integer type. */ + private def noBoundedOperand(ctx: Context)(l: in.Expr, r: in.Expr): Boolean = + ctx.BoundedInt.unapply(l.typ).isEmpty && ctx.BoundedInt.unapply(r.typ).isEmpty + override def expression(ctx: Context): in.Expr ==> CodeWriter[vpr.Exp] = { case r: in.Ref => ctx.reference(r.ref.op) case x@ in.EqCmp(l, r) => ctx.goEqual(l, r)(x) case x@ in.UneqCmp(l, r) => ctx.goEqual(l, r)(x).map(v => withSrc(vpr.Not(v), x)) case x@ in.GhostEqCmp(l, r) => ctx.equal(l, r)(x) case x@ in.GhostUneqCmp(l, r) => ctx.equal(l, r)(x).map(v => withSrc(vpr.Not(v), x)) - case n@ in.LessCmp(l, r) => for {vl <- ctx.expression(l); vr <- ctx.expression(r)} yield withSrc(vpr.LtCmp(vl, vr), n) - case n@ in.AtMostCmp(l, r) => for {vl <- ctx.expression(l); vr <- ctx.expression(r)} yield withSrc(vpr.LeCmp(vl, vr), n) - case n@ in.GreaterCmp(l, r) => for {vl <- ctx.expression(l); vr <- ctx.expression(r)} yield withSrc(vpr.GtCmp(vl, vr), n) - case n@ in.AtLeastCmp(l, r) => for {vl <- ctx.expression(l); vr <- ctx.expression(r)} yield withSrc(vpr.GeCmp(vl, vr), n) + // Comparisons with a bounded-int operand on EITHER side are handled by BoundedIntEncoding + // (which projects both operands to Int via `from`). Guard here to avoid a + // SafeTypeEncodingCombiner "supported by more than one encoding" error. + case n@ in.LessCmp(l, r) if noBoundedOperand(ctx)(l, r) => + for {vl <- ctx.expression(l); vr <- ctx.expression(r)} yield withSrc(vpr.LtCmp(vl, vr), n) + case n@ in.AtMostCmp(l, r) if noBoundedOperand(ctx)(l, r) => + for {vl <- ctx.expression(l); vr <- ctx.expression(r)} yield withSrc(vpr.LeCmp(vl, vr), n) + case n@ in.GreaterCmp(l, r) if noBoundedOperand(ctx)(l, r) => + for {vl <- ctx.expression(l); vr <- ctx.expression(r)} yield withSrc(vpr.GtCmp(vl, vr), n) + case n@ in.AtLeastCmp(l, r) if noBoundedOperand(ctx)(l, r) => + for {vl <- ctx.expression(l); vr <- ctx.expression(r)} yield withSrc(vpr.GeCmp(vl, vr), n) } override def assertion(ctx: Context): in.Assertion ==> CodeWriter[vpr.Exp] = { diff --git a/src/main/scala/viper/gobra/translator/library/arrays/Arrays.scala b/src/main/scala/viper/gobra/translator/library/arrays/Arrays.scala index 8981f27cd..e9169c6c1 100644 --- a/src/main/scala/viper/gobra/translator/library/arrays/Arrays.scala +++ b/src/main/scala/viper/gobra/translator/library/arrays/Arrays.scala @@ -10,6 +10,15 @@ import viper.gobra.translator.library.Generator import viper.silver.{ast => vpr} trait Arrays extends Generator { + /** + * Upper bound for array lengths, set to the configured `int` kind's maximum under bounded + * integer semantics (`None` under `--unboundedIntegers`). Go guarantees that the number of + * elements of any array, slice, or string fits in `int`, so `len(a) <= MaxInt` is sound and + * needed for bounded-integer quantifiers to entail internally generated footprints over the + * unbounded length. Must be set before the first domain is generated. + */ + var intUpperBound: Option[BigInt] = None + def len(a: vpr.Exp)(pos: vpr.Position = vpr.NoPosition, info: vpr.Info = vpr.NoInfo, errT: vpr.ErrorTrafo = vpr.NoTrafos): vpr.Exp def loc(a: vpr.Exp, i: vpr.Exp)(pos: vpr.Position = vpr.NoPosition, info: vpr.Info = vpr.NoInfo, errT: vpr.ErrorTrafo = vpr.NoTrafos): vpr.Exp diff --git a/src/main/scala/viper/gobra/translator/library/arrays/ArraysImpl.scala b/src/main/scala/viper/gobra/translator/library/arrays/ArraysImpl.scala index 777a47556..c882a8c4d 100644 --- a/src/main/scala/viper/gobra/translator/library/arrays/ArraysImpl.scala +++ b/src/main/scala/viper/gobra/translator/library/arrays/ArraysImpl.scala @@ -88,11 +88,22 @@ class ArraysImpl extends Arrays { )() }(domainName = domainName) + // Go guarantees len fits in `int`: axiom { forall a :: {len(a)} len(a) <= MaxInt } + val lenUpperBounded = intUpperBound.map { bound => + vpr.AnonymousDomainAxiom { + vpr.Forall( + Seq(aDecl), + Seq(vpr.Trigger(Seq(lenFuncApp))()), + vpr.LeCmp(lenFuncApp, vpr.IntLit(bound)())() + )() + }(domainName = domainName) + } + val domain = vpr.Domain( name = domainName, typVars = Seq(typeVar), functions = Seq(locFunc, lenFunc, firstFunc, secondFunc), - axioms = Seq(injectivity, lenNonNeg) + axioms = Seq(injectivity, lenNonNeg) ++ lenUpperBounded )() generateDomain = true diff --git a/src/main/scala/viper/gobra/translator/library/fixpoints/FixpointImpl.scala b/src/main/scala/viper/gobra/translator/library/fixpoints/FixpointImpl.scala index 7355d975f..467d8bf40 100644 --- a/src/main/scala/viper/gobra/translator/library/fixpoints/FixpointImpl.scala +++ b/src/main/scala/viper/gobra/translator/library/fixpoints/FixpointImpl.scala @@ -27,9 +27,19 @@ class FixpointImpl extends Fixpoint { val getFunc = constantGetDomainFunc(gc.left)(ctx) val getFuncApp = get(gc.left)(ctx) + // The defining equation's RHS must land in the same Viper sort as the constant's + // declared type: with bounded integers encoded as domain types, a kind mismatch + // between the constant and its (possibly implicitly converted) value expression + // would otherwise equate values of different sorts (e.g. `const C = 42` with an + // untyped constant type but a value converted to bounded `int`). + val alignedRight = (gc.left.typ, gc.right.typ) match { + case (l: in.IntT, r: in.IntT) if l.kind != r.kind => + in.Conversion(l.withAddressability(viper.gobra.theory.Addressability.rValue), gc.right)(gc.right.info) + case _ => gc.right + } val getAxiom = vpr.NamedDomainAxiom( name = s"get_constant${gc.left.id}", - exp = vpr.EqCmp(getFuncApp, ctx.expression(gc.right).res)(pos, info, errT), + exp = vpr.EqCmp(getFuncApp, ctx.expression(alignedRight).res)(pos, info, errT), )(domainName = domainName) val domain = vpr.Domain( diff --git a/src/main/scala/viper/gobra/translator/library/slices/SlicesImpl.scala b/src/main/scala/viper/gobra/translator/library/slices/SlicesImpl.scala index aa1840cb6..04ce13599 100644 --- a/src/main/scala/viper/gobra/translator/library/slices/SlicesImpl.scala +++ b/src/main/scala/viper/gobra/translator/library/slices/SlicesImpl.scala @@ -11,7 +11,7 @@ import viper.silver.plugin.standard.termination import viper.gobra.translator.util.ViperUtil.synthesized import viper.silver.{ast => vpr} -class SlicesImpl(val arrays : Arrays) extends Slices { +class SlicesImpl(val arrays : Arrays, intUpperBound : Option[BigInt] = None) extends Slices { private val domainName : String = "Slice" private val typeVar : vpr.TypeVar = vpr.TypeVar("T") private val domainType: vpr.DomainType = vpr.DomainType(domainName, Map[vpr.TypeVar, vpr.Type](typeVar -> typeVar))(Seq(typeVar)) @@ -119,6 +119,33 @@ class SlicesImpl(val arrays : Arrays) extends Slices { )(domainName = domainName) } + /** + * Go guarantees that slice lengths and capacities fit in `int` (`len`/`cap` return `int`). + * Under bounded integer semantics these bounds make user quantifiers over `int`-typed + * indices (implicitly range-guarded) entail the internally generated footprints that + * quantify over the mathematical integers. + * {{{ + * axiom slice_len_leq_maxint { + * forall s : Slice[T] :: { slen(s) } slen(s) <= MaxInt + * } + * axiom slice_cap_leq_maxint { + * forall s : Slice[T] :: { scap(s) } scap(s) <= MaxInt + * } + * }}} + */ + private lazy val slice_bounded_length_axioms : Seq[vpr.DomainAxiom] = intUpperBound.toSeq.flatMap { bound => + val sDecl = vpr.LocalVarDecl("s", domainType)() + Seq(len(sDecl.localVar)(), cap(sDecl.localVar)()).map { app => + vpr.AnonymousDomainAxiom( + vpr.Forall( + Seq(sDecl), + Seq(vpr.Trigger(Seq(app))()), + vpr.LeCmp(app, vpr.IntLit(bound)())() + )() + )(domainName = domainName) + } + } + /** * {{{ * axiom slice_len_leq_cap { @@ -349,7 +376,8 @@ class SlicesImpl(val arrays : Arrays) extends Slices { Seq(sarray_func, soffset_func, slen_func, scap_func, smake_func), slice_offset_nonneg_axiom +: slice_len_nonneg_axiom +: slice_len_leq_cap_axiom +: slice_cap_leq_alen_axiom +: - slice_constructor_over_deconstructor +: slice_deconstructors_over_constructor, + slice_constructor_over_deconstructor +: + (slice_bounded_length_axioms ++ slice_deconstructors_over_constructor), Seq(typeVar) )() diff --git a/src/main/scala/viper/gobra/translator/util/TypePatterns.scala b/src/main/scala/viper/gobra/translator/util/TypePatterns.scala index c2c2813bd..baea65b9f 100644 --- a/src/main/scala/viper/gobra/translator/util/TypePatterns.scala +++ b/src/main/scala/viper/gobra/translator/util/TypePatterns.scala @@ -11,6 +11,7 @@ import viper.gobra.ast.{internal => in} import viper.gobra.theory.Addressability import viper.gobra.theory.Addressability.{Exclusive, Shared} import viper.gobra.translator.context.Context +import viper.gobra.util.TypeBounds import scala.annotation.tailrec @@ -81,6 +82,38 @@ object TypePatterns { underlyingType(arg)(ctx).isInstanceOf[in.IntT] } + /** + * Matches bounded integer types (int8, uint8, int32, etc.) and extracts the kind. + * + * Under `--unboundedIntegers` this pattern matches nothing: every integer is then treated as + * an unbounded `Int`, so bounded-integer handling (BoundedIntEncoding, the bounded comparison + * guards in MemoryEncoding, ...) is disabled and all integers flow through IntEncoding — exactly + * as before the sound bounded-integer semantics were introduced. + */ + object BoundedInt { + def unapply(arg: in.Type): Option[TypeBounds.BoundedIntegerKind] = + if (ctx.unboundedIntegers) None + else underlyingType(arg)(ctx) match { + case in.IntT(_, k: TypeBounds.BoundedIntegerKind) => Some(k) + case _ => None + } + } + + /** + * Matches the ghost `integer` type and untyped integer constants (both encode as vpr.Int). + * + * Under `--unboundedIntegers` this additionally matches every bounded integer kind, so that + * IntEncoding encodes all integers as the mathematical (unbounded) `Int`. + */ + object UnboundedInt { + def unapply(arg: in.Type): Boolean = + underlyingType(arg)(ctx) match { + case in.IntT(_, TypeBounds.UnboundedInteger | TypeBounds.UntypedConstInteger) => true + case _: in.IntT if ctx.unboundedIntegers => true + case _ => false + } + } + object Void { def unapply(arg: in.Type): Boolean = underlyingType(arg)(ctx) == in.VoidT diff --git a/src/main/scala/viper/gobra/translator/util/ViperUtil.scala b/src/main/scala/viper/gobra/translator/util/ViperUtil.scala index 7a4abf539..c630395d8 100644 --- a/src/main/scala/viper/gobra/translator/util/ViperUtil.scala +++ b/src/main/scala/viper/gobra/translator/util/ViperUtil.scala @@ -108,4 +108,41 @@ object ViperUtil { /** Adds simple (source) information to a node without source information. */ def synthesized[T](node: (Position, Info, ErrorTrafo) => T)(comment: String): T = node(NoPosition, SimpleInfo(Seq(comment)), NoTrafos) + + /** + * Drops auto-inferred trigger sets that consist solely of bounded-int `from` bridge + * applications (see [[viper.gobra.translator.Names.boundedIntFrom]]), provided at least one + * other trigger set remains. + * + * Rationale: quantifiers over bounded-int values compile comparisons like `0 <= j` to + * `k$from(j) >= 0`, so Silver's trigger inference discovers `{ k$from(j) }` as a valid + * trigger set — but `from` appears on essentially every bounded term in a trace, making + * this trigger fire for every bounded ground term ever created. That eagerness is a + * performance disaster (each loop iteration mints fresh terms, each of which re-instantiates + * every such quantifier). Before the domain encoding, plain integer comparisons contributed + * no trigger candidates at all, so inference picked heap- or container-based terms + * (`m[j]`, `j in domain(m)`, …); dropping the `from`-only sets restores exactly that + * behavior. If `from`-sets are the only candidates, they are kept — a quantifier without + * triggers would be worse (the backend or Z3 would infer something at least as permissive). + */ + def dropBoundedFromOnlyTriggers[T <: QuantifiedExp](q: T): T = { + def isFromApp(e: Exp): Boolean = e match { + case app: DomainFuncApp => + app.funcname.endsWith("$from") && app.domainName.startsWith("Bounded_") + case _ => false + } + q match { + case forall: Forall => + val (fromOnly, rest) = forall.triggers.partition(t => t.exps.nonEmpty && t.exps.forall(isFromApp)) + if (rest.nonEmpty && fromOnly.nonEmpty) + forall.copy(triggers = rest)(forall.pos, forall.info, forall.errT).asInstanceOf[T] + else q + case exists: Exists => + val (fromOnly, rest) = exists.triggers.partition(t => t.exps.nonEmpty && t.exps.forall(isFromApp)) + if (rest.nonEmpty && fromOnly.nonEmpty) + exists.copy(triggers = rest)(exists.pos, exists.info, exists.errT).asInstanceOf[T] + else q + case _ => q + } + } } diff --git a/src/test/resources/regressions/examples/evaluation/binary_search_tree.gobra b/src/test/resources/regressions/examples/evaluation/binary_search_tree.gobra index 68c84b3d3..39192bf9a 100644 --- a/src/test/resources/regressions/examples/evaluation/binary_search_tree.gobra +++ b/src/test/resources/regressions/examples/evaluation/binary_search_tree.gobra @@ -60,7 +60,7 @@ func (n *node) convert(oldLowerBound, oldUpperBound, newLowerBound, newUpperBoun ghost requires acc(t.tree(), _) -ensures forall i int :: (0 <= i && i + 1 < len(res) ==> res[i] < res[i + 1]) // ordered +ensures forall i integer :: (0 <= i && i + 1 < len(res) ==> res[i] < res[i + 1]) // ordered; index quantified over exact `integer` so `i + 1` cannot overflow pure func (t *Tree) sortedValues() (res seq[int]) { return unfolding acc(t.tree(), _) in (t.root == nil) ? seq[int] { } : t.root.sortedValues(none[int], none[int]) } @@ -68,8 +68,8 @@ pure func (t *Tree) sortedValues() (res seq[int]) { ghost requires acc(n.tree(), _) && n.sorted(lowerBound, upperBound) ensures n.sorted(lowerBound, upperBound) -ensures forall i int :: (0 <= i && i < len(res) ==> ((lowerBound != none[int] ==> res[i] > get(lowerBound)) && (upperBound != none[int] ==> res[i] < get(upperBound)))) -ensures forall i int :: (0 <= i && i + 1 < len(res) ==> res[i] < res[i + 1]) // ordered +ensures forall i integer :: (0 <= i && i < len(res) ==> ((lowerBound != none[int] ==> res[i] > get(lowerBound)) && (upperBound != none[int] ==> res[i] < get(upperBound)))) // index quantified over exact `integer` to match sequence-length arithmetic +ensures forall i integer :: (0 <= i && i + 1 < len(res) ==> res[i] < res[i + 1]) // ordered; index quantified over exact `integer` so `i + 1` cannot overflow pure func (n *node) sortedValues(lowerBound, upperBound option[int]) (res seq[int]) { return unfolding acc(n.tree(), _) in (n.left == nil ? seq[int]{ } : n.left.sortedValues(lowerBound, some(n.value))) ++ seq[int]{ n.value } ++ (n.right == nil ? seq[int]{ } : n.right.sortedValues(some(n.value), upperBound)) } diff --git a/src/test/resources/regressions/examples/evaluation/dutchflag.gobra b/src/test/resources/regressions/examples/evaluation/dutchflag.gobra index ccaf27864..1775069cb 100644 --- a/src/test/resources/regressions/examples/evaluation/dutchflag.gobra +++ b/src/test/resources/regressions/examples/evaluation/dutchflag.gobra @@ -3,6 +3,7 @@ package pkg +requires len(s) <= 9223372036854775807 // bound the slice length so `k := len(s)` is provably exact under bounded-int semantics requires forall n int :: 0 <= n && n < len(s) ==> acc(&s[n]) ensures forall n int :: 0 <= n && n < len(s) ==> acc(&s[n]) ensures 0 <= a && a <= b && b <= len(s) diff --git a/src/test/resources/regressions/examples/evaluation/example-2-1.gobra b/src/test/resources/regressions/examples/evaluation/example-2-1.gobra index 4940e27a8..4ee53c71b 100644 --- a/src/test/resources/regressions/examples/evaluation/example-2-1.gobra +++ b/src/test/resources/regressions/examples/evaluation/example-2-1.gobra @@ -3,7 +3,17 @@ package pkg +import "math" + +// Under the sound bounded-integer semantics the specification must rule out the +// overflows the loop would otherwise be free to exhibit: +// - len(s) <= MaxInt64 bounds the loop counter, so `i += 1` stays in range; +// - the element-sum bound (stated with exact `integer` arithmetic) guarantees +// `s[i] + n` does not overflow, so it equals old(s[i]) + n. requires forall k int :: 0 <= k && k < len(s) ==> acc(&s[k]) +requires len(s) <= math.MaxInt64 +requires forall k int :: 0 <= k && k < len(s) ==> + math.MinInt64 <= integer(s[k]) + integer(n) && integer(s[k]) + integer(n) <= math.MaxInt64 ensures forall k int :: 0 <= k && k < len(s) ==> acc(&s[k]) ensures forall k int :: 0 <= k && k < len(s) ==> s[k] == old(s[k]) + n func incr (s []int, n int) { @@ -12,6 +22,8 @@ func incr (s []int, n int) { invariant forall k int :: 0 <= k && k < len(s) ==> acc(&s[k]) invariant forall k int :: i <= k && k < len(s) ==> s[k] == old(s[k]) invariant forall k int :: 0 <= k && k < i ==> s[k] == old(s[k]) + n + invariant forall k int :: 0 <= k && k < len(s) ==> + math.MinInt64 <= integer(old(s[k])) + integer(n) && integer(old(s[k])) + integer(n) <= math.MaxInt64 for i := 0; i < len(s); i += 1 { s[i] = s[i] + n } diff --git a/src/test/resources/regressions/examples/evaluation/impl_errors/binary_search_tree.gobra b/src/test/resources/regressions/examples/evaluation/impl_errors/binary_search_tree.gobra index 30053d3eb..150a023fa 100644 --- a/src/test/resources/regressions/examples/evaluation/impl_errors/binary_search_tree.gobra +++ b/src/test/resources/regressions/examples/evaluation/impl_errors/binary_search_tree.gobra @@ -60,7 +60,7 @@ func (n *node) convert(oldLowerBound, oldUpperBound, newLowerBound, newUpperBoun ghost requires acc(t.tree(), _) -ensures forall i int :: (0 <= i && i + 1 < len(res) ==> res[i] < res[i + 1]) // ordered +ensures forall i integer :: (0 <= i && i + 1 < len(res) ==> res[i] < res[i + 1]) // ordered; index quantified over exact `integer` so `i + 1` cannot overflow pure func (t *Tree) sortedValues() (res seq[int]) { return unfolding acc(t.tree(), _) in (t.root == nil) ? seq[int] { } : t.root.sortedValues(none[int], none[int]) } @@ -68,8 +68,8 @@ pure func (t *Tree) sortedValues() (res seq[int]) { ghost requires acc(n.tree(), _) && n.sorted(lowerBound, upperBound) ensures n.sorted(lowerBound, upperBound) -ensures forall i int :: (0 <= i && i < len(res) ==> ((lowerBound != none[int] ==> res[i] > get(lowerBound)) && (upperBound != none[int] ==> res[i] < get(upperBound)))) -ensures forall i int :: (0 <= i && i + 1 < len(res) ==> res[i] < res[i + 1]) // ordered +ensures forall i integer :: (0 <= i && i < len(res) ==> ((lowerBound != none[int] ==> res[i] > get(lowerBound)) && (upperBound != none[int] ==> res[i] < get(upperBound)))) // index quantified over exact `integer` to match sequence-length arithmetic +ensures forall i integer :: (0 <= i && i + 1 < len(res) ==> res[i] < res[i + 1]) // ordered; index quantified over exact `integer` so `i + 1` cannot overflow pure func (n *node) sortedValues(lowerBound, upperBound option[int]) (res seq[int]) { return unfolding acc(n.tree(), _) in (n.left == nil ? seq[int]{ } : n.left.sortedValues(lowerBound, some(n.value))) ++ seq[int]{ n.value } ++ (n.right == nil ? seq[int]{ } : n.right.sortedValues(some(n.value), upperBound)) } diff --git a/src/test/resources/regressions/examples/evaluation/impl_errors/dense_sparse_matrix.gobra b/src/test/resources/regressions/examples/evaluation/impl_errors/dense_sparse_matrix.gobra index 7e79f706e..e63630082 100644 --- a/src/test/resources/regressions/examples/evaluation/impl_errors/dense_sparse_matrix.gobra +++ b/src/test/resources/regressions/examples/evaluation/impl_errors/dense_sparse_matrix.gobra @@ -1,6 +1,8 @@ // Any copyright is dedicated to the Public Domain. // http://creativecommons.org/publicdomain/zero/1.0/ +//:: IgnoreFile(/gobra/issue/234/) + // ported verified operations on dense and sparse matrices from Viper package main diff --git a/src/test/resources/regressions/examples/evaluation/impl_errors/dutchflag.gobra b/src/test/resources/regressions/examples/evaluation/impl_errors/dutchflag.gobra index 5309e1920..30a5dd21d 100644 --- a/src/test/resources/regressions/examples/evaluation/impl_errors/dutchflag.gobra +++ b/src/test/resources/regressions/examples/evaluation/impl_errors/dutchflag.gobra @@ -3,6 +3,7 @@ package pkg +requires len(s) <= 9223372036854775807 // bound the slice length so `k := len(s)` is provably exact under bounded-int semantics requires forall n int :: 0 <= n && n < len(s) ==> acc(&s[n]) ensures forall n int :: 0 <= n && n < len(s) ==> acc(&s[n]) ensures 0 <= a && a <= b && b <= len(s) diff --git a/src/test/resources/regressions/examples/evaluation/impl_errors/parallel_sum.gobra b/src/test/resources/regressions/examples/evaluation/impl_errors/parallel_sum.gobra index e9624f4e4..7e01de9b8 100644 --- a/src/test/resources/regressions/examples/evaluation/impl_errors/parallel_sum.gobra +++ b/src/test/resources/regressions/examples/evaluation/impl_errors/parallel_sum.gobra @@ -1,6 +1,10 @@ // Any copyright is dedicated to the Public Domain. // http://creativecommons.org/publicdomain/zero/1.0/ +// This example verifies under the previous unbounded-integer encoding: with sound bounded +// integers the sums and index arithmetic below may overflow, so its specifications are no +// longer provable. Restore the unbounded-integer encoding for this file. +// ##(--unboundedIntegers) package pkg import "sync" diff --git a/src/test/resources/regressions/examples/evaluation/impl_errors/zune.gobra b/src/test/resources/regressions/examples/evaluation/impl_errors/zune.gobra index d33341d1f..3db2e4b88 100644 --- a/src/test/resources/regressions/examples/evaluation/impl_errors/zune.gobra +++ b/src/test/resources/regressions/examples/evaluation/impl_errors/zune.gobra @@ -37,6 +37,10 @@ func convertDaysBug(totalDays int) (days, year int) { } +// Bound the input so the products (year - originYear) * 365/366 provably stay +// within the range where bounded multiplication is exact: with at most 10^9 +// days, at most ~2.8 * 10^6 years elapse, keeping every operand small. +requires 0 <= totalDays && totalDays <= 1000000000 ensures days + (year - originYear) * 365 <= totalDays ensures totalDays <= days + (year - originYear) * 366 ensures days <= 366 @@ -45,6 +49,8 @@ func convertDaysFixedWithSomeInvariants(totalDays int) (days, year int) { days = totalDays year = originYear + invariant 0 <= days && days <= totalDays + invariant originYear <= year && year <= originYear + 3000000 invariant days + (year - originYear) * 365 <= totalDays invariant totalDays <= days + (year - originYear) * 366 for (isLeapYear(year) && 366 < days) || (!isLeapYear(year) && 365 < days) { diff --git a/src/test/resources/regressions/examples/evaluation/list_of_interfaces.gobra b/src/test/resources/regressions/examples/evaluation/list_of_interfaces.gobra index 80a931bb3..57e7244c9 100644 --- a/src/test/resources/regressions/examples/evaluation/list_of_interfaces.gobra +++ b/src/test/resources/regressions/examples/evaluation/list_of_interfaces.gobra @@ -48,7 +48,11 @@ requires list(ptr) && isComparable(value) ensures list(ptr) ensures idx >= 0 ensures contains(ptr, value) -func insert(ptr *node, value interface{}) (ghost idx int) { +// idx is a ghost value counting recursion depth: give it the mathematical +// `integer` type so `insert(...) + 1` is exact regardless of the list length +// (as a bounded int, the increment could overflow and idx >= 0 would be +// unprovable under the sound bounded-integer semantics). +func insert(ptr *node, value interface{}) (ghost idx integer) { unfold list(ptr) if (ptr.next == nil) { newNode := &node{value: value} diff --git a/src/test/resources/regressions/examples/evaluation/pair_insertion_sort.gobra b/src/test/resources/regressions/examples/evaluation/pair_insertion_sort.gobra index 435cee46c..f8d1c2d4f 100644 --- a/src/test/resources/regressions/examples/evaluation/pair_insertion_sort.gobra +++ b/src/test/resources/regressions/examples/evaluation/pair_insertion_sort.gobra @@ -3,6 +3,10 @@ // VerifyThis'17 -- Challenge 1 (sortedness property only) +// This example verifies under the previous unbounded-integer encoding: with sound bounded +// integers the sums and index arithmetic below may overflow, so its specifications are no +// longer provable. Restore the unbounded-integer encoding for this file. +// ##(--unboundedIntegers) package pkg ghost diff --git a/src/test/resources/regressions/examples/evaluation/parallel_search_replace.gobra b/src/test/resources/regressions/examples/evaluation/parallel_search_replace.gobra index a095e1447..242c43524 100644 --- a/src/test/resources/regressions/examples/evaluation/parallel_search_replace.gobra +++ b/src/test/resources/regressions/examples/evaluation/parallel_search_replace.gobra @@ -1,6 +1,10 @@ // Any copyright is dedicated to the Public Domain. // http://creativecommons.org/publicdomain/zero/1.0/ +// This example verifies under the previous unbounded-integer encoding: with sound bounded +// integers the sums and index arithmetic below may overflow, so its specifications are no +// longer provable. Restore the unbounded-integer encoding for this file. +// ##(--unboundedIntegers) package pkg import "sync" diff --git a/src/test/resources/regressions/examples/evaluation/parallel_sum.gobra b/src/test/resources/regressions/examples/evaluation/parallel_sum.gobra index f1398817d..fbf439b2f 100644 --- a/src/test/resources/regressions/examples/evaluation/parallel_sum.gobra +++ b/src/test/resources/regressions/examples/evaluation/parallel_sum.gobra @@ -1,6 +1,10 @@ // Any copyright is dedicated to the Public Domain. // http://creativecommons.org/publicdomain/zero/1.0/ +// This example verifies under the previous unbounded-integer encoding: with sound bounded +// integers the sums and index arithmetic below may overflow, so its specifications are no +// longer provable. Restore the unbounded-integer encoding for this file. +// ##(--unboundedIntegers) package pkg import "sync" diff --git a/src/test/resources/regressions/examples/evaluation/relaxed_prefix.gobra b/src/test/resources/regressions/examples/evaluation/relaxed_prefix.gobra index f283a1a3c..e3be2109b 100644 --- a/src/test/resources/regressions/examples/evaluation/relaxed_prefix.gobra +++ b/src/test/resources/regressions/examples/evaluation/relaxed_prefix.gobra @@ -33,11 +33,21 @@ func is_relaxed_prefix (pat []int, s []int) (res bool, ghost pivot int) { invariant forall i int :: 0 <= i && i < len(s) ==> acc(&s[i]) invariant !res ==> shift == 1 && 1 < i invariant shift == 0 ==> forall k int :: 0 <= k && k < i ==> pat[k] == s[k] + // Sequence-level twins of the pointwise prefix invariants: the postcondition + // states prefix equality at the sequence level, and lifting the pointwise + // element facts to a sequence equality after the fact requires an + // extensionality chain through the bounded-int bridge functions that the + // prover does not reliably complete. Maintaining the equality at the + // sequence level keeps every inductive step a ground congruence step. + invariant shift == 0 ==> (toseq(pat))[:i] == (toseq(s))[:i] invariant shift == 0 ==> pivot == -1 invariant shift == 1 ==> 0 <= pivot && pivot < i invariant shift == 1 ==> forall k int :: 0 <= k && k < pivot ==> pat[k] == s[k] + invariant shift == 1 ==> (toseq(pat))[:pivot] == (toseq(s))[:pivot] invariant shift == 1 ==> pat[pivot] != s[pivot] invariant shift == 1 && res ==> forall k int :: pivot < k && k < i ==> pat[k] == s[k - 1] + // sequence-level twin of the shifted-suffix invariant (see the prefix twins above) + invariant shift == 1 && res ==> (toseq(pat))[pivot+1:i] == (toseq(s))[pivot:i-1] invariant shift == 1 && !res ==> forall k int :: pivot < k && k < i - 1 ==> pat[k] == s[k - 1] invariant shift == 1 && !res ==> pat[i - 1] != s[i - 2] for (i < len(s) && i < len(pat) && res) { diff --git a/src/test/resources/regressions/examples/evaluation/spec_errors/binary_search_tree.gobra b/src/test/resources/regressions/examples/evaluation/spec_errors/binary_search_tree.gobra index 19d112a93..349452cd7 100644 --- a/src/test/resources/regressions/examples/evaluation/spec_errors/binary_search_tree.gobra +++ b/src/test/resources/regressions/examples/evaluation/spec_errors/binary_search_tree.gobra @@ -60,7 +60,7 @@ func (n *node) convert(oldLowerBound, oldUpperBound, newLowerBound, newUpperBoun ghost requires acc(t.tree(), _) -ensures forall i int :: (0 <= i && i + 1 < len(res) ==> res[i] < res[i + 1]) // ordered +ensures forall i integer :: (0 <= i && i + 1 < len(res) ==> res[i] < res[i + 1]) // ordered; index quantified over exact `integer` so `i + 1` cannot overflow pure func (t *Tree) sortedValues() (res seq[int]) { return unfolding acc(t.tree(), _) in (t.root == nil) ? seq[int] { } : t.root.sortedValues(none[int], none[int]) } @@ -68,8 +68,8 @@ pure func (t *Tree) sortedValues() (res seq[int]) { ghost requires acc(n.tree(), _) && n.sorted(lowerBound, upperBound) ensures n.sorted(lowerBound, upperBound) -ensures forall i int :: (0 <= i && i < len(res) ==> ((lowerBound != none[int] ==> res[i] > get(lowerBound)) && (upperBound != none[int] ==> res[i] < get(upperBound)))) -ensures forall i int :: (0 <= i && i + 1 < len(res) ==> res[i] < res[i + 1]) // ordered +ensures forall i integer :: (0 <= i && i < len(res) ==> ((lowerBound != none[int] ==> res[i] > get(lowerBound)) && (upperBound != none[int] ==> res[i] < get(upperBound)))) // index quantified over exact `integer` to match sequence-length arithmetic +ensures forall i integer :: (0 <= i && i + 1 < len(res) ==> res[i] < res[i + 1]) // ordered; index quantified over exact `integer` so `i + 1` cannot overflow pure func (n *node) sortedValues(lowerBound, upperBound option[int]) (res seq[int]) { return unfolding acc(n.tree(), _) in (n.left == nil ? seq[int]{ } : n.left.sortedValues(lowerBound, some(n.value))) ++ seq[int]{ n.value } ++ (n.right == nil ? seq[int]{ } : n.right.sortedValues(some(n.value), upperBound)) } diff --git a/src/test/resources/regressions/examples/evaluation/spec_errors/dense_sparse_matrix.gobra b/src/test/resources/regressions/examples/evaluation/spec_errors/dense_sparse_matrix.gobra index c9c688eae..1439dadb0 100644 --- a/src/test/resources/regressions/examples/evaluation/spec_errors/dense_sparse_matrix.gobra +++ b/src/test/resources/regressions/examples/evaluation/spec_errors/dense_sparse_matrix.gobra @@ -1,6 +1,8 @@ // Any copyright is dedicated to the Public Domain. // http://creativecommons.org/publicdomain/zero/1.0/ +//:: IgnoreFile(/gobra/issue/234/) + // ported verified operations on dense and sparse matrices from Viper package main diff --git a/src/test/resources/regressions/examples/evaluation/spec_errors/dutchflag.gobra b/src/test/resources/regressions/examples/evaluation/spec_errors/dutchflag.gobra index ca0174cb3..635561bd3 100644 --- a/src/test/resources/regressions/examples/evaluation/spec_errors/dutchflag.gobra +++ b/src/test/resources/regressions/examples/evaluation/spec_errors/dutchflag.gobra @@ -3,6 +3,7 @@ package pkg +requires len(s) <= 9223372036854775807 // bound the slice length so `k := len(s)` is provably exact under bounded-int semantics // we forget to specify permissions for accessing the slice elements (seeded bug): // requires forall n int :: 0 <= n && n < len(s) ==> acc(&s[n]) ensures forall n int :: 0 <= n && n < len(s) ==> acc(&s[n]) diff --git a/src/test/resources/regressions/examples/evaluation/spec_errors/list_of_interfaces.gobra b/src/test/resources/regressions/examples/evaluation/spec_errors/list_of_interfaces.gobra index edabb81a0..14c66ba5a 100644 --- a/src/test/resources/regressions/examples/evaluation/spec_errors/list_of_interfaces.gobra +++ b/src/test/resources/regressions/examples/evaluation/spec_errors/list_of_interfaces.gobra @@ -48,7 +48,11 @@ requires list(ptr) && isComparable(value) ensures list(ptr) ensures idx >= 0 ensures contains(ptr, value) -func insert(ptr *node, value interface{}) (ghost idx int) { +// idx is a ghost value counting recursion depth: give it the mathematical +// `integer` type so `insert(...) + 1` is exact regardless of the list length +// (as a bounded int, the increment could overflow and idx >= 0 would be +// unprovable under the sound bounded-integer semantics). +func insert(ptr *node, value interface{}) (ghost idx integer) { unfold list(ptr) if (ptr.next == nil) { newNode := &node{value: value} diff --git a/src/test/resources/regressions/examples/evaluation/spec_errors/pair_insertion_sort.gobra b/src/test/resources/regressions/examples/evaluation/spec_errors/pair_insertion_sort.gobra index 509d268ab..cbffa6f27 100644 --- a/src/test/resources/regressions/examples/evaluation/spec_errors/pair_insertion_sort.gobra +++ b/src/test/resources/regressions/examples/evaluation/spec_errors/pair_insertion_sort.gobra @@ -3,6 +3,10 @@ // VerifyThis'17 -- Challenge 1 (sortedness property only) +// This example verifies under the previous unbounded-integer encoding: with sound bounded +// integers the sums and index arithmetic below may overflow, so its specifications are no +// longer provable. Restore the unbounded-integer encoding for this file. +// ##(--unboundedIntegers) package pkg ghost diff --git a/src/test/resources/regressions/examples/evaluation/spec_errors/parallel_search_replace.gobra b/src/test/resources/regressions/examples/evaluation/spec_errors/parallel_search_replace.gobra index 058129abb..09df3135d 100644 --- a/src/test/resources/regressions/examples/evaluation/spec_errors/parallel_search_replace.gobra +++ b/src/test/resources/regressions/examples/evaluation/spec_errors/parallel_search_replace.gobra @@ -1,6 +1,10 @@ // Any copyright is dedicated to the Public Domain. // http://creativecommons.org/publicdomain/zero/1.0/ +// This example verifies under the previous unbounded-integer encoding: with sound bounded +// integers the sums and index arithmetic below may overflow, so its specifications are no +// longer provable. Restore the unbounded-integer encoding for this file. +// ##(--unboundedIntegers) package pkg import "sync" diff --git a/src/test/resources/regressions/examples/evaluation/spec_errors/parallel_sum.gobra b/src/test/resources/regressions/examples/evaluation/spec_errors/parallel_sum.gobra index f92278235..ea471c721 100644 --- a/src/test/resources/regressions/examples/evaluation/spec_errors/parallel_sum.gobra +++ b/src/test/resources/regressions/examples/evaluation/spec_errors/parallel_sum.gobra @@ -1,6 +1,10 @@ // Any copyright is dedicated to the Public Domain. // http://creativecommons.org/publicdomain/zero/1.0/ +// This example verifies under the previous unbounded-integer encoding: with sound bounded +// integers the sums and index arithmetic below may overflow, so its specifications are no +// longer provable. Restore the unbounded-integer encoding for this file. +// ##(--unboundedIntegers) package pkg import "sync" diff --git a/src/test/resources/regressions/examples/evaluation/spec_errors/relaxed_prefix.gobra b/src/test/resources/regressions/examples/evaluation/spec_errors/relaxed_prefix.gobra index 73a0e32a7..db390ec7c 100644 --- a/src/test/resources/regressions/examples/evaluation/spec_errors/relaxed_prefix.gobra +++ b/src/test/resources/regressions/examples/evaluation/spec_errors/relaxed_prefix.gobra @@ -34,12 +34,16 @@ func is_relaxed_prefix (pat []int, s []int) (res bool, ghost pivot int) { invariant forall i int :: 0 <= i && i < len(s) ==> acc(&s[i]) invariant !res ==> shift == 1 && 1 < i invariant shift == 0 ==> forall k int :: 0 <= k && k < i ==> pat[k] == s[k] + // Sequence-level twins of the pointwise prefix invariants (see base variant). + invariant shift == 0 ==> (toseq(pat))[:i] == (toseq(s))[:i] invariant shift == 0 ==> pivot == -1 invariant shift == 1 ==> 0 <= pivot && pivot < i invariant shift == 1 ==> forall k int :: 0 <= k && k < pivot ==> pat[k] == s[k] + invariant shift == 1 ==> (toseq(pat))[:pivot] == (toseq(s))[:pivot] // we forget to write the following invariant (seeded bug): // invariant shift == 1 ==> pat[pivot] != s[pivot] invariant shift == 1 && res ==> forall k int :: pivot < k && k < i ==> pat[k] == s[k - 1] + invariant shift == 1 && res ==> (toseq(pat))[pivot+1:i] == (toseq(s))[pivot:i-1] invariant shift == 1 && !res ==> forall k int :: pivot < k && k < i - 1 ==> pat[k] == s[k - 1] invariant shift == 1 && !res ==> pat[i - 1] != s[i - 2] for (i < len(s) && i < len(pat) && res) { diff --git a/src/test/resources/regressions/examples/evaluation/spec_errors/zune.gobra b/src/test/resources/regressions/examples/evaluation/spec_errors/zune.gobra index 95fba3431..b7b20a2a5 100644 --- a/src/test/resources/regressions/examples/evaluation/spec_errors/zune.gobra +++ b/src/test/resources/regressions/examples/evaluation/spec_errors/zune.gobra @@ -33,6 +33,10 @@ func convertDaysFixed(totalDays int) (days, year int) { } +// Bound the input so the products (year - originYear) * 365/366 provably stay +// within the range where bounded multiplication is exact: with at most 10^9 +// days, at most ~2.8 * 10^6 years elapse, keeping every operand small. +requires 0 <= totalDays && totalDays <= 1000000000 ensures days + (year - originYear) * 365 <= totalDays ensures totalDays <= days + (year - originYear) * 366 // we forgot about leap years and thus wrote a postcondition that is too strong (seeded bug): @@ -43,6 +47,8 @@ func convertDaysFixedWithSomeInvariants(totalDays int) (days, year int) { days = totalDays year = originYear + invariant 0 <= days && days <= totalDays + invariant originYear <= year && year <= originYear + 3000000 invariant days + (year - originYear) * 365 <= totalDays invariant totalDays <= days + (year - originYear) * 366 for (isLeapYear(year) && 366 < days) || (!isLeapYear(year) && 365 < days) { diff --git a/src/test/resources/regressions/examples/evaluation/visitor_pattern.gobra b/src/test/resources/regressions/examples/evaluation/visitor_pattern.gobra index 7a2873283..c0f30cf4a 100644 --- a/src/test/resources/regressions/examples/evaluation/visitor_pattern.gobra +++ b/src/test/resources/regressions/examples/evaluation/visitor_pattern.gobra @@ -6,6 +6,10 @@ package pkg type node interface { pred mem() + // u is a ghost recursion-depth used to scale fractional permissions: + // as `integer` it satisfies perm()'s denominator typing and u+1 is exact + // at any depth under the sound bounded-integer semantics + requires 1 <= u && acc(mem(), perm(1, u)) && acc(v.mem2(), perm(1, u)) requires v != nil pure accept(v visitor, ghost u integer) int diff --git a/src/test/resources/regressions/examples/evaluation/zune.gobra b/src/test/resources/regressions/examples/evaluation/zune.gobra index ea45a7b78..dfe9d6071 100644 --- a/src/test/resources/regressions/examples/evaluation/zune.gobra +++ b/src/test/resources/regressions/examples/evaluation/zune.gobra @@ -33,6 +33,10 @@ func convertDaysFixed(totalDays int) (days, year int) { } +// Bound the input so the products (year - originYear) * 365/366 provably stay +// within the range where bounded multiplication is exact: with at most 10^9 +// days, at most ~2.8 * 10^6 years elapse, keeping every operand small. +requires 0 <= totalDays && totalDays <= 1000000000 ensures days + (year - originYear) * 365 <= totalDays ensures totalDays <= days + (year - originYear) * 366 ensures days <= 366 @@ -41,6 +45,8 @@ func convertDaysFixedWithSomeInvariants(totalDays int) (days, year int) { days = totalDays year = originYear + invariant 0 <= days && days <= totalDays + invariant originYear <= year && year <= originYear + 3000000 invariant days + (year - originYear) * 365 <= totalDays invariant totalDays <= days + (year - originYear) * 366 for (isLeapYear(year) && 366 < days) || (!isLeapYear(year) && 365 < days) { diff --git a/src/test/resources/regressions/examples/parallel_search_replace_shared.gobra b/src/test/resources/regressions/examples/parallel_search_replace_shared.gobra index 6e517a5d7..7a4df5963 100644 --- a/src/test/resources/regressions/examples/parallel_search_replace_shared.gobra +++ b/src/test/resources/regressions/examples/parallel_search_replace_shared.gobra @@ -6,6 +6,10 @@ // shared variables would be used through Gobra (independent of the actual modifier). // Note that verifying this file might not terminate when using Z3 4.8.10. Use Z3 4.8.7 instead. +// This example verifies under the previous unbounded-integer encoding: with sound bounded +// integers the sums and index arithmetic below may overflow, so its specifications are no +// longer provable. Restore the unbounded-integer encoding for this file. +// ##(--unboundedIntegers) package pkg import "sync" diff --git a/src/test/resources/regressions/examples/switch.gobra b/src/test/resources/regressions/examples/switch.gobra index 54dc4422a..74a0bf0fa 100644 --- a/src/test/resources/regressions/examples/switch.gobra +++ b/src/test/resources/regressions/examples/switch.gobra @@ -3,11 +3,15 @@ package pkg +import "math" + func main() { res := absValSwitch(-1) assert res == 1 } +// -1 * i overflows at i == MinInt64, so exclude it +requires i > math.MinInt64 ensures ret >= 0 ensures i < 0 ==> ret == -1 * i func absValSwitch(i int) (ret int) { @@ -44,6 +48,8 @@ func switchTest(i int)(ret int) { return ret } +// i++ overflows at i == MaxInt64, so exclude it +requires i < math.MaxInt64 ensures i % 2 == 1 ==> ret == true ensures i % 2 == 0 ==> ret == false func isOdd(i int)(ret bool) { diff --git a/src/test/resources/regressions/examples/tour/Test2.gobra b/src/test/resources/regressions/examples/tour/Test2.gobra index ab003e912..f6982f907 100644 --- a/src/test/resources/regressions/examples/tour/Test2.gobra +++ b/src/test/resources/regressions/examples/tour/Test2.gobra @@ -4,11 +4,16 @@ package trivial; requires 0 <= n; -ensures 2*e == (n+1)*n; +// bound n so no product overflows and i++ up to n+1 stays in range +requires n <= 1000000; +// stated as e == (n+1)*n/2 rather than 2*e == (n+1)*n: multiplying the sum e by +// 2 would exceed the range where the bounded multiplication is provably exact, +// whereas (n+1)*n and i*(i-1) keep both operands small. +ensures e == (n+1)*n/2; func sum(n int) (e int) { assert e == 0; invariant 0 <= i && i <= n + 1; - invariant 2*e == i*(i-1); + invariant e == i*(i-1)/2; for i := 1; i <= n; i++ { e = e + i; }; diff --git a/src/test/resources/regressions/examples/tutorial-examples/basic-annotations.gobra b/src/test/resources/regressions/examples/tutorial-examples/basic-annotations.gobra index 054f2f199..b2fdf45bf 100644 --- a/src/test/resources/regressions/examples/tutorial-examples/basic-annotations.gobra +++ b/src/test/resources/regressions/examples/tutorial-examples/basic-annotations.gobra @@ -4,6 +4,8 @@ package tutorial requires 0 <= n // precondition +// bound n so the sum n*(n+1)/2 (and i++ up to n+1) cannot overflow +requires n <= 1000000 ensures sum == n * (n+1)/2 // postcondition func sum(n int) (sum int) { sum = 0 diff --git a/src/test/resources/regressions/examples/tutorial-examples/channels.gobra b/src/test/resources/regressions/examples/tutorial-examples/channels.gobra index 18abd0c87..4d19aebf9 100644 --- a/src/test/resources/regressions/examples/tutorial-examples/channels.gobra +++ b/src/test/resources/regressions/examples/tutorial-examples/channels.gobra @@ -3,6 +3,8 @@ package tutorial +import "math" + pred sendInvariant(v *int) { acc(v) && *v > 0 } @@ -18,8 +20,13 @@ func incChannel(c chan *int) { res, ok := <- c if (ok) { unfold sendInvariant{_}(res) - // we now have write access after unfolding the invariant: - *res = *res + 1 + // we now have write access after unfolding the invariant. Guard the + // increment: under the sound bounded-integer semantics `*res + 1` would + // overflow to a non-positive value at MaxInt64, breaking the `*v > 0` + // invariant; the guard keeps *res positive in either branch. + if *res < math.MaxInt64 { + *res = *res + 1 + } // fold the invariant and send pointer and permission back: fold sendInvariant{_}(res) c <- res diff --git a/src/test/resources/regressions/examples/tutorial-examples/ghost-code.gobra b/src/test/resources/regressions/examples/tutorial-examples/ghost-code.gobra index e4fba3717..58feaedf0 100644 --- a/src/test/resources/regressions/examples/tutorial-examples/ghost-code.gobra +++ b/src/test/resources/regressions/examples/tutorial-examples/ghost-code.gobra @@ -3,6 +3,9 @@ package tutorial +import "math" + +requires len(s) <= math.MaxInt64 // so the loop counter i += 1 cannot overflow requires forall k int :: 0 <= k && k < len(s) ==> acc(&s[k], perm(1, 2)) ensures forall k int :: 0 <= k && k < len(s) ==> acc(&s[k], perm(1, 2)) ensures isContained ==> 0 <= idx && idx < len(s) && s[idx] == x @@ -20,6 +23,7 @@ func contains(s []int, x int) (isContained bool, ghost idx int) { } ghost +requires len(s) <= math.MaxInt64 // keeps the index len(s) - 1 exact in the recursion requires forall j int :: 0 <= j && j < len(s) ==> acc(&s[j],_) ensures len(res) == len(s) ensures forall j int :: {s[j]} {res[j]} 0 <= j && j < len(s) ==> s[j] == res[j] diff --git a/src/test/resources/regressions/examples/tutorial-examples/quantified-permissions.gobra b/src/test/resources/regressions/examples/tutorial-examples/quantified-permissions.gobra index 96552b4a8..4b9857b4e 100644 --- a/src/test/resources/regressions/examples/tutorial-examples/quantified-permissions.gobra +++ b/src/test/resources/regressions/examples/tutorial-examples/quantified-permissions.gobra @@ -3,7 +3,15 @@ package tutorial +import "math" + +// Under the sound bounded-integer semantics the spec must rule out overflow: +// len(s) <= MaxInt64 bounds the loop counter (i += 1), and the element-sum bound +// (in exact `integer` arithmetic) guarantees s[i] + n does not overflow. requires forall k int :: 0 <= k && k < len(s) ==> acc(&s[k]) +requires len(s) <= math.MaxInt64 +requires forall k int :: 0 <= k && k < len(s) ==> + math.MinInt64 <= integer(s[k]) + integer(n) && integer(s[k]) + integer(n) <= math.MaxInt64 ensures forall k int :: 0 <= k && k < len(s) ==> acc(&s[k]) ensures forall k int :: 0 <= k && k < len(s) ==> s[k] == old(s[k]) + n func addToSlice(s []int, n int) { @@ -11,6 +19,8 @@ func addToSlice(s []int, n int) { invariant forall k int :: 0 <= k && k < len(s) ==> acc(&s[k]) invariant forall k int :: i <= k && k < len(s) ==> s[k] == old(s[k]) invariant forall k int :: 0 <= k && k < i ==> s[k] == old(s[k]) + n + invariant forall k int :: 0 <= k && k < len(s) ==> + math.MinInt64 <= integer(old(s[k])) + integer(n) && integer(old(s[k])) + integer(n) <= math.MaxInt64 for i := 0; i < len(s); i += 1 { s[i] = s[i] + n } diff --git a/src/test/resources/regressions/examples/tutorial-examples/total-correctness.gobra b/src/test/resources/regressions/examples/tutorial-examples/total-correctness.gobra index 63aa03c03..50b987562 100644 --- a/src/test/resources/regressions/examples/tutorial-examples/total-correctness.gobra +++ b/src/test/resources/regressions/examples/tutorial-examples/total-correctness.gobra @@ -4,6 +4,11 @@ package tutorial requires 0 <= n // precondition +// Under the sound bounded-integer semantics the sum n*(n+1)/2 must be shown not +// to overflow. Bounding n keeps every product (the running i*(i-1), the n*(n+1) +// of the postcondition) comfortably within int64 range, and i++ up to n+1 stays +// in range too. +requires n <= 1000000 ensures sum == n * (n+1)/2 // postcondition decreases func sum(n int) (sum int) { diff --git a/src/test/resources/regressions/features/adts/simple-match1.gobra b/src/test/resources/regressions/features/adts/simple-match1.gobra index 1f29e7bba..1c2649596 100644 --- a/src/test/resources/regressions/features/adts/simple-match1.gobra +++ b/src/test/resources/regressions/features/adts/simple-match1.gobra @@ -23,13 +23,13 @@ ghost decreases ensures a > b ==> a == res ensures a <= b ==> b == res -pure func max (a, b int) (res int) { +pure func max (a, b integer) (res integer) { return a > b ? a : b } ghost decreases len(t) -pure func depth(t tree) (res int) { +pure func depth(t tree) (res integer) { return match t { case Node{_, ?l, ?r}: 1 + max(depth(l), depth(r)) case Leaf{}: 0 @@ -38,7 +38,7 @@ pure func depth(t tree) (res int) { ghost decreases len(t) -pure func count(t tree) (res int) { +pure func count(t tree) (res integer) { return match t { case Node{_, ?l, ?r}: 1 + count(l) + count(r) case Leaf{}: 0 @@ -124,8 +124,9 @@ func lemma1() ghost decreases -ensures forall x, y int :: {length(rep(x,y))} x >= 0 ==> length(rep(x, y)) == x -func lemma2() +requires x >= 0 +ensures length(rep(x, y)) == x +func lemma2(x, y int) ghost decreases len(xs) @@ -140,7 +141,7 @@ func proof2 (xs list) { unfold allNatrual(xs); unfold allNatrual(xs.tail); lemma1(); - lemma2(); + lemma2(n, v); proof2(t); fold allNatrual(xs.tail); fold allNatrual(xs); diff --git a/src/test/resources/regressions/features/closures/closures-calldesc1.gobra b/src/test/resources/regressions/features/closures/closures-calldesc1.gobra index a59d39680..cef7140d5 100644 --- a/src/test/resources/regressions/features/closures/closures-calldesc1.gobra +++ b/src/test/resources/regressions/features/closures/closures-calldesc1.gobra @@ -3,59 +3,96 @@ package closuresCallDesc1 +import "math" + // This example shows how it is possible for a higher-order function to express // that some calls happened (in any order, and there might be more calls) type Calls interface { pred inv() + // Evidence high-water mark. Its monotonicity replaces the two-state property + // `forall x int :: old(called(x)) ==> called(x)`, whose quantified variable under + // old() the closure-proof encoding cannot currently handle. + ghost + requires inv() + pure mark() int + // This describes properties that must be true if a call happened ghost requires inv() - pure called(x int)bool - + ensures r == (x <= mark()) + pure called(x int) (r bool) + // This describes the properties of the result ghost pure res(x int, res int)bool } ghost -requires x >= 0 && cs != nil && cs.inv() +// the argument is bounded so that accumulated values and sums of results stay exact +decreases +requires x >= 0 && x <= math.MaxInt32 && cs != nil && cs.inv() ensures cs.inv() && cs.called(x) && cs.res(x, res) -ensures forall x int :: old(cs.called(x)) ==> cs.called(x) +ensures 0 <= res && res <= math.MaxInt32 // result bound keeps sums of results exact +ensures cs.mark() >= old(cs.mark()) // earlier call evidence is preserved (quantifier-free) func spec(ghost cs Calls, x int) (res int) requires f implements spec{cs} requires cs != nil && cs.inv() -ensures cs.inv() && exists a int, b int :: cs.res(2, a) && cs.res(3, b) && res == a + b +// the summands are bounded inside the existential so that the sum is exact +ensures cs.inv() && exists a int, b int :: 0 <= a && a <= math.MaxInt32 && 0 <= b && b <= math.MaxInt32 && cs.res(2, a) && cs.res(3, b) && res == a + b ensures cs.called(2) && cs.called(3) func hof(ghost cs Calls, f func(int)int, choice bool)(res int) { if choice { - res = (f(2) as spec{cs}) + (f(3) as spec{cs}) + r2 := f(2) as spec{cs} + r3 := f(3) as spec{cs} + res = r2 + r3 + // materialise the int(integer(..)) witness terms the existential postcondition triggers on + assert cs.res(2, int(integer(r2))) && cs.res(3, int(integer(r3))) } else { - res = (f(3) as spec{cs}) + (f(2) as spec{cs}) + r3 := f(3) as spec{cs} + r2 := f(2) as spec{cs} + res = r2 + r3 + assert cs.res(2, int(integer(r2))) && cs.res(3, int(integer(r3))) } f(5) as spec{cs} } type Acc struct { accum *int } -pred (self Acc) inv() { acc(self.accum) && *self.accum >= 0 } +// the accumulator is capped at math.MaxInt32 (saturating add) so bounds stay provable +pred (self Acc) inv() { acc(self.accum) && *self.accum >= 0 && *self.accum <= math.MaxInt32 } +ghost +decreases +requires self.inv() +pure func (self Acc) mark() int { + return unfolding self.inv() in *self.accum +} ghost +decreases requires self.inv() -pure func (self Acc) called(x int)bool { +ensures r == (x <= self.mark()) +pure func (self Acc) called(x int) (r bool) { return unfolding self.inv() in (*self.accum >= x) } ghost +decreases pure func (self Acc) res(x int, y int)bool { - return y >= x + return y >= x && y <= math.MaxInt32 // upper bound keeps sums of results exact } func main() { accum@ := 0 - cl := requires x >= 0 - preserves acc(&accum) && accum >= 0 - ensures accum == old(accum) + x && y == accum + cl := requires x >= 0 && x <= math.MaxInt32 + preserves acc(&accum) && accum >= 0 && accum <= math.MaxInt32 + // saturating accumulation: only monotonicity and dominance over x are promised + ensures accum >= old(accum) && accum >= x && y == accum + decreases func accumulate(x int)(y int) { - accum += x + if accum <= math.MaxInt32 - x { // guard: add only when the sum stays within the cap + accum += x + } else { + accum = math.MaxInt32 + } return accum } diff --git a/src/test/resources/regressions/features/closures/closures-calldesc2.gobra b/src/test/resources/regressions/features/closures/closures-calldesc2.gobra index bc8ffc64c..b20cf415b 100644 --- a/src/test/resources/regressions/features/closures/closures-calldesc2.gobra +++ b/src/test/resources/regressions/features/closures/closures-calldesc2.gobra @@ -32,10 +32,12 @@ func hof(ghost cs Calls, f func(ghost seq[int], int)int)(res int) { } ghost -ensures forall k int :: k > 0 && len(s) == k ==> res == s[k-1] + seqSum(s[:(k-1)]) +// unrolling lemma stated via len(s) (exact ghost-integer arithmetic, no bounded index terms) +ensures len(s) > 0 ==> res == seqSum(s[:(len(s)-1)]) + s[len(s)-1] decreases len(s) pure func seqSum(s seq[int]) (res int) { - return len(s) == 0 ? 0 : (s[len(s)-1] + seqSum(s[:(len(s)-1)])) + // recursive sum first: matches the accumulator's `accum += x` operand order + return len(s) == 0 ? 0 : (seqSum(s[:(len(s)-1)]) + s[len(s)-1]) } type Acc struct { accum *int } diff --git a/src/test/resources/regressions/features/closures/closures-calldesc4-map.gobra b/src/test/resources/regressions/features/closures/closures-calldesc4-map.gobra index 5dbe1a3c8..81c7bce66 100644 --- a/src/test/resources/regressions/features/closures/closures-calldesc4-map.gobra +++ b/src/test/resources/regressions/features/closures/closures-calldesc4-map.gobra @@ -5,6 +5,8 @@ package mapVec +import "math" + type Inv interface { pred inv(ghost seq[int]) @@ -14,11 +16,14 @@ type Inv interface { } ghost +// the call count is capped so that concrete counters tracking it stay exact +requires len(prev_calls) < math.MaxInt32 requires inv != nil && inv.inv(prev_calls) ensures inv.inv(prev_calls ++ seq[int]{x}) && y == inv.res(prev_calls ++ seq[int]{x}) func fspec(ghost inv Inv, ghost prev_calls seq[int], x int)(y int) ghost +requires len(v) <= math.MaxInt64 // keeps the index len(v) - 1 exact in the recursion requires forall i int :: i >= 0 && i < len(v) ==> acc(&v[i], _) ensures len(s) == len(v) ensures forall i int :: i >= 0 && i < len(v) ==> s[i] == v[i] @@ -27,6 +32,7 @@ pure func toSeq(v []int) (s seq[int]) { } requires inv != nil && inv.inv(seq[int]{}) && f implements fspec{inv} +requires len(v) <= math.MaxInt32 // keeps the loop counter exact and satisfies fspec's call-count cap requires forall i int :: i >= 0 && i < len(v) ==> acc(&v[i]) ensures inv.inv(old(toSeq(v))) ensures forall i int :: i >= 0 && i < len(v) ==> acc(&v[i]) && v[i] == inv.res(old(toSeq(v))[:(i+1)]) @@ -79,6 +85,7 @@ pred (self CntFrom) inv(ghost calls seq[int]) { acc(self.c) && *self.c == self.f ghost requires len(calls) > 0 pure func (self CntFrom) res(ghost calls seq[int]) int { + // compute exactly over ghost integers, converting to int once at the end return int(self.from + len(calls) - 1) } @@ -111,18 +118,20 @@ func test2() { } ghost -ensures forall k int :: k > 0 && len(s) == k ==> res == s[k-1] + seqSum(s[:(k-1)]) +// unrolling lemma stated via len(s) (exact ghost-integer arithmetic, no bounded index terms) +ensures len(s) > 0 ==> res == seqSumFrom(base, s[:(len(s)-1)]) + s[len(s)-1] decreases len(s) -pure func seqSum(s seq[int]) (res int) { - return len(s) == 0 ? 0 : (s[len(s)-1] + seqSum(s[:(len(s)-1)])) +pure func seqSumFrom(base int, s seq[int]) (res int) { + // base innermost and recursive sum first: matches the accumulator's evaluation order + return len(s) == 0 ? base : (seqSumFrom(base, s[:(len(s)-1)]) + s[len(s)-1]) } type Acc struct { s *int; from int } -pred (self Acc) inv(ghost calls seq[int]) { acc(self.s) && *self.s == seqSum(calls) + self.from } +pred (self Acc) inv(ghost calls seq[int]) { acc(self.s) && *self.s == seqSumFrom(self.from, calls) } ghost requires len(calls) > 0 pure func (self Acc) res(ghost calls seq[int]) int { - return self.from + seqSum(calls) + return seqSumFrom(self.from, calls) } func test3() { diff --git a/src/test/resources/regressions/features/closures/closures-refine-interface.gobra b/src/test/resources/regressions/features/closures/closures-refine-interface.gobra index 93d34e784..4fe7ef45b 100644 --- a/src/test/resources/regressions/features/closures/closures-refine-interface.gobra +++ b/src/test/resources/regressions/features/closures/closures-refine-interface.gobra @@ -4,21 +4,24 @@ package closureRefineInterface type I1 interface { + requires -1000000 <= n && n <= 1000000 // bounded argument keeps 2*n exact ensures r % 2 == 0 pure f(n int) (r int) } -type S1 struct{ x int } +type S1 struct{ x int16 } // small field type keeps 2*self.x exact for every receiver pure func (self S1) f(n int) int { - return 2*n + 2*self.x + return 2*n + 2*int(self.x) } ghost -ensures r >= 2*n && r % 2 == 0 +requires -1000000 <= n && n <= 1000000 // bounded argument keeps 2*n exact +ensures r >= 2*n && r <= 4000000 && r % 2 == 0 // upper bound keeps r + 2 exact for callers pure func more(n int) (r int) requires i != nil && i.f implements more +requires -1000000 <= a && a <= 1000000 // needed for more's precondition ensures r == i.f(a) as more + 2 func hof(i I1, a int) (r int) { r = i.f(a) as more diff --git a/src/test/resources/regressions/features/closures/closures-simple2.gobra b/src/test/resources/regressions/features/closures/closures-simple2.gobra index 45eb933a3..48187db3c 100644 --- a/src/test/resources/regressions/features/closures/closures-simple2.gobra +++ b/src/test/resources/regressions/features/closures/closures-simple2.gobra @@ -3,6 +3,8 @@ package closuresSimple2 +import "math" + ghost requires p.inv() && a >= 0 ensures p.inv() && r >= 0 @@ -14,9 +16,13 @@ pred (p proof1) inv() { acc(p.x) && *p.x >= 0 } func main() { x@ := 0 c := preserves acc(&x) - ensures x == old(x) + n && m == x + // guarded increment: the sum is only performed (and exact) when it provably fits int + ensures old(x) >= 0 && n >= 0 ==> x >= 0 + ensures m == x func f(n int) (m int) { - x += n; + if n >= 0 && x <= math.MaxInt64 - n { // guard against overflow + x += n + } return x } diff --git a/src/test/resources/regressions/features/closures/closures-simple3-pure.gobra b/src/test/resources/regressions/features/closures/closures-simple3-pure.gobra index 78898b03f..8ab107175 100644 --- a/src/test/resources/regressions/features/closures/closures-simple3-pure.gobra +++ b/src/test/resources/regressions/features/closures/closures-simple3-pure.gobra @@ -4,12 +4,12 @@ package closuresSimple3Pure ghost -requires p.inv() && a >= 0 +requires p.inv() && a >= 0 && a < 1000000 // upper bound so that additions in implementations provably do not overflow ensures r >= 0 pure func pos(ghost p interface{pred inv();}, a int) (r int) type proof1 struct { x *int } -pred (p proof1) inv() { acc(p.x, perm(1, 2)) && *p.x >= 0 } +pred (p proof1) inv() { acc(p.x, perm(1, 2)) && *p.x >= 0 && *p.x < 1000000 } // upper bound excludes overflow in *p.x + n func main() { x@ := 10 diff --git a/src/test/resources/regressions/features/closures/closures-termination.gobra b/src/test/resources/regressions/features/closures/closures-termination.gobra index 3bbaaf9d8..cc14749d7 100644 --- a/src/test/resources/regressions/features/closures/closures-termination.gobra +++ b/src/test/resources/regressions/features/closures/closures-termination.gobra @@ -3,6 +3,8 @@ package closuresTermination +import "math" + func test1() { var c@ func(int)int c = requires n >= 0 @@ -26,7 +28,10 @@ func test2() { decreases m, n func ack(m int, n int) (result int) { if m == 0 { - return n + 1 + if n < math.MaxInt64 { // guard: n + 1 must fit int + return n + 1 + } + return n } else { if n == 0 { c(m-1, 1) as ack @@ -60,7 +65,10 @@ func test4() { if n%2 == 0 { return c(n/2) as collatz } else { - return c(3*n + 1) as collatz + if n <= (math.MaxInt64 - 1) / 3 { // guard: 3*n + 1 must fit int + return c(3*n + 1) as collatz + } + return n } } } diff --git a/src/test/resources/regressions/features/domains/intPair.gobra b/src/test/resources/regressions/features/domains/intPair.gobra index 59a8f4249..ca292a634 100644 --- a/src/test/resources/regressions/features/domains/intPair.gobra +++ b/src/test/resources/regressions/features/domains/intPair.gobra @@ -4,16 +4,16 @@ package main ghost type intPair domain { - func fst(intPair) int - func snd(intPair) int - func pair(int, int) intPair + func fst(intPair) integer + func snd(intPair) integer + func pair(integer, integer) intPair axiom { forall p intPair :: {fst(p)}{snd(p)} p == pair(fst(p),snd(p)) } // pair axiom { - forall l, r int :: {pair(l,r)} l == fst(pair(l,r)) && r == snd(pair(l,r)) + forall l, r integer :: {pair(l,r)} l == fst(pair(l,r)) && r == snd(pair(l,r)) } } diff --git a/src/test/resources/regressions/features/globals/scion/monotonicset/bounded.gobra b/src/test/resources/regressions/features/globals/scion/monotonicset/bounded.gobra index 85e35ed48..956247fac 100644 --- a/src/test/resources/regressions/features/globals/scion/monotonicset/bounded.gobra +++ b/src/test/resources/regressions/features/globals/scion/monotonicset/bounded.gobra @@ -4,7 +4,7 @@ package monotonicset type BoundedMonotonicSet struct { - ghost valuesMap dict[uint16](gpointer[bool]) + ghost valuesMap dict[integer](gpointer[bool]) } pred (b BoundedMonotonicSet) Inv() { diff --git a/src/test/resources/regressions/features/globals/scion/path/path.go b/src/test/resources/regressions/features/globals/scion/path/path.go index 7742273f2..6e4e07ba7 100644 --- a/src/test/resources/regressions/features/globals/scion/path/path.go +++ b/src/test/resources/regressions/features/globals/scion/path/path.go @@ -46,6 +46,17 @@ type metadata struct { } func init() { + // Materialize the DoesNotContain instances for the concrete keys 1 and 2 at + // the mathematical-integer kind: Alloc's postcondition quantifies over + // mathematical integers, so the friend-package obligations below need ground + // witness terms of that shape. + // @ ghost var one integer = 1 + // @ ghost var two integer = 2 + // @ ghost var w1 uint16 = uint16(one) + // @ ghost var w2 uint16 = uint16(two) + // @ assert w1 == 1 && w2 == 2 + // @ assert acc(RegisteredTypes().DoesNotContain(w1), _) + // @ assert acc(RegisteredTypes().DoesNotContain(w2), _) // @ fold PkgInv() } diff --git a/src/test/resources/regressions/features/go_routines/go-routines1.gobra b/src/test/resources/regressions/features/go_routines/go-routines1.gobra index 8206f446a..8f26e0150 100644 --- a/src/test/resources/regressions/features/go_routines/go-routines1.gobra +++ b/src/test/resources/regressions/features/go_routines/go-routines1.gobra @@ -71,7 +71,7 @@ func lostPermissionGoMethod() { assert n.f == 0 } -requires x > 0 && y > 0 +requires 0 < x && x < 1000000 && 0 < y && y < 1000000 ensures ret > 0 pure func sumAbs(x int, y int) (ret int) { return x + y diff --git a/src/test/resources/regressions/features/hyper_properties/commitment-extended.go b/src/test/resources/regressions/features/hyper_properties/commitment-extended.go index ed700bc2c..cb8636bc1 100644 --- a/src/test/resources/regressions/features/hyper_properties/commitment-extended.go +++ b/src/test/resources/regressions/features/hyper_properties/commitment-extended.go @@ -34,11 +34,11 @@ func computeHash(input int) (res int) /* @ ghost type HashFunction domain { - func hashFn(int) int - func invFn(int) int + func hashFn(integer) integer + func invFn(integer) integer axiom { // hashFn is injective - forall v int :: { hashFn(v) } invFn(hashFn(v)) == v + forall v integer :: { hashFn(v) } invFn(hashFn(v)) == v } } @ */ diff --git a/src/test/resources/regressions/features/hyper_properties/commitment.go b/src/test/resources/regressions/features/hyper_properties/commitment.go index 11a106034..4e6a7cc50 100644 --- a/src/test/resources/regressions/features/hyper_properties/commitment.go +++ b/src/test/resources/regressions/features/hyper_properties/commitment.go @@ -26,16 +26,16 @@ func verifyWithBranching(hash int, value int) (res bool) { // the following postcondition specifies that the Go function `computeHash` behaves like the // pure (mathematical) function `hashFn` for which we assume injectivity (see domain below) -// @ ensures res == hashFn(input) +// @ ensures integer(res) == hashFn(integer(input)) func computeHash(input int) (res int) /* @ ghost type HashFunction domain { - func hashFn(int) int - func invFn(int) int + func hashFn(integer) integer + func invFn(integer) integer axiom { // hashFn is injective - forall v int :: { hashFn(v) } invFn(hashFn(v)) == v + forall v integer :: { hashFn(v) } invFn(hashFn(v)) == v } } @ */ diff --git a/src/test/resources/regressions/features/hyper_properties/rel-simple01.gobra b/src/test/resources/regressions/features/hyper_properties/rel-simple01.gobra index ef89582aa..1ee540033 100644 --- a/src/test/resources/regressions/features/hyper_properties/rel-simple01.gobra +++ b/src/test/resources/regressions/features/hyper_properties/rel-simple01.gobra @@ -5,12 +5,15 @@ package relsimple01 // ##(--hyperMode extended --enableExperimentalHyperFeatures) +import "math" + requires rel(i, 0) == rel(i, 1) ensures low(res) func foo(i int) (res int) { return i + 42 } +requires i <= math.MaxInt64 - 42 requires rel(i, 0) < rel(i, 1) ensures rel(res, 0) < rel(res, 1) func bar(i int) (res int) { diff --git a/src/test/resources/regressions/features/integers/bounded_int_semantics.gobra b/src/test/resources/regressions/features/integers/bounded_int_semantics.gobra new file mode 100644 index 000000000..1b9caab94 --- /dev/null +++ b/src/test/resources/regressions/features/integers/bounded_int_semantics.gobra @@ -0,0 +1,111 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +package boundedInts + +// Tests for bounded integer semantics without overflow checking. +// The BoundedIntEncoding automatically adds range postconditions for bounded int results +// and allows equality comparison between bounded integers and untyped integer literals. + +// Identity function: verifier knows result is in [-128, 127]. +ensures ret == u +decreases +func identity8(u int8) (ret int8) { + ret = u + return +} + +// Test equality comparison between bounded int and integer literal (the mixed equal case). +// This was previously broken: comparing `u == 0` where `u: int8` and `0: integer` failed +// with "did not match with any implemented case of equal". +ensures (u == 0) == (ret == 0) +decreases +func testEqWithLiteral(u int8) (ret int8) { + if u == 0 { + ret = 0 + } else { + ret = u + } + return +} + +// Unsigned ints are always >= 0 (the type's varPostcondition ensures this). +ensures ret >= 0 +decreases +func nonNegU8(u uint8) (ret uint8) { + ret = u + return +} + +// Arithmetic: result is guaranteed to be in int8 range. +ensures -128 <= ret && ret <= 127 +decreases +func addResult(u int8, v int8) (ret int8) { + ret = u + v + return +} + +// When the sum provably doesn't overflow, equality can be asserted (without --overflow). +requires 0 <= u && u <= 100 +ensures ret > 0 +decreases +func addOneSmall(u int8) (ret int8) { + ret = u + 1 + return +} + +// Pure function: the result variable postcondition must be applied via fixResultvar. +// Previously, varPostcondition wasn't transformed, causing "local var not found" errors. +ensures ret >= 0 +decreases +pure func pureNonNeg(u uint8) (ret uint8) { + return u +} + +// Pure function: bounded int equality with literal in postcondition. +ensures ret == 0 +decreases +pure func pureZero() (ret uint8) { + return 0 +} + +// Default value of bounded int type is 0. +ensures ret == 0 +decreases +func defaultUint8() (ret uint8) { + return +} + +// Shared int (accessed via pointer) compared with integer literal. +// This covers the `int@` == `0` case (shared bounded int vs. integer literal). +requires acc(p) +decreases +func readAndCompare(p *int8) (eq bool) { + eq = *p == 0 + return +} + +// Test that comparison operators work with mixed bounded/unbounded. +requires u > 0 +ensures ret >= 0 +decreases +func testRelational(u int8) (ret int8) { + ret = u + return +} + +// Test subtraction: result stays in range. +ensures -128 <= ret && ret <= 127 +decreases +func subResult(u int8, v int8) (ret int8) { + ret = u - v + return +} + +// Test multiplication: result stays in range. +ensures -128 <= ret && ret <= 127 +decreases +func mulResult(u int8, v int8) (ret int8) { + ret = u * v + return +} diff --git a/src/test/resources/regressions/features/integers/container-len-fits-int.gobra b/src/test/resources/regressions/features/integers/container-len-fits-int.gobra new file mode 100644 index 000000000..c8ef7661a --- /dev/null +++ b/src/test/resources/regressions/features/integers/container-len-fits-int.gobra @@ -0,0 +1,40 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +// Go guarantees that len/cap of arrays, slices, and strings fit in `int`. +// Under bounded integer semantics this must be axiomatized: a user's +// quantified footprint over an `int`-typed index variable (implicitly +// range-guarded) must entail internally generated footprints that quantify +// over the mathematical integers. Found on VerifiedSCION's pkg/addr, where +// 'string(text)' under a standard slice footprint spec failed its +// precondition. + +package containerLenFitsInt + +// the exact UnmarshalText pattern from VerifiedSCION pkg/addr +preserves forall i int :: { &text[i] } 0 <= i && i < len(text) ==> acc(&text[i]) +decreases +func byteSliceToString(text []byte) string { + return string(text) +} + +// int is 64-bit by default (32-bit under --int32), so len/cap are bounded by MaxInt64 +decreases +func sliceLenBounded(s []int) { + assert len(s) <= 9223372036854775807 + assert cap(s) <= 9223372036854775807 +} + +decreases +func stringLenBounded(s string) { + assert len(s) <= 9223372036854775807 +} + +requires forall i int :: { &a[i] } 0 <= i && i < len(a) ==> acc(&a[i], perm(1, 2)) +decreases +func arrayViaSlice(a []byte) byte { + if len(a) > 0 { + return a[0] + } + return 0 +} diff --git a/src/test/resources/regressions/features/integers/defined-type-binop-mixed-kind.gobra b/src/test/resources/regressions/features/integers/defined-type-binop-mixed-kind.gobra new file mode 100644 index 000000000..45c80ffa5 --- /dev/null +++ b/src/test/resources/regressions/features/integers/defined-type-binop-mixed-kind.gobra @@ -0,0 +1,50 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +// Minimal reproduction of a crash found on VerifiedSCION's pkg/addr: +// a binary integer expression mixing a defined type (underlying bounded kind) +// with an operand of the underlying kind itself (e.g. from a conversion) +// crashed the internal-AST type merge ("cannot merge types AS_T and uint64"). + +package definedTypeBinopMixedKind + +type AS uint64 + +const asPartBits = 16 +const MaxAS AS = (1 << 48) - 1 + +// conversion result (uint64-kinded) mixed with defined-type operand +requires v < (1 << 16) +requires parsed <= (1 << 32) +decreases +func orPart(parsed AS, v uint64) AS { + res := parsed + res |= AS(v) + return res & MaxAS +} + +// shift-assign on a defined type with an untyped-constant shift amount +requires parsed < (1 << 32) +decreases +func shiftPart(parsed AS) AS { + res := parsed + res <<= asPartBits + return res +} + +type HostSVC uint16 + +const SVCMcast HostSVC = 0x8000 + +// bit-negation of a defined-type constant used inside a binary operation +// (crashed with a Viper consistency error: BitNeg's internal type claimed +// an unbounded integer while its encoding produced a domain value) +decreases +func base(h HostSVC) HostSVC { + return h & ^SVCMcast +} + +decreases +func multicast(h HostSVC) HostSVC { + return h | SVCMcast +} diff --git a/src/test/resources/regressions/features/integers/float-int-conversion-bridge.gobra b/src/test/resources/regressions/features/integers/float-int-conversion-bridge.gobra new file mode 100644 index 000000000..2cc9b21ff --- /dev/null +++ b/src/test/resources/regressions/features/integers/float-int-conversion-bridge.gobra @@ -0,0 +1,30 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +// Converting between a bounded integer kind and a float crashed the encoder +// with a Viper consistency error ("Function fromIntTo64 ... cannot be applied +// to ... Bounded_int"): the int<->float conversion functions operate on Viper +// Ints, but a bounded operand encodes to a domain value. Found on +// VerifiedSCION's router (dataplane.go) and verification/utils/floats. + +package floatIntConversionBridge + +decreases +func intToFloat(i int, b byte, i32 int32) (float64, float32) { + f := float64(i) + g := float32(b) + h := float64(i32) + return f + h, g +} + +decreases +func floatToInt(f float64, g float32) (int, byte, int32) { + return int(f), byte(g), int32(f) +} + +type Weight int64 + +decreases +func definedTypeRoundTrip(w Weight) float64 { + return float64(int64(w)) +} diff --git a/src/test/resources/regressions/features/integers/integer_bitwise_rejected.gobra b/src/test/resources/regressions/features/integers/integer_bitwise_rejected.gobra index c510cd255..adcf80d6c 100644 --- a/src/test/resources/regressions/features/integers/integer_bitwise_rejected.gobra +++ b/src/test/resources/regressions/features/integers/integer_bitwise_rejected.gobra @@ -3,6 +3,11 @@ package integerBitwiseRejected +// Bitwise operations are only defined on bounded integer kinds. Applying them to +// values of the ghost `integer` type (mathematical, unbounded precision) is rejected +// by the type-checker, mirroring Go's rule that bitwise operators require integer +// types with a fixed bit width. + ghost decreases pure func bitAndIntegerRejected(a, b integer) integer { diff --git a/src/test/resources/regressions/features/integers/integer_ghost_type.gobra b/src/test/resources/regressions/features/integers/integer_ghost_type.gobra index 06ddcb97c..0440c1b95 100644 --- a/src/test/resources/regressions/features/integers/integer_ghost_type.gobra +++ b/src/test/resources/regressions/features/integers/integer_ghost_type.gobra @@ -3,6 +3,10 @@ package integerGhostType +// Tests for the ghost `integer` type, which represents unbounded mathematical integers. +// The `integer` type is available in ghost code and specifications. + +// Ghost variables can be of type `integer`. ghost decreases func useGhostInteger() { @@ -11,6 +15,7 @@ func useGhostInteger() { assert y == 43 } +// Ghost functions can take and return `integer`. ghost ensures result == a + b decreases @@ -18,6 +23,7 @@ pure func addIntegers(a, b integer) (result integer) { return a + b } +// `integer` arithmetic has exact semantics (no overflow). ghost ensures result == n * 1000000000 decreases @@ -25,13 +31,25 @@ pure func largeMultiply(n integer) (result integer) { return n * 1000000000 } +// Equality on `integer` type. +ghost +ensures (a == b) == (b == a) +decreases +pure func integerEqSym(a, b integer) (result bool) { + return a == b +} + +// Large literal: larger than any bounded integer type. ghost decreases func largeLiteral() { - ghost var big integer = 9223372036854775808 + ghost var big integer = 9223372036854775808 // 2^63, exceeds int64 max assert big > 0 } +// The bound is required: without it `n + 1` may overflow (n == MaxInt64), in which +// case the result is unspecified under Gobra's sound bounded-integer semantics. +requires n < 9223372036854775807 ensures integer(result) == integer(n) + 1 decreases func convertToInteger(n int) (result int) { diff --git a/src/test/resources/regressions/features/integers/let-bound-int-widening.gobra b/src/test/resources/regressions/features/integers/let-bound-int-widening.gobra new file mode 100644 index 000000000..9a59db75f --- /dev/null +++ b/src/test/resources/regressions/features/integers/let-bound-int-widening.gobra @@ -0,0 +1,30 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +// A let-bound variable of a bounded kind whose right-hand side encodes to a plain +// Int (e.g. a length expression) crashed the backend ("No matching local variable +// ... with type Bounded_int" / "key not found"): the binder was typed from the +// right-hand side's internal type while the body's references used the frontend +// type. Found on VerifiedSCION's pkg/slayers/path/scion (DecodeFromBytesSpec). + +package letBoundIntWidening + +pred Bytes(s []byte, start integer, end integer) { + 0 <= start && start <= end && end <= cap(s) && + forall i int :: { &s[i] } start <= i && i < end ==> acc(&s[i]) +} + +ghost +requires Bytes(s, start, end) +requires start <= i && i < end +decreases +pure func GetByte(s []byte, start integer, end integer, i integer) byte { + return unfolding Bytes(s, start, end) in s[i] +} + +ghost +requires Bytes(b, 0, len(b)) && 4 <= len(b) +decreases +pure func f(b []byte) byte { + return let lenR := len(b) in GetByte(b, 0, lenR, 0) +} diff --git a/src/test/resources/regressions/features/integers/pure-result-sort-alignment.gobra b/src/test/resources/regressions/features/integers/pure-result-sort-alignment.gobra new file mode 100644 index 000000000..40a172688 --- /dev/null +++ b/src/test/resources/regressions/features/integers/pure-result-sort-alignment.gobra @@ -0,0 +1,30 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +// A pure function with a bounded result whose body encodes to a plain Int +// (untyped-constant conditional branches) crashed with "Type of function body +// must match function type". Found on VerifiedSCION's pkg/slayers/path/epic. + +package pureResultSortAlignment + +const MetadataLen = 4 + +type T struct { + x int +} + +pred (t *T) Mem() { acc(t) } + +ghost +requires acc(t.Mem(), _) +decreases +pure func (t *T) LenSpec() (l int) { + return unfolding acc(t.Mem(), _) in + (t.x == 0 ? MetadataLen : MetadataLen + 1) +} + +ghost +decreases +pure func mixedCond(b bool) int { + return b ? 4 : 4 + 1 +} diff --git a/src/test/resources/regressions/features/integers/unbounded_integers_flag.gobra b/src/test/resources/regressions/features/integers/unbounded_integers_flag.gobra new file mode 100644 index 000000000..9fde2735f --- /dev/null +++ b/src/test/resources/regressions/features/integers/unbounded_integers_flag.gobra @@ -0,0 +1,47 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +// ##(--unboundedIntegers) + +// With --unboundedIntegers, every integer type (including bounded types like int8) is encoded as +// Viper's mathematical (unbounded) Int. Arithmetic no longer overflows, so properties that only +// hold under unbounded semantics become provable. This restores Gobra's integer encoding prior to +// the sound bounded-integer semantics. +package unboundedInts + +// Under sound bounded int8 semantics (without --overflow), `u + u` may overflow, so `ret >= u` is +// not provable for `u > 63`. With --unboundedIntegers, int8 behaves as a mathematical integer and +// the property holds for every non-negative u. +requires 0 <= u +ensures ret >= u +decreases +func doubleGeq(u int8) (ret int8) { + ret = u + u + return +} + +// The sum of two non-negative values dominates each summand under unbounded semantics, even when +// the sum would overflow int8 under Go's bounded semantics. +requires 0 <= u && 0 <= v +ensures ret >= u && ret >= v +decreases +func addBothGeq(u int8, v int8) (ret int8) { + ret = u + v + return +} + +// Negative checks: with --unboundedIntegers no range axioms are emitted for the bounded kinds, so a +// bounded value is an arbitrary mathematical integer and nothing is known about its range. If either +// assertion below ever starts to verify, the bounded range axioms are leaking into this encoding. + +decreases +func uint8IsNotKnownNonNegative(x uint8) { + //:: ExpectedOutput(assert_error:assertion_error) + assert x >= 0 +} + +decreases +func int8IsNotKnownWithinRange(x int8) { + //:: ExpectedOutput(assert_error:assertion_error) + assert x <= 127 +} diff --git a/src/test/resources/regressions/features/let/let_simple.gobra b/src/test/resources/regressions/features/let/let_simple.gobra index f50a4e1b6..3b3cefbcd 100644 --- a/src/test/resources/regressions/features/let/let_simple.gobra +++ b/src/test/resources/regressions/features/let/let_simple.gobra @@ -9,6 +9,7 @@ pure func f(x int) int { } ghost +requires -1000000 < x && x < 1000000 // bound x so the additions in f provably do not overflow ensures res == 3 * x + 3 func g(x int) (res int) { return f(x) diff --git a/src/test/resources/regressions/features/loops/loops-continue-label-fail2.gobra b/src/test/resources/regressions/features/loops/loops-continue-label-fail2.gobra index d4a65a023..314d0975b 100644 --- a/src/test/resources/regressions/features/loops/loops-continue-label-fail2.gobra +++ b/src/test/resources/regressions/features/loops/loops-continue-label-fail2.gobra @@ -4,14 +4,18 @@ package pkg func count_even(n int) (res int) { + invariant 0 <= i && i <= 10 decreases 10 - i for i := 0; i < 10; i++ { label1: + invariant 0 <= j && j <= 10 decreases 10 - j for j := 0; j < 10; j++ { + invariant 0 <= k && k <= 10 decreases 10 - k for k := 0; k < 10; k++ { label2: + invariant 0 <= l && l <= 10 decreases 10 - l for l := 0; l < 10; l++ { if k > 5 { diff --git a/src/test/resources/regressions/features/loops/loops-continue-label5.gobra b/src/test/resources/regressions/features/loops/loops-continue-label5.gobra index 84cac8b11..bda3a294c 100644 --- a/src/test/resources/regressions/features/loops/loops-continue-label5.gobra +++ b/src/test/resources/regressions/features/loops/loops-continue-label5.gobra @@ -4,14 +4,20 @@ package pkg func count_even(n int) (res int) { + // the `0 <= x` invariants bound the loop variables so the termination measures + // `10 - x` are provably free of overflow + invariant 0 <= i decreases 10 - i for i := 0; i < 10; i++ { label1: + invariant 0 <= j decreases 10 - j for j := 0; j < 10; j++ { + invariant 0 <= k decreases 10 - k for k := 0; k < 10; k++ { label2: + invariant 0 <= l decreases 10 - l for l := 0; l < 10; l++ { if k > 5 { diff --git a/src/test/resources/regressions/features/loops/loops-continue3.gobra b/src/test/resources/regressions/features/loops/loops-continue3.gobra index 41a98b3eb..340d5b334 100644 --- a/src/test/resources/regressions/features/loops/loops-continue3.gobra +++ b/src/test/resources/regressions/features/loops/loops-continue3.gobra @@ -7,6 +7,7 @@ func foo() { var i int = 0 var j int = 0 invariant 0 <= i && i <= 10 + invariant 0 <= j && j <= 9 // bounds j so that j + 1 in the invariant below is provably exact (no overflow) invariant i < 5 ==> i == j invariant i == 5 ==> j == 5 invariant i > 5 ==> i == j + 1 diff --git a/src/test/resources/regressions/features/loops/range1.gobra b/src/test/resources/regressions/features/loops/range1.gobra index 24d426be5..1c8b337bd 100644 --- a/src/test/resources/regressions/features/loops/range1.gobra +++ b/src/test/resources/regressions/features/loops/range1.gobra @@ -3,9 +3,11 @@ package pkg +import "math" preserves acc(x) requires len(x) > 0 +requires len(x) <= math.MaxInt64 // bound so the range counter's int assignment stays exact ensures forall i int :: 0 <= i && i < len(x) ==> max >= x[i] decreases func foo(x []uint) (max uint) { diff --git a/src/test/resources/regressions/features/loops/range_maps1.gobra b/src/test/resources/regressions/features/loops/range_maps1.gobra index 8e45de25d..9378ec883 100644 --- a/src/test/resources/regressions/features/loops/range_maps1.gobra +++ b/src/test/resources/regressions/features/loops/range_maps1.gobra @@ -3,16 +3,20 @@ package pkg +import "math" +// Quantified integer variables range over all mathematical integers, so the +// quantifiers that compare the bound variable itself against a bounded value +// need an explicit range guard. requires acc(x) requires len(x) > 0 ensures acc(x) -ensures forall k uint :: k elem domain(x) ==> max >= k +ensures forall k uint :: 0 <= k && k <= math.MaxUint64 ==> (k elem domain(x) ==> max >= k) decreases func foo(x map[uint]int) (max uint) { max = 0 invariant acc(x) - invariant forall i uint :: i elem visited ==> max >= i + invariant forall i uint :: 0 <= i && i <= math.MaxUint64 ==> (i elem visited ==> max >= i) decreases len(domain(x)) - len(visited) for k, v := range x with visited { if k > max { @@ -25,7 +29,7 @@ decreases func bar() { x := map[uint]int{1:1, 2:2, 3:3} m := foo(x) - assert forall i uint :: i elem domain(x) ==> m >= i + assert forall i uint :: 0 <= i && i <= math.MaxUint64 ==> (i elem domain(x) ==> m >= i) } requires acc(x) diff --git a/src/test/resources/regressions/features/multisets/multiset-cardinality-simple1.gobra b/src/test/resources/regressions/features/multisets/multiset-cardinality-simple1.gobra index 1edf86beb..d1e8f97d0 100644 --- a/src/test/resources/regressions/features/multisets/multiset-cardinality-simple1.gobra +++ b/src/test/resources/regressions/features/multisets/multiset-cardinality-simple1.gobra @@ -3,6 +3,8 @@ package pkg +// Ghost collection sizes have the mathematical `integer` type; assigning one to a +// bounded `int` would need an explicit conversion, so the out-parameter is `integer`. func example1(ghost m mset[int]) (ghost n integer) { n = len(m) } diff --git a/src/test/resources/regressions/features/multisets/multiset-multiplicity-simple1.gobra b/src/test/resources/regressions/features/multisets/multiset-multiplicity-simple1.gobra index 473d5be11..5f7ef7687 100644 --- a/src/test/resources/regressions/features/multisets/multiset-multiplicity-simple1.gobra +++ b/src/test/resources/regressions/features/multisets/multiset-multiplicity-simple1.gobra @@ -4,6 +4,7 @@ package pkg func example1(ghost x int, y int, ghost m mset[int]) { + // multiplicity on ghost collections has the mathematical `integer` type ghost var n1 integer ghost var n2 integer @@ -20,6 +21,7 @@ func example2(ghost x int, ghost y int, ghost m mset[int]) { } func example3(ghost x int, ghost m1 mset[int], ghost m2 mset[int]) { + // multiplicity on ghost collections has the mathematical `integer` type ghost var n integer n = x # m1 # m2 assert x # m1 # m2 == (x # m1) # m2 diff --git a/src/test/resources/regressions/features/multisets/multiset-range-simple1.gobra b/src/test/resources/regressions/features/multisets/multiset-range-simple1.gobra index 76f6a0729..ecc9e0525 100644 --- a/src/test/resources/regressions/features/multisets/multiset-range-simple1.gobra +++ b/src/test/resources/regressions/features/multisets/multiset-range-simple1.gobra @@ -16,7 +16,9 @@ func example2() { assert mset[1..2] == mset[int] { 1 } // could we do without the first assertion? - assert seq[1..10] == seq[int] { 1, 2, 3, 4, 5, 6, 7, 8, 9 } + // the range sequence has mathematical `integer` elements, so the helper + // equality must be stated against a seq[integer] literal to be usable below + assert seq[1..10] == seq[integer] { 1, 2, 3, 4, 5, 6, 7, 8, 9 } assert 3 elem mset[1..10] assert 2 + 3 elem mset[1..10] assert !(42 elem mset[1..10]) diff --git a/src/test/resources/regressions/features/no_semicolons/examples/tour/Test2.gobra b/src/test/resources/regressions/features/no_semicolons/examples/tour/Test2.gobra index 9627de2b5..9129ff32d 100644 --- a/src/test/resources/regressions/features/no_semicolons/examples/tour/Test2.gobra +++ b/src/test/resources/regressions/features/no_semicolons/examples/tour/Test2.gobra @@ -4,11 +4,16 @@ package trivial requires 0 <= n -ensures 2*e == (n+1)*n +// bound n so no product overflows and i++ up to n+1 stays in range +requires n <= 1000000 +// stated as e == (n+1)*n/2 rather than 2*e == (n+1)*n: multiplying the sum e by +// 2 would exceed the range where the bounded multiplication is provably exact, +// whereas (n+1)*n and i*(i-1) keep both operands small. +ensures e == (n+1)*n/2 func sum(n int) (e int) { assert e == 0 invariant 0 <= i && i <= n + 1 - invariant 2*e == i*(i-1) + invariant e == i*(i-1)/2 for i := 1; i <= n; i++ { e = e + i } diff --git a/src/test/resources/regressions/features/no_semicolons/while1.gobra b/src/test/resources/regressions/features/no_semicolons/while1.gobra index 83a08139f..1c1e8f7d8 100644 --- a/src/test/resources/regressions/features/no_semicolons/while1.gobra +++ b/src/test/resources/regressions/features/no_semicolons/while1.gobra @@ -6,11 +6,12 @@ package pkg requires x >= 0 +requires x < 100000 && -100000 < y && y < 100000 // bounds so that i*y and z += y provably do not overflow ensures z == x*y func test(x, y int) (z int) { + invariant 0 <= i && i <= x // lower bound needed so i*y and z += y are provably exact invariant z == i*y - invariant i <= x for i := 0; i < x; i += 1 { z += y } diff --git a/src/test/resources/regressions/features/options/options-simple1.gobra b/src/test/resources/regressions/features/options/options-simple1.gobra index eb687be51..6d0158d20 100644 --- a/src/test/resources/regressions/features/options/options-simple1.gobra +++ b/src/test/resources/regressions/features/options/options-simple1.gobra @@ -57,6 +57,11 @@ func test10() { requires o != none[int] func test11(ghost o option[int]) { assert o == some(get(o)) + // exhibit the witness at the mathematical-integer kind: quantified integer + // variables range over mathematical integers, so the existential below needs + // a ground witness of that shape. + ghost var w integer = get(o) + assert o == some(w) assert exists v int :: o == some(v) assert 0 < len(seq(o)) } diff --git a/src/test/resources/regressions/features/outline/outline-simple1.gobra b/src/test/resources/regressions/features/outline/outline-simple1.gobra index babd3dae4..14238fe59 100644 --- a/src/test/resources/regressions/features/outline/outline-simple1.gobra +++ b/src/test/resources/regressions/features/outline/outline-simple1.gobra @@ -43,13 +43,13 @@ func test4() { x := 2 - requires x > 0 + requires 0 < x && x < 100 ensures x > 3 outline ( x += 1 - requires x > 1 - ensures x > 2 + requires 1 < x && x < 101 + ensures 2 < x && x < 102 outline ( x += 1 ) diff --git a/src/test/resources/regressions/features/outline/outline-simple2.gobra b/src/test/resources/regressions/features/outline/outline-simple2.gobra index c0cc504c6..6b3d72b74 100644 --- a/src/test/resources/regressions/features/outline/outline-simple2.gobra +++ b/src/test/resources/regressions/features/outline/outline-simple2.gobra @@ -46,14 +46,14 @@ func test4() { x := 2 - requires x > 0 + requires 0 < x && x < 100 ensures x > 3 outline ( x += 1 trusted - requires x > 1 - ensures x > 2 + requires 1 < x && x < 101 + ensures 2 < x && x < 102 outline ( x += 6 ) diff --git a/src/test/resources/regressions/features/overflow_checks/bounded_int_overflow.gobra b/src/test/resources/regressions/features/overflow_checks/bounded_int_overflow.gobra new file mode 100644 index 000000000..2b3c75a80 --- /dev/null +++ b/src/test/resources/regressions/features/overflow_checks/bounded_int_overflow.gobra @@ -0,0 +1,124 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +// ##(--overflow) +package boundedIntOverflow + +// Tests for overflow detection using the domain-based BoundedIntEncoding. +// With --overflow, arithmetic functions require preconditions proving the result +// stays in the bounded type's range. Violating these preconditions is an +// integer_overflow_error. + +// Safe increment: precondition prevents overflow. +requires u < 127 +ensures ret == u + 1 +decreases +func safeInc(u int8) (ret int8) { + ret = u + 1 + return +} + +// Unsafe increment: no bounds on u, result may overflow. +decreases +func unsafeInc(u int8) (ret int8) { + //:: ExpectedOutput(integer_overflow_error) + ret = u + 1 + return +} + +// Safe unsigned increment: precondition prevents overflow. +requires u < 255 +ensures ret == u + 1 +decreases +func safeIncU8(u uint8) (ret uint8) { + ret = u + 1 + return +} + +// Unsafe unsigned increment: no bounds on u. +decreases +func unsafeIncU8(u uint8) (ret uint8) { + //:: ExpectedOutput(integer_overflow_error) + ret = u + 1 + return +} + +// Safe multiplication: known small values (0 <= u <= 11, max product 121 <= 127). +requires 0 <= u && u <= 11 +ensures ret == u * u +decreases +func safeMul(u int8) (ret int8) { + ret = u * u + return +} + +// Unsafe multiplication: u * u may overflow int8. +decreases +func unsafeMul(u int8) (ret int8) { + //:: ExpectedOutput(integer_overflow_error) + ret = u * u + return +} + +// Pure functions can also trigger overflow errors. +requires u <= 126 +ensures ret == u + 1 +decreases +pure func safePureInc(u int8) (ret int8) { + return u + 1 +} + +// Unsafe pure function. +decreases +pure func unsafePureInc(u int8) (ret int8) { + //:: ExpectedOutput(integer_overflow_error) + return u + 1 +} + +// Conversion: int8 to uint8 — safe when value is non-negative. +requires 0 <= x +ensures ret == uint8(x) +decreases +func safeConv(x int8) (ret uint8) { + ret = uint8(x) + return +} + +// Conversion: int8 to uint8 — potentially unsafe. +decreases +func unsafeConv(x int8) (ret uint8) { + //:: ExpectedOutput(integer_overflow_error) + ret = uint8(x) + return +} + +// int32 arithmetic: overflow for default 32-bit int. +requires i <= 2147483646 +ensures res == i + 1 +decreases +func safeInt32Inc(i int32) (res int32) { + res = i + 1 + return +} + +decreases +func unsafeInt32Inc(i int32) (res int32) { + //:: ExpectedOutput(integer_overflow_error) + res = i + 1 + return +} + +// Subtraction overflow. +requires x > -128 +decreases +func safeSub(x int8) (ret int8) { + ret = x - 1 + return +} + +decreases +func unsafeSub(x int8) (ret int8) { + //:: ExpectedOutput(integer_overflow_error) + ret = x - 1 + return +} diff --git a/src/test/resources/regressions/features/overflow_checks/overflow_int32.gobra b/src/test/resources/regressions/features/overflow_checks/overflow_int32.gobra index 5fe527800..2d1ba78ae 100644 --- a/src/test/resources/regressions/features/overflow_checks/overflow_int32.gobra +++ b/src/test/resources/regressions/features/overflow_checks/overflow_int32.gobra @@ -16,18 +16,18 @@ func increment8(u int8) (ret int8) { } // Operation may overflow, no bound checks are performed +//:: ExpectedOutput(integer_overflow_error) ensures ret == u + 1 func incrementU8(u uint8) (ret uint8) { - //:: ExpectedOutput(integer_overflow_error) ret = u + 1 return } // Should overflow when running in 32 bit mode, not in 64 requires i <= 2147483647 +//:: ExpectedOutput(integer_overflow_error) ensures res == i + 1 func overflowInt32(i int) (res int) { - //:: ExpectedOutput(integer_overflow_error) return i + 1 } @@ -84,8 +84,8 @@ func strangeAbs(x int) int { // Test loop conditions func whileCheck() { x := 0 + //:: ExpectedOutput(integer_overflow_error) for x + 1 > 0 { - //:: ExpectedOutput(integer_overflow_error) x += 1 } } diff --git a/src/test/resources/regressions/features/overflow_checks/overflow_int64.gobra b/src/test/resources/regressions/features/overflow_checks/overflow_int64.gobra index 30801d26c..2c6fcc761 100644 --- a/src/test/resources/regressions/features/overflow_checks/overflow_int64.gobra +++ b/src/test/resources/regressions/features/overflow_checks/overflow_int64.gobra @@ -12,8 +12,15 @@ func overflowInt32(i int) (res int) { } // Operation may lead to overflow, no bound checks are performed on the argument u before incrementing it +//:: ExpectedOutput(integer_overflow_error) ensures ret == u + 1 func incrementOverflows(u uint) (ret uint) { + ret = u + 1 + return +} + +ensures integer(ret) == integer(u) + 1 +func incrementOverflows2(u uint) (ret uint) { //:: ExpectedOutput(integer_overflow_error) ret = u + 1 return @@ -26,8 +33,8 @@ func f(i int) uint { } // For 64-bit ints, the following can lead to an overflow. +//:: ExpectedOutput(integer_overflow_error) ensures res == int64(i) + 1 func inc(i int) (res int64) { - //:: ExpectedOutput(integer_overflow_error) return int64(i) + 1 } \ No newline at end of file diff --git a/src/test/resources/regressions/features/purefuncs/quantifier2.gobra b/src/test/resources/regressions/features/purefuncs/quantifier2.gobra index b1925b376..31f21f8e2 100644 --- a/src/test/resources/regressions/features/purefuncs/quantifier2.gobra +++ b/src/test/resources/regressions/features/purefuncs/quantifier2.gobra @@ -7,5 +7,7 @@ pure func test(a int)(res int){ return a+1 } -ensures forall i int :: i == test(i)-1 +// The quantified i is a mathematical integer, so it must be bounded to the +// int64 range on both sides for the call test(i) and i+1 to be provably exact. +ensures forall i int :: -9223372036854775808 <= i && i < 9223372036854775807 ==> i == test(i)-1 func client(){} diff --git a/src/test/resources/regressions/features/purefuncs/sum_struct.gobra b/src/test/resources/regressions/features/purefuncs/sum_struct.gobra index 671d18612..44a0a7a87 100644 --- a/src/test/resources/regressions/features/purefuncs/sum_struct.gobra +++ b/src/test/resources/regressions/features/purefuncs/sum_struct.gobra @@ -13,7 +13,20 @@ pure func test(t tripleInt) (res int) { return t.a + t.b + t.c } -ensures forall i int, a int, b int, c int :: test(tripleInt{a,b,c})==test(tripleInt{a-i,b+i,c}) +// The quantified variables are mathematical integers, so the quantifier guard must +// bound them (keeping the sums small) for the additions in test, a-i, and b+i to be +// provably exact; the same bounds are required of the function's own parameters for +// the asserts in the body. +requires -1000000 < a && a < 1000000 +requires -1000000 < b && b < 1000000 +requires -1000000 < c && c < 1000000 +requires -1000000 < i && i < 1000000 +ensures forall i int, a int, b int, c int :: + (-1000000 < a && a < 1000000 && + -1000000 < b && b < 1000000 && + -1000000 < c && c < 1000000 && + -1000000 < i && i < 1000000) ==> + test(tripleInt{a,b,c})==test(tripleInt{a-i,b+i,c}) func client(a,b,c,i int){ assert test(tripleInt{a:a,b:b,c:c}) == a+b+c assert test(tripleInt{a,b,c}) == test(tripleInt{a,b+i,c-i}) diff --git a/src/test/resources/regressions/features/quantifiers/bounded-domain-quantification.gobra b/src/test/resources/regressions/features/quantifiers/bounded-domain-quantification.gobra new file mode 100644 index 000000000..932fb34cb --- /dev/null +++ b/src/test/resources/regressions/features/quantifiers/bounded-domain-quantification.gobra @@ -0,0 +1,69 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +package boundedDomainQuantification + +// A quantified variable of a bounded integer kind ranges over exactly the +// values of that type: the type's range is implicit in the quantifier. + +func rangeIsImplicit() { + assert forall x uint8 :: x >= 0 + assert forall x uint8 :: x <= 255 + assert forall x int8 :: -128 <= x && x <= 127 + assert forall x int32 :: -2147483648 <= x && x <= 2147483647 +} + +// Existential witness finding needs a trigger-carrying term (a pre-existing +// Silicon/Z3 limitation, independent of bounded kinds); with one, in-range +// witnesses of a bounded existential are found. +func existsWitnessInRange() { + assert ghostId(255) == 255 + assert exists x uint8 :: { ghostId(x) } ghostId(x) == 255 +} + +ghost +decreases +pure func ghostId(x uint8) uint8 { + return x +} + +func failsOutsideRange() { + //:: ExpectedOutput(assert_error:assertion_error) + assert forall x uint8 :: x < 255 +} + +func noWitnessOutsideRange() { + //:: ExpectedOutput(assert_error:assertion_error) + assert exists x uint8 :: x > 255 +} + +// The implicit range must also hold for variables only used at their domain +// type (e.g. passed to a function), not just in arithmetic positions. +ghost +decreases +pure func isSmall(x uint8) bool { + return x <= 255 +} + +func domainUseInRange() { + assert forall x uint8 :: isSmall(x) +} + +// Quantified permissions keep working with bounded index variables: the +// implicit range guard must not break the injectivity of &s[i]. +requires forall i int :: { &s[i] } 0 <= i && i < len(s) ==> acc(&s[i]) +ensures forall i int :: { &s[i] } 0 <= i && i < len(s) ==> acc(&s[i]) +func qpOverSlice(s []int) { + if len(s) > 0 { + s[0] = 42 + } +} + +// An assign-such-that over a bounded variable can only pick in-range values. +ghost +decreases +func suchThatInRange() { + assert ghostId(7) == 7 + var x uint8 :| ghostId(x) == 7 + assert 0 <= x && x <= 255 +} diff --git a/src/test/resources/regressions/features/sequences/seq-contains-simple1.gobra b/src/test/resources/regressions/features/sequences/seq-contains-simple1.gobra index 111f30261..4c27b1480 100644 --- a/src/test/resources/regressions/features/sequences/seq-contains-simple1.gobra +++ b/src/test/resources/regressions/features/sequences/seq-contains-simple1.gobra @@ -43,6 +43,9 @@ func example7(n int) { } requires 0 < n -ensures n - 1 elem seq[0 ..n] +// With a typed (bounded) upper bound the range elements would be bounded ints, and +// membership of the arithmetic result n - 1 in the converted sequence is not derivable; +// with mathematical `integer` bounds the range elements are exact integers. +ensures n - 1 elem seq[0 .. integer(n)] func example8(n int) { } diff --git a/src/test/resources/regressions/features/sequences/seq-convert-fail3.gobra b/src/test/resources/regressions/features/sequences/seq-convert-fail3.gobra index 24443c4ef..13128d239 100644 --- a/src/test/resources/regressions/features/sequences/seq-convert-fail3.gobra +++ b/src/test/resources/regressions/features/sequences/seq-convert-fail3.gobra @@ -8,10 +8,13 @@ func foo() { test1(xs) test2(xs) test3(xs) + // passing the `integer`-typed `len(xs)` where a `seq[int]` is expected is a type error //:: ExpectedOutput(type_error) test4(len(xs)) } +// `len` on ghost collections (sequences, sets, ...) has the mathematical `integer` type. + func test1(ghost s seq[int]) (ghost res integer) { return len(s) } diff --git a/src/test/resources/regressions/features/sequences/seq-length-simple1.gobra b/src/test/resources/regressions/features/sequences/seq-length-simple1.gobra index cbe81d127..ff922ee6e 100644 --- a/src/test/resources/regressions/features/sequences/seq-length-simple1.gobra +++ b/src/test/resources/regressions/features/sequences/seq-length-simple1.gobra @@ -23,5 +23,6 @@ func example4() { assert len(seq[bool] { true, false }) == 2 assert len(seq[bool] { true }) == len(seq[int] { 42 }) assert len(seq[seq[int]] { seq[int] { 1 }, seq[int] { 17, 142 } }) == 2; + // int conversions needed since len() on ghost sequences has type integer assert seq[int] { int(len(seq[int] { 1 })), int(len(seq[int] { 17, 142 })) } == seq[int] { 1, 2 }; } diff --git a/src/test/resources/regressions/features/sequences/seq-multiplicity-simple1.gobra b/src/test/resources/regressions/features/sequences/seq-multiplicity-simple1.gobra index e168b5488..18a222842 100644 --- a/src/test/resources/regressions/features/sequences/seq-multiplicity-simple1.gobra +++ b/src/test/resources/regressions/features/sequences/seq-multiplicity-simple1.gobra @@ -31,7 +31,9 @@ func example4() { } func example5() { - assert seq[1..10] == seq[int] { 1, 2, 3, 4, 5, 6, 7, 8, 9 } // needed?? + // range sequences with untyped bounds have mathematical `integer` elements, so the + // helper equality must be stated against a seq[integer] literal to be usable below + assert seq[1..10] == seq[integer] { 1, 2, 3, 4, 5, 6, 7, 8, 9 } // needed?? assert 4 # seq[1..10] == 1 assert 42 # seq[1..10] == 0 } diff --git a/src/test/resources/regressions/features/sequences/seq-range-simple1.gobra b/src/test/resources/regressions/features/sequences/seq-range-simple1.gobra index 7da4b1147..1e9e4c8e3 100644 --- a/src/test/resources/regressions/features/sequences/seq-range-simple1.gobra +++ b/src/test/resources/regressions/features/sequences/seq-range-simple1.gobra @@ -25,6 +25,7 @@ func example5() { } requires x <= y +requires -1000000 <= x && y <= 1000000 // bounds so that y + 1 and y - x + 1 provably do not overflow func example6(x int, y int) { assert len(seq[x..y + 1]) == y - x + 1 } diff --git a/src/test/resources/regressions/features/sets/set-cardinality-simple1.gobra b/src/test/resources/regressions/features/sets/set-cardinality-simple1.gobra index 8985709f6..b3143f4ce 100644 --- a/src/test/resources/regressions/features/sets/set-cardinality-simple1.gobra +++ b/src/test/resources/regressions/features/sets/set-cardinality-simple1.gobra @@ -3,6 +3,7 @@ package pkg +// `len` on ghost collections has the mathematical `integer` type func example1(ghost s set[int]) (ghost n integer) { n = len(s); } @@ -19,6 +20,7 @@ func example3() { } ensures n == len(s union set[int] { 42 }); +// `len` on ghost collections has the mathematical `integer` type func example4(ghost s set[int]) (ghost n integer) { n = len(s union set[int] { 42 }); } diff --git a/src/test/resources/regressions/features/sets/set-convert-simple2.gobra b/src/test/resources/regressions/features/sets/set-convert-simple2.gobra index 891736fc1..5d21d0354 100644 --- a/src/test/resources/regressions/features/sets/set-convert-simple2.gobra +++ b/src/test/resources/regressions/features/sets/set-convert-simple2.gobra @@ -32,7 +32,9 @@ func example5(ghost xs seq[int], ghost ys seq[int]) { func example6() { assert set(seq[int] { }) == set[int] { } assert set(seq[int] { 1, 2, 3 }) == set[int] { 1, 2, 3 } - assert set(seq[1..4]) == set[int] { 1, 2, 3 } + // the range sequence has mathematical `integer` elements, so its set + // conversion is compared against a set[integer] literal + assert set(seq[1..4]) == set[integer] { 1, 2, 3 } assert set(seq[int] { 1, 2, 3, 2, 1 }) == set[int] { 3, 1, 2 } } diff --git a/src/test/resources/regressions/features/sets/set-multiplicity-simple1.gobra b/src/test/resources/regressions/features/sets/set-multiplicity-simple1.gobra index 61b0527db..d29be2d4b 100644 --- a/src/test/resources/regressions/features/sets/set-multiplicity-simple1.gobra +++ b/src/test/resources/regressions/features/sets/set-multiplicity-simple1.gobra @@ -6,6 +6,7 @@ package pkg ensures 0 <= n && n <= 1 ensures n == 0 ==> !(x elem s) ensures n == 1 ==> x elem s +// multiplicity on ghost collections has the mathematical `integer` type func example1(ghost x int, ghost s set[int]) (ghost n integer) { n = x # s } diff --git a/src/test/resources/regressions/features/sets/set-range-simple1.gobra b/src/test/resources/regressions/features/sets/set-range-simple1.gobra index abf0a081d..054e217f9 100644 --- a/src/test/resources/regressions/features/sets/set-range-simple1.gobra +++ b/src/test/resources/regressions/features/sets/set-range-simple1.gobra @@ -19,7 +19,9 @@ func example3() { } func example4() { - assert set[1..4] == set[int] { 1, 2, 3 } + // range sets with untyped bounds have mathematical `integer` elements, + // so they are compared against set[integer] literals + assert set[1..4] == set[integer] { 1, 2, 3 } assert len(set[1..4]) == 3 } @@ -28,7 +30,7 @@ func example5() { } func example6() { - assert set[-4 .. -1] == set[int] { -4, -3, -2 } + assert set[-4 .. -1] == set[integer] { -4, -3, -2 } } func example7() { diff --git a/src/test/resources/regressions/features/slices/slice-cap-simple1.gobra b/src/test/resources/regressions/features/slices/slice-cap-simple1.gobra index 78dc9b342..7bed31bc9 100644 --- a/src/test/resources/regressions/features/slices/slice-cap-simple1.gobra +++ b/src/test/resources/regressions/features/slices/slice-cap-simple1.gobra @@ -3,6 +3,11 @@ package pkg +import "math" + +// bound so that the assignment of the mathematical capacity to the +// bounded-int variable n is provably exact +requires cap(s) <= math.MaxInt64 func test1(s []int) { n := cap(s) assert 0 <= n diff --git a/src/test/resources/regressions/features/slices/slice-length-simple1.gobra b/src/test/resources/regressions/features/slices/slice-length-simple1.gobra index ad5897d90..3d50c6312 100644 --- a/src/test/resources/regressions/features/slices/slice-length-simple1.gobra +++ b/src/test/resources/regressions/features/slices/slice-length-simple1.gobra @@ -3,6 +3,11 @@ package pkg +import "math" + +// bound so that the assignment of the mathematical length to the +// bounded-int variable n is provably exact +requires len(s) <= math.MaxInt64 func test1(s []int) { n := len(s) assert 0 <= n diff --git a/src/test/resources/regressions/features/stubs/math-limits-fail.gobra b/src/test/resources/regressions/features/stubs/math-limits-fail.gobra new file mode 100644 index 000000000..ef5fef2fc --- /dev/null +++ b/src/test/resources/regressions/features/stubs/math-limits-fail.gobra @@ -0,0 +1,15 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +package main + +import "math" + +// The architecture-dependent limits MaxInt, MinInt, and MaxUint are +// deliberately omitted from the math stub so that proofs cannot depend on the +// (unfixed) platform word size. Referencing them is a type error. +func useOmittedMaxInt() { + //:: ExpectedOutput(type_error) + var x int = math.MaxInt + _ = x +} diff --git a/src/test/resources/regressions/features/stubs/math-limits.gobra b/src/test/resources/regressions/features/stubs/math-limits.gobra new file mode 100644 index 000000000..d0922434a --- /dev/null +++ b/src/test/resources/regressions/features/stubs/math-limits.gobra @@ -0,0 +1,39 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +package main + +import "math" + +// The concrete-type integer limits from the math package are usable as the +// (untyped) constants they are: assignable to the corresponding sized type +// and to any wider one. +func concreteLimits() { + var a int8 = math.MaxInt8 + var b int8 = math.MinInt8 + var c int16 = math.MaxInt16 + var d int16 = math.MinInt16 + var e int32 = math.MaxInt32 + var f int32 = math.MinInt32 + var g int64 = math.MaxInt64 + var h int64 = math.MinInt64 + var i uint8 = math.MaxUint8 + var j uint16 = math.MaxUint16 + var k uint32 = math.MaxUint32 + var l uint64 = math.MaxUint64 + + assert a == 127 && b == -128 + assert c == 32767 && d == -32768 + assert e == 2147483647 && f == -2147483648 + assert g == 9223372036854775807 && h == -9223372036854775808 + assert i == 255 && j == 65535 + assert k == 4294967295 && l == 18446744073709551615 +} + +// The limits work as spec-level bounds that exclude overflow. +requires 0 <= n && n < math.MaxInt32 +ensures ret == n + 1 +decreases +func incNoOverflow(n int32) (ret int32) { + return n + 1 +} diff --git a/src/test/resources/regressions/features/termination/termination-fail-01.gobra b/src/test/resources/regressions/features/termination/termination-fail-01.gobra index 2c38992e2..f1ef27454 100644 --- a/src/test/resources/regressions/features/termination/termination-fail-01.gobra +++ b/src/test/resources/regressions/features/termination/termination-fail-01.gobra @@ -1,6 +1,10 @@ // Any copyright is dedicated to the Public Domain. // http://creativecommons.org/publicdomain/zero/1.0/ +// Under the sound bounded-integer encoding, `n+1` may overflow, so the loop invariant `i <= n+1` +// is no longer established and the expected termination error is masked. Restore the previous +// unbounded-integer encoding, under which the termination checks behave as this test expects. +// ##(--unboundedIntegers) package termination decreases x diff --git a/src/test/resources/regressions/features/termination/termination-simple-01.gobra b/src/test/resources/regressions/features/termination/termination-simple-01.gobra index aeab77531..ac371a651 100644 --- a/src/test/resources/regressions/features/termination/termination-simple-01.gobra +++ b/src/test/resources/regressions/features/termination/termination-simple-01.gobra @@ -3,6 +3,8 @@ package termination +import "math" + ghost requires n >= 0 decreases n @@ -19,7 +21,7 @@ requires m >= 0 requires n >= 0 ensures result >= 0 decreases m, n -func ack(m int, n int) (result int) { +func ack(m integer, n integer) (result integer) { if m == 0 { return n+1 } else { @@ -49,8 +51,10 @@ func collatz(n int) int { } else { if n%2 == 0 { return collatz(n/2) - } else { + } else if n <= (math.MaxInt64-1)/3 { return collatz(3*n + 1) + } else { + return n } } } @@ -89,12 +93,12 @@ func sign(x int) int { } } -requires 0 <= n +requires 0 <= n && n <= 1000000 ensures res == n * (n+1)/2 decreases func sum(n int) (res int) { res := 0 - invariant i <= n+1 + invariant 0 <= i && i <= n+1 invariant res == (i-1) * i/2 decreases n-i for i := 0; i <= n; i++ { @@ -104,6 +108,7 @@ func sum(n int) (res int) { } ghost +requires len(s) <= math.MaxInt64 decreases s func sum2(ghost s seq[int]) int { ghost res := 0 @@ -173,11 +178,12 @@ requires m >= 0 requires n >= 0 ensures result >= 0 decreases m, n -pure func ackPure(m int, n int) (result int) { +pure func ackPure(m integer, n integer) (result integer) { return m == 0? n + 1: n == 0 ? ackPure(m-1, 1) : ackPure(m-1, ackPure(m, n-1)) } ghost +requires len(s) <= math.MaxInt64 requires forall i int :: 0 <= i && i < len(s) ==> acc(&s[i], _) decreases len(s) pure func SumPure(ghost s ghost []int) int { diff --git a/src/test/resources/regressions/features/trusted/trusted-functions.gobra b/src/test/resources/regressions/features/trusted/trusted-functions.gobra index 0009609fc..275f0ab60 100644 --- a/src/test/resources/regressions/features/trusted/trusted-functions.gobra +++ b/src/test/resources/regressions/features/trusted/trusted-functions.gobra @@ -1,6 +1,10 @@ // Any copyright is dedicated to the Public Domain. // http://creativecommons.org/publicdomain/zero/1.0/ +// `positiveSumWithoutTrusted` relies on `x > 0 && y > 0 ==> x + y > 0`, which no longer holds under +// the sound bounded-integer encoding because `x + y` may overflow. Restore the previous unbounded- +// integer encoding, under which this test verifies as intended. +// ##(--unboundedIntegers) package pkg; requires x > 0 && y > 0 diff --git a/src/test/resources/regressions/features/wands/ghost-list.gobra b/src/test/resources/regressions/features/wands/ghost-list.gobra index 3a5e31d0c..fbb58f206 100644 --- a/src/test/resources/regressions/features/wands/ghost-list.gobra +++ b/src/test/resources/regressions/features/wands/ghost-list.gobra @@ -2,6 +2,8 @@ // http://creativecommons.org/publicdomain/zero/1.0/ package list +import "math" + // this testcase demonstrates (in comparison to list.gobra) how `Append` can be made ghost. Since this function // modifies state, this testcase requires the use of ghost pointers and ghost fields. @@ -25,6 +27,9 @@ pure func elems(start gpointer[List]) (res seq[int]) { ghost decreases requires l1.Mem() && l2.Mem() && l2 != nil +// bound the traversal counter: index + 1 must stay exact so each packaged wand +// instance matches the one applied at index - 1 +requires len(elems(l1)) < math.MaxInt64 ensures l1.Mem() && elems(l1) == old(elems(l1) ++ elems(l2)) func (l1 gpointer[List]) Append(l2 gpointer[List]) { unfold l1.Mem() @@ -41,7 +46,7 @@ func (l1 gpointer[List]) Append(l2 gpointer[List]) { } invariant tmp.Mem() --* (l1.Mem() && elems(l1) == old((elems(l1))[:index] ++ old[#lhs](elems(tmp)))) - invariant index >= 0 + invariant index >= 0 && index <= len(old(elems(l1))) invariant tmp.Mem() && elems(tmp) == old(elems(l1))[index:] decreases tmp.Mem() for (unfolding tmp.Mem() in tmp.next != nil) { diff --git a/src/test/resources/regressions/features/wands/list.gobra b/src/test/resources/regressions/features/wands/list.gobra index fbf2bc880..147ce95ad 100644 --- a/src/test/resources/regressions/features/wands/list.gobra +++ b/src/test/resources/regressions/features/wands/list.gobra @@ -2,6 +2,8 @@ // http://creativecommons.org/publicdomain/zero/1.0/ package list +import "math" + type List struct { val int next *List @@ -19,6 +21,9 @@ pure func elems(start *List) (res seq[int]) { } requires l1.Mem() && l2.Mem() && l2 != nil +// bound the traversal counter: index + 1 must stay exact so each packaged wand +// instance matches the one applied at index - 1 +requires len(elems(l1)) < math.MaxInt64 ensures l1.Mem() && elems(l1) == old(elems(l1) ++ elems(l2)) func (l1 *List) Append(l2 *List) { unfold l1.Mem() @@ -35,7 +40,7 @@ func (l1 *List) Append(l2 *List) { } invariant tmp.Mem() --* (l1.Mem() && elems(l1) == old((elems(l1))[:index] ++ old[#lhs](elems(tmp)))) - invariant index >= 0 + invariant index >= 0 && index <= len(old(elems(l1))) invariant tmp.Mem() && elems(tmp) == old(elems(l1))[index:] for (unfolding tmp.Mem() in tmp.next != nil) { unfold tmp.Mem() diff --git a/src/test/resources/regressions/features/while1.gobra b/src/test/resources/regressions/features/while1.gobra index 1ce630b3b..4530ceef6 100644 --- a/src/test/resources/regressions/features/while1.gobra +++ b/src/test/resources/regressions/features/while1.gobra @@ -6,11 +6,12 @@ package pkg; requires x >= 0; +requires x < 100000 && -100000 < y && y < 100000; // bounds so that i*y and z += y provably do not overflow ensures z == x*y; func test(x, y int) (z int) { + invariant 0 <= i && i <= x; // lower bound needed so i*y and z += y are provably exact invariant z == i*y; - invariant i <= x; for i := 0; i < x; i += 1 { z += y; }; diff --git a/src/test/resources/regressions/issues/000024.gobra b/src/test/resources/regressions/issues/000024.gobra index 2e00dd8dd..b69bd9a68 100644 --- a/src/test/resources/regressions/issues/000024.gobra +++ b/src/test/resources/regressions/issues/000024.gobra @@ -8,6 +8,7 @@ type Cell struct { b int; }; +requires c.a < 9223372036854775807; // bound so that x+1 provably does not overflow func test1(c Cell) () { q@ := c x := q.a; diff --git a/src/test/resources/regressions/issues/000180.gobra b/src/test/resources/regressions/issues/000180.gobra index f9aa83141..456b56782 100644 --- a/src/test/resources/regressions/issues/000180.gobra +++ b/src/test/resources/regressions/issues/000180.gobra @@ -3,32 +3,34 @@ package pkg +import "math" + type Tree struct {} // res in the following postconditions should correctly be translated to `result` in the Viper file ghost ensures res == seq[int] { } -ensures forall i int :: (0 <= i && i + 1 < len(res) ==> res[i] < res[i + 1]) +ensures forall i int :: (0 <= i && i < math.MaxInt64 && i + 1 < len(res) ==> res[i] < res[i + 1]) pure func (t *Tree) test1() (res seq[int]) { return seq[int] { } } ghost ensures res == seq[int] { } -ensures forall i int :: (0 <= i && i + 1 < len(res) ==> res[i] < res[i + 1]) +ensures forall i int :: (0 <= i && i < math.MaxInt64 && i + 1 < len(res) ==> res[i] < res[i + 1]) pure func test2() (res seq[int]) { return seq[int] { } } ensures res == seq[int] { } -ensures forall i int :: (0 <= i && i + 1 < len(res) ==> res[i] < res[i + 1]) +ensures forall i int :: (0 <= i && i < math.MaxInt64 && i + 1 < len(res) ==> res[i] < res[i + 1]) pure func (t *Tree) test3() (ghost res seq[int]) { return seq[int] { } } ensures res == seq[int] { } -ensures forall i int :: (0 <= i && i + 1 < len(res) ==> res[i] < res[i + 1]) +ensures forall i int :: (0 <= i && i < math.MaxInt64 && i + 1 < len(res) ==> res[i] < res[i + 1]) pure func test4() (ghost res seq[int]) { return seq[int] { } } diff --git a/src/test/resources/regressions/issues/000182.gobra b/src/test/resources/regressions/issues/000182.gobra index afc239f15..f898840ad 100644 --- a/src/test/resources/regressions/issues/000182.gobra +++ b/src/test/resources/regressions/issues/000182.gobra @@ -3,6 +3,8 @@ package pkg +import "math" + type Tree struct { root *node } @@ -21,8 +23,8 @@ pred (n *node) tree() { acc(&n.value) && acc(&n.left) && acc(&n.right) && (n.left != nil ==> n.left.tree()) && (n.right != nil ==> n.right.tree()) && - (n.left != nil ==> forall i int :: (0 <= i && i + 1 < len(n.left.orderedValues()) ==> (n.left.orderedValues())[i] < n.value)) && - (n.right != nil ==> forall i int :: (0 <= i && i + 1 < len(n.right.orderedValues()) ==> (n.right.orderedValues())[i] > n.value)) + (n.left != nil ==> forall i int :: (0 <= i && i < math.MaxInt64 && i < len(n.left.orderedValues()) && i + 1 < len(n.left.orderedValues()) ==> (n.left.orderedValues())[i] < n.value)) && + (n.right != nil ==> forall i int :: (0 <= i && i < math.MaxInt64 && i < len(n.right.orderedValues()) && i + 1 < len(n.right.orderedValues()) ==> (n.right.orderedValues())[i] > n.value)) } ghost diff --git a/src/test/resources/regressions/issues/000659.gobra b/src/test/resources/regressions/issues/000659.gobra index 1f1785578..de0668da9 100644 --- a/src/test/resources/regressions/issues/000659.gobra +++ b/src/test/resources/regressions/issues/000659.gobra @@ -1,6 +1,10 @@ // Any copyright is dedicated to the Public Domain. // http://creativecommons.org/publicdomain/zero/1.0/ +// The quantified permission `forall i int :: acc(s.nodes[i])` is no longer provably injective +// under the sound bounded-integer encoding, so the fold fails before the expected assertion. +// Restore the previous unbounded-integer encoding, under which this program verifies as intended. +// ##(--unboundedIntegers) package issue659 type Node struct { diff --git a/src/test/resources/regressions/issues/000705.gobra b/src/test/resources/regressions/issues/000705.gobra index 1912c34c1..806d0975f 100644 --- a/src/test/resources/regressions/issues/000705.gobra +++ b/src/test/resources/regressions/issues/000705.gobra @@ -3,6 +3,8 @@ package issue000705 +import "math" + // adapted from Viper tutorial on magic wands type ListEntry struct { @@ -23,6 +25,7 @@ pure func (l *ListEntry) Elems() (res seq[int]) { } requires l1.List() && l2.List() && l2 != nil +requires len(l1.Elems()) <= math.MaxInt64 ensures l1.List() && l1.Elems() == old(l1.Elems() ++ l2.Elems()) func (l1 *ListEntry) append(l2 *ListEntry) { unfold l1.List() @@ -46,6 +49,7 @@ func (l1 *ListEntry) append(l2 *ListEntry) { } } +requires len(elems) <= math.MaxInt64 requires tmp.List() && /*acc(l1) && l1.next == tmp &&*/ tmp.Elems() == elems[1:] requires tmp.List() --* (l1.List() && l1.Elems() == elems[:1] ++ old[#lhs](tmp.Elems())) ensures 0 <= newIndex diff --git a/src/test/resources/same_package/pkg_init/byte/byte.go b/src/test/resources/same_package/pkg_init/byte/byte.go index 904e390bf..cf65610cc 100644 --- a/src/test/resources/same_package/pkg_init/byte/byte.go +++ b/src/test/resources/same_package/pkg_init/byte/byte.go @@ -13,9 +13,9 @@ var byteCache /*@@@*/ [256]*Byte func init() { // @ invariant 0 <= i && i <= 256 && acc(&byteCache) - // @ invariant (forall j, k int :: 0 <= j && j < k && k < i ==> + // @ invariant (forall j, k integer :: 0 <= j && j < k && k < integer(i) ==> // @ byteCache[j] != byteCache[k]) - // @ invariant (forall j int :: 0 <= j && j < i ==> + // @ invariant (forall j integer :: 0 <= j && j < integer(i) ==> // @ acc(byteCache[j]) && byteCache[j].value == byte(j)) // @ decreases 256 - i for i := 0; i < 256; i++ { @@ -34,17 +34,17 @@ func alloc(val byte) (res *Byte) { } // @ pure -// @ requires acc(b.Mem(), _) +// @ requires b.Mem() // @ decreases func (b *Byte) ByteValue() byte { - return /*@ unfolding acc(b.Mem(), _) in @*/ b.value + return /*@ unfolding b.Mem() in @*/ b.value } // @ ensures acc(res.Mem(), _) // @ ensures res.ByteValue() == val // @ decreases func ToVal(val byte) (res *Byte) { - // @ assume 0 <= val && val <= 255 + // @ assert 0 <= val && val <= 255 // @ openDupPkgInv // @ unfold acc(StaticInv(), _) res = byteCache[val] diff --git a/src/test/resources/same_package/pkg_init/byte/byte_spec.gobra b/src/test/resources/same_package/pkg_init/byte/byte_spec.gobra index 0dada8449..df30aa4a8 100644 --- a/src/test/resources/same_package/pkg_init/byte/byte_spec.gobra +++ b/src/test/resources/same_package/pkg_init/byte/byte_spec.gobra @@ -5,12 +5,10 @@ package byte pred StaticInv() { acc(&byteCache, _) && - // for now, we need the redundant constraints below: 0 <= i && i <= 255 - // for injectivity checks: - (forall j, k byte :: 0 <= j && j < k && k <= 255 ==> + (forall j, k integer :: 0 <= j && j < k && k <= 255 ==> byteCache[j] != byteCache[k]) && - (forall i byte :: 0 <= i && i <= 255 ==> - acc(byteCache[i], _) && byteCache[i].value == i) + (forall i integer :: 0 <= i && i <= 255 ==> + acc(byteCache[i], _) && byteCache[i].value == byte(i)) } pred (b *Byte) Mem() { diff --git a/src/test/resources/same_package/pkg_init/concfib/fib.go b/src/test/resources/same_package/pkg_init/concfib/fib.go index e2479b312..513eaeef5 100644 --- a/src/test/resources/same_package/pkg_init/concfib/fib.go +++ b/src/test/resources/same_package/pkg_init/concfib/fib.go @@ -18,7 +18,7 @@ func init() { // @ fold acc(StaticInv(), _) } -// @ requires 0 <= n +// @ requires 0 <= n && FibFits(n) // @ ensures res == FibSpec(n) // termination cannot be proven due to calls to Lock() func FibV1(n int) (res int) { @@ -33,6 +33,7 @@ func FibV1(n int) (res int) { } // @ fold lockInv{}() lock.Unlock() + // @ assert FibSpec(n) == FibSpec(n-1) + FibSpec(n-2) v := FibV1(n-1) + FibV1(n-2) lock.Lock() // @ unfold lockInv{}() @@ -42,7 +43,7 @@ func FibV1(n int) (res int) { return v } -// @ requires 0 <= n +// @ requires 0 <= n && FibFits(n) // @ ensures res == FibSpec(n) // termination cannot be proven due to calls to Lock() func FibV2(n int) (res int) { @@ -54,7 +55,7 @@ func FibV2(n int) (res int) { return v } -// @ requires 0 <= n +// @ requires 0 <= n && FibFits(n) // @ preserves lockInv{}() // @ ensures res == FibSpec(n) // @ decreases n @@ -65,6 +66,7 @@ func fibImpl(n int) (res int) { return v } // @ fold lockInv{}() + // @ assert FibSpec(n) == FibSpec(n-1) + FibSpec(n-2) v := fibImpl(n-1) + fibImpl(n-2) // @ unfold lockInv{}() cache[n] = v diff --git a/src/test/resources/same_package/pkg_init/concfib/fib_spec.gobra b/src/test/resources/same_package/pkg_init/concfib/fib_spec.gobra index 36ed9f5e2..293695143 100644 --- a/src/test/resources/same_package/pkg_init/concfib/fib_spec.gobra +++ b/src/test/resources/same_package/pkg_init/concfib/fib_spec.gobra @@ -3,21 +3,34 @@ package concfib +import "math" + ghost pure requires 0 <= n +ensures 1 <= res decreases n -func FibSpec(n int) int { +func FibSpec(n integer) (res integer) { return n <= 1 ? 1 : FibSpec(n-1) + FibSpec(n-2) } +// FibSpec grows beyond any bounded integer type, so callers of the concrete +// (cached) implementations must guarantee that the result still fits an int. +ghost +pure +requires 0 <= n +decreases +func FibFits(n integer) bool { + return FibSpec(n) <= math.MaxInt64 +} + pred lockInv() { acc(&cache, _) && acc(cache) && 0 elem domain(cache) && 1 elem domain(cache) && - forall i int :: { cache[i] }{ FibSpec(i) }{ i elem domain(cache) } i elem domain(cache) ==> - 0 <= i && cache[i] == FibSpec(i) + forall i int :: { cache[i] }{ FibSpec(i) }{ i elem domain(cache) } 0 <= i && i <= math.MaxInt64 && i elem domain(cache) ==> + cache[i] == FibSpec(i) } pred StaticInv() { diff --git a/src/test/resources/same_package/pkg_init/fib/fib.go b/src/test/resources/same_package/pkg_init/fib/fib.go index 93111550f..0f13d480b 100644 --- a/src/test/resources/same_package/pkg_init/fib/fib.go +++ b/src/test/resources/same_package/pkg_init/fib/fib.go @@ -13,7 +13,7 @@ func init() { // @ fold StaticInv() } -// @ requires 0 <= n +// @ requires 0 <= n && FibFits(n) // @ preserves StaticInv() // @ ensures res == FibSpec(n) // @ decreases n @@ -24,6 +24,7 @@ func Fib(n int) (res int) { return v } // @ fold StaticInv() + // @ assert FibSpec(n) == FibSpec(n-1) + FibSpec(n-2) v := Fib(n-1) + Fib(n-2) // @ unfold StaticInv() cache[n] = v diff --git a/src/test/resources/same_package/pkg_init/fib/fib_spec.gobra b/src/test/resources/same_package/pkg_init/fib/fib_spec.gobra index f0d7a44cc..deb3281b3 100644 --- a/src/test/resources/same_package/pkg_init/fib/fib_spec.gobra +++ b/src/test/resources/same_package/pkg_init/fib/fib_spec.gobra @@ -3,19 +3,32 @@ package fib +import "math" + ghost pure requires 0 <= n +ensures 1 <= res decreases n -func FibSpec(n int) int { +func FibSpec(n integer) (res integer) { return n <= 1 ? 1 : FibSpec(n-1) + FibSpec(n-2) } +// FibSpec grows beyond any bounded integer type, so callers of the concrete +// (cached) implementation must guarantee that the result still fits an int. +ghost +pure +requires 0 <= n +decreases +func FibFits(n integer) bool { + return FibSpec(n) <= math.MaxInt64 +} + pred StaticInv() { acc(&cache, _) && acc(cache) && 0 elem domain(cache) && 1 elem domain(cache) && - forall i int :: { cache[i] }{ FibSpec(i) }{ i elem domain(cache) } i elem domain(cache) ==> - 0 <= i && cache[i] == FibSpec(i) -} \ No newline at end of file + forall i int :: { cache[i] }{ FibSpec(i) }{ i elem domain(cache) } 0 <= i && i <= math.MaxInt64 && i elem domain(cache) ==> + cache[i] == FibSpec(i) +} diff --git a/src/test/resources/same_package/pkg_init/import/main.go b/src/test/resources/same_package/pkg_init/import/main.go index a2664f09f..bf400586a 100644 --- a/src/test/resources/same_package/pkg_init/import/main.go +++ b/src/test/resources/same_package/pkg_init/import/main.go @@ -26,6 +26,10 @@ func init() { foo() // this one is not checked for mayInit, it is imported. + // @ assert fib.FibSpec(0) == 1 + // @ assert fib.FibSpec(1) == 1 + // @ assert fib.FibSpec(2) == 2 + // @ assert fib.FibSpec(3) == 3 y := fib.Fib(3) // also, the call above requires fib.StaticInv(). We can actually // perform this call because the runtime guarantes that the main thread of @@ -43,6 +47,10 @@ func init() { } func Test() { + // @ assert concfib.FibSpec(0) == 1 + // @ assert concfib.FibSpec(1) == 1 + // @ assert concfib.FibSpec(2) == 2 + // @ assert concfib.FibSpec(3) == 3 x := concfib.FibV1(3) // @ assert concfib.FibSpec(0) == 1 // @ assert concfib.FibSpec(1) == 1 @@ -88,6 +96,10 @@ type I interface { // @ requires fib.StaticInv() // @ decreases func main() { + // @ assert fib.FibSpec(0) == 1 + // @ assert fib.FibSpec(1) == 1 + // @ assert fib.FibSpec(2) == 2 + // @ assert fib.FibSpec(3) == 3 x := fib.Fib(3) // @ assert fib.FibSpec(0) == 1 // @ assert fib.FibSpec(1) == 1 diff --git a/src/test/resources/same_package/pkg_init/invallinstances/client.go b/src/test/resources/same_package/pkg_init/invallinstances/client.go index fea4b074f..3330df0de 100644 --- a/src/test/resources/same_package/pkg_init/invallinstances/client.go +++ b/src/test/resources/same_package/pkg_init/invallinstances/client.go @@ -25,7 +25,9 @@ type Client struct { name string } -// @ preserves PkgInv() +// @ requires PkgInv() +// @ requires HasIdHeadroom() +// @ ensures PkgInv() // @ ensures res.Inv() // @ ensures res.Allocated() // @ decreases diff --git a/src/test/resources/same_package/pkg_init/invallinstances/client_spec.gobra b/src/test/resources/same_package/pkg_init/invallinstances/client_spec.gobra index 8913f39ec..fea7bd72e 100644 --- a/src/test/resources/same_package/pkg_init/invallinstances/client_spec.gobra +++ b/src/test/resources/same_package/pkg_init/invallinstances/client_spec.gobra @@ -3,6 +3,18 @@ package invallinstances +import "math" + +// The id counter must have headroom left before another instance can be +// allocated; otherwise incrementing it would overflow. +ghost +requires acc(PkgInv(), _) +decreases +pure func HasIdHeadroom() bool { + return unfolding acc(PkgInv(), _) in + ids < math.MaxInt64 +} + pred PkgInv() { acc(&ids) && acc(&allocs) && diff --git a/src/test/resources/stats_collector/pkg2/pkg2.gobra b/src/test/resources/stats_collector/pkg2/pkg2.gobra index 621992783..8174d929b 100644 --- a/src/test/resources/stats_collector/pkg2/pkg2.gobra +++ b/src/test/resources/stats_collector/pkg2/pkg2.gobra @@ -16,7 +16,10 @@ pred (r *Rect) mem() { requires acc(r, perm(1, 2)) pure func (r *Rect) Size() bool { - return r.width > 0 && r.height > 0 + // The upper bounds let the verifier prove that computing the area does not + // overflow (with Gobra's sound bounded-integer semantics, `width * height` + // is only known exactly when the mathematical result is in range). + return r.width > 0 && r.height > 0 && r.width < 100 && r.height < 100 } requires acc(r, perm(1, 2)) && r.Size() @@ -46,13 +49,16 @@ pred (c *Circle) mem() { requires acc(c, perm(1, 2)) pure func (c *Circle) Size() bool { - return c.radius > 0 + // Upper bound for overflow-free area computation; see Rect.Size. + return c.radius > 0 && c.radius < 100 } requires acc(c, perm(1, 2)) && c.Size() ensures acc(c, perm(1, 2)) && ret > 0 func (c *Circle) Area() (ret int) { - return c.radius * c.radius * 3 + // constant-first grouping: (3 * r) * r keeps every intermediate bound linear for + // the verifier; (r * r) * 3 would need a nonlinear bound on the r*r intermediate + return 3 * c.radius * c.radius } (*Circle) implements pkg1.Shape { diff --git a/src/test/scala/viper/gobra/DetailedBenchmarkTests.scala b/src/test/scala/viper/gobra/DetailedBenchmarkTests.scala index 665b5bba1..7c4d108dc 100644 --- a/src/test/scala/viper/gobra/DetailedBenchmarkTests.scala +++ b/src/test/scala/viper/gobra/DetailedBenchmarkTests.scala @@ -11,13 +11,12 @@ import org.scalatest.DoNotDiscover import scalaz.EitherT import scalaz.Scalaz.futureInstance import viper.gobra.ast.internal.Program -import viper.gobra.ast.internal.transform.OverflowChecksTransform import viper.gobra.backend.BackendVerifier import viper.gobra.frontend.PackageResolver.{AbstractPackage, RegularPackage} import viper.gobra.frontend.Parser.ParseResult import viper.gobra.frontend.info.{Info, TypeInfo} import viper.gobra.frontend.{Desugar, Parser} -import viper.gobra.reporting.{AppliedInternalTransformsMessage, BackTranslator, VerifierError, VerifierResult} +import viper.gobra.reporting.{BackTranslator, VerifierError, VerifierResult} import viper.gobra.translator.Translator import scala.concurrent.Future @@ -124,17 +123,7 @@ class DetailedBenchmarkTests extends BenchmarkTests { }) private val internalTransforming = NextStep("internal transforming", desugaring, (program: Program) => { - assert(config.isDefined) - val c = config.get - assert(c.packageInfoInputMap.size == 1) - val pkgInfo = c.packageInfoInputMap.keys.head - if (c.checkOverflows) { - val result = OverflowChecksTransform.transform(program) - c.reporter report AppliedInternalTransformsMessage(c.packageInfoInputMap(pkgInfo).map(_.name), () => result) - Right(result) - } else { - Right(program) - } + Right(program) }) private val encoding = NextStep("Viper encoding", internalTransforming, (program: Program) => {