diff --git a/src/main/resources/stubs/encoding/binary/binary.gobra b/src/main/resources/stubs/encoding/binary/binary.gobra index b5c63d26b..d3f3b15a0 100644 --- a/src/main/resources/stubs/encoding/binary/binary.gobra +++ b/src/main/resources/stubs/encoding/binary/binary.gobra @@ -11,13 +11,16 @@ package binary // 16-, 32-, or 64-bit unsigned integers. type ByteOrder interface { requires acc(&b[0], _) && acc(&b[1], _) + decreases pure Uint16(b []byte) uint16 requires acc(&b[0], _) && acc(&b[1], _) && acc(&b[2], _) && acc(&b[3], _) + decreases pure Uint32(b []byte) uint32 requires acc(&b[0], _) && acc(&b[1], _) && acc(&b[2], _) && acc(&b[3], _) requires acc(&b[4], _) && acc(&b[5], _) && acc(&b[6], _) && acc(&b[7], _) + decreases pure Uint64(b []byte) uint64 requires acc(&b[0]) && acc(&b[1]) @@ -37,6 +40,7 @@ type ByteOrder interface { decreases PutUint64(b []byte, uint64) + decreases pure String() string } diff --git a/src/main/scala/viper/gobra/ast/frontend/Ast.scala b/src/main/scala/viper/gobra/ast/frontend/Ast.scala index 9d3e73eec..1c6ebe75c 100644 --- a/src/main/scala/viper/gobra/ast/frontend/Ast.scala +++ b/src/main/scala/viper/gobra/ast/frontend/Ast.scala @@ -157,9 +157,19 @@ sealed trait PDependentDef extends PNode { sealed trait PCodeRoot extends PNode sealed trait PCodeRootWithResult extends PCodeRoot { + def args: Vector[PParameter] def result: PResult } +/** + * A code root that carries a specification: a function or method declaration, a closure declaration, or the + * signature of an interface method. Notably, this excludes `PMethodImplementationProof`, which inherits the + * specification of the interface method that it proves and thus does not have one of its own. + */ +sealed trait PCodeRootWithSpec extends PCodeRootWithResult { + def spec: PFunctionSpec +} + case class PConstDecl(specs: Vector[PConstSpec]) extends PActualMember with PActualStatement with PGhostifiableStatement with PGhostifiableMember with PDeclaration case class PConstSpec(typ: Option[PType], right: Vector[PExpression], left: Vector[PDefLikeId]) extends PNode @@ -183,7 +193,7 @@ case class PFunctionDecl( result: PResult, spec: PFunctionSpec, body: Option[(PBodyParameterInfo, PBlock)] - ) extends PFunctionOrClosureDecl with PFunctionOrMethodDecl with PCodeRootWithResult with PWithBody with PGhostifiableMember + ) extends PFunctionOrClosureDecl with PFunctionOrMethodDecl with PCodeRootWithSpec with PWithBody with PGhostifiableMember case class PMethodDecl( id: PIdnDef, @@ -192,7 +202,7 @@ case class PMethodDecl( result: PResult, spec: PFunctionSpec, body: Option[(PBodyParameterInfo, PBlock)] - ) extends PFunctionOrMethodDecl with PDependentDef with PScope with PCodeRootWithResult with PWithBody with PGhostifiableMember + ) extends PFunctionOrMethodDecl with PDependentDef with PScope with PCodeRootWithSpec with PWithBody with PGhostifiableMember sealed trait PTypeDecl extends PActualMember with PActualStatement with PGhostifiableStatement with PGhostifiableMember with PDeclaration { @@ -468,7 +478,7 @@ case class PFunctionLit(id: Option[PIdnDef], decl: PClosureDecl) extends PLitera case class PClosureDecl(args: Vector[PParameter], result: PResult, spec: PFunctionSpec, - body: Option[(PBodyParameterInfo, PBlock)]) extends PFunctionOrClosureDecl with PCodeRootWithResult with PActualMisc + body: Option[(PBodyParameterInfo, PBlock)]) extends PFunctionOrClosureDecl with PCodeRootWithSpec with PWithBody with PActualMisc case class PClosureSpecInstance(func: PNameOrDot, params: Vector[PKeyedElement]) extends PGhostMisc { require(params.forall(p => p.exp.isInstanceOf[PExpCompositeVal])) @@ -785,7 +795,7 @@ case class PInterfaceName(typ: PTypeName) extends PInterfaceClause // Felix: I see `isGhost` as part of the declaration and not as port of the specification. // In the past, I usually created some ghost wrapper for these cases, but I wanted to get rid of them in the future. -case class PMethodSig(id: PIdnDef, args: Vector[PParameter], result: PResult, spec: PFunctionSpec, isGhost: Boolean) extends PInterfaceClause with PDependentDef with PScope with PCodeRootWithResult +case class PMethodSig(id: PIdnDef, args: Vector[PParameter], result: PResult, spec: PFunctionSpec, isGhost: Boolean) extends PInterfaceClause with PDependentDef with PScope with PCodeRootWithSpec /** * Identifiers diff --git a/src/main/scala/viper/gobra/frontend/ParseTreeTranslator.scala b/src/main/scala/viper/gobra/frontend/ParseTreeTranslator.scala index b6e52706f..0141ae4fb 100644 --- a/src/main/scala/viper/gobra/frontend/ParseTreeTranslator.scala +++ b/src/main/scala/viper/gobra/frontend/ParseTreeTranslator.scala @@ -939,7 +939,7 @@ class ParseTreeTranslator(pom: PositionManager, source: Source, specOnly : Boole isAtomic = ctx.atomic, opensInvs = ctx.opensInv, mayBeUsedInInit = ctx.mayInit, - ) + ).at(ctx) } /** diff --git a/src/main/scala/viper/gobra/frontend/info/implementation/typing/ExprTyping.scala b/src/main/scala/viper/gobra/frontend/info/implementation/typing/ExprTyping.scala index 65895b96e..8ab1693ad 100644 --- a/src/main/scala/viper/gobra/frontend/info/implementation/typing/ExprTyping.scala +++ b/src/main/scala/viper/gobra/frontend/info/implementation/typing/ExprTyping.scala @@ -247,7 +247,8 @@ trait ExprTyping extends BaseTyping { this: TypeInfoImpl => capturedLocalVariables(f.decl).flatMap(v => addressable.errors(enclosingExpr(v).get)(v)) ++ wellDefVariadicArgs(f.args) ++ f.id.fold(noMessages)(id => wellDefID(id).out) ++ - error(f, "Opaque function literals are not yet supported.", f.spec.isOpaque) + error(f, "Opaque function literals are not yet supported.", f.spec.isOpaque) ++ + wellDefIfPureClosure(f) case n: PInvoke => val mayInit = isEnclosingMayInit(n) diff --git a/src/main/scala/viper/gobra/frontend/info/implementation/typing/MemberTyping.scala b/src/main/scala/viper/gobra/frontend/info/implementation/typing/MemberTyping.scala index f6d937228..271bb22e6 100644 --- a/src/main/scala/viper/gobra/frontend/info/implementation/typing/MemberTyping.scala +++ b/src/main/scala/viper/gobra/frontend/info/implementation/typing/MemberTyping.scala @@ -26,16 +26,14 @@ trait MemberTyping extends BaseTyping { this: TypeInfoImpl => wellDefIfPureFunction(n) ++ wellDefIfInitBlock(n) ++ wellDefIfMain(n) ++ - wellFoundedIfNeeded(n) ++ - atomicMemberIsWellFormed(n) ++ - noConditionalMeasureIfGhostOrPure(n) + wellFoundedIfGhost(n) ++ + atomicMemberIsWellFormed(n) case m: PMethodDecl => wellDefVariadicArgs(m.args) ++ isReceiverType.errors(miscType(m.receiver))(member) ++ wellDefIfPureMethod(m) ++ - wellFoundedIfNeeded(m) ++ - atomicMemberIsWellFormed(m) ++ - noConditionalMeasureIfGhostOrPure(m) + wellFoundedIfGhost(m) ++ + atomicMemberIsWellFormed(m) case b: PConstDecl => b.specs.flatMap(wellDefConstSpec) case g: PVarDecl if isGlobalVarDeclaration(g) => diff --git a/src/main/scala/viper/gobra/frontend/info/implementation/typing/TerminationTyping.scala b/src/main/scala/viper/gobra/frontend/info/implementation/typing/TerminationTyping.scala index fe7cf1ee5..fd6e58288 100644 --- a/src/main/scala/viper/gobra/frontend/info/implementation/typing/TerminationTyping.scala +++ b/src/main/scala/viper/gobra/frontend/info/implementation/typing/TerminationTyping.scala @@ -6,8 +6,8 @@ package viper.gobra.frontend.info.implementation.typing -import org.bitbucket.inkytonik.kiama.util.Messaging.{Messages, error} -import viper.gobra.ast.frontend.{PTerminationMeasure, PTupleTerminationMeasure, PWildcardMeasure} +import org.bitbucket.inkytonik.kiama.util.Messaging.{Messages, error, noMessages} +import viper.gobra.ast.frontend.{PFunctionSpec, PNode, PTerminationMeasure, PTupleTerminationMeasure, PWildcardMeasure} import viper.gobra.frontend.info.implementation.TypeInfoImpl /** @@ -39,6 +39,17 @@ trait TerminationTyping extends BaseTyping { this: TypeInfoImpl => case _ => false } + /** + * The semantics of wildcard termination measures in interface method specifications is not clear. + * Thus, they are rejected. + */ + private[typing] def noWildcardMeasureErrors(measures: Vector[PTerminationMeasure]): Messages = + measures.flatMap { + case w: PWildcardMeasure => + error(w, "Wildcard termination measures are not allowed in the specifications of interface methods.") + case _ => noMessages + } + private[typing] def noConditionalMeasureErrors(measures: Vector[PTerminationMeasure]): Messages = measures.flatMap { m => error(m, @@ -46,6 +57,25 @@ trait TerminationTyping extends BaseTyping { this: TypeInfoImpl => isConditional(m)) } + /** + * Checks that `spec` guarantees that the member it belongs to terminates on every call, as Gobra + * requires of all ghost and pure members: functions, methods, and interface method signatures alike. + * The missing-measure error is reported on `node`, which should be the member or signature that + * `spec` specifies. + * + * Ghost and pure members are held to a stricter rule than `measuresGuaranteeTermination`: they may + * not carry a conditional measure at all because we do not check that the conditions of conditional + * measures cover all inputs. The two problems are therefore reported separately, the missing + * measure on `node` and each conditional measure on the measure itself, and only the former + * is subject to `disableCheckTerminationPureFns`. + */ + private[typing] def mustTerminateErrors(node: PNode, spec: PFunctionSpec): Messages = { + val missingMeasureError = error(node, + "Ghost and pure functions, methods, and interface methods must have termination measures, but none was found.", + !config.disableCheckTerminationPureFns && spec.terminationMeasures.isEmpty) + missingMeasureError ++ noConditionalMeasureErrors(spec.terminationMeasures) + } + private[typing] def hasSameMeasureType(measures: Vector[PTerminationMeasure]): Boolean = { val tupleMeasureTypes = measures .collect { case ttm: PTupleTerminationMeasure => ttm } diff --git a/src/main/scala/viper/gobra/frontend/info/implementation/typing/TypeTyping.scala b/src/main/scala/viper/gobra/frontend/info/implementation/typing/TypeTyping.scala index 167c3ed3c..57dab73f3 100644 --- a/src/main/scala/viper/gobra/frontend/info/implementation/typing/TypeTyping.scala +++ b/src/main/scala/viper/gobra/frontend/info/implementation/typing/TypeTyping.scala @@ -75,28 +75,9 @@ trait TypeTyping extends BaseTyping { this: TypeInfoImpl => } } - // The semantics of wildcard termination measures in interface method specifications is not clear. - // Thus, they are rejected. - val sigsWithWildcardMeasuresErrors = t.methSpecs.flatMap { sig => - sig.spec.terminationMeasures.flatMap { - case w: PWildcardMeasure => - error(w, s"Wildcard termination measures are not allowed in the specifications of interface methods.") - case _ => noMessages - } - } - val sigsWithConditionalMeasuresErrors = t.methSpecs.flatMap { sig => - if (sig.isGhost || sig.spec.isPure) - noConditionalMeasureErrors(sig.spec.terminationMeasures) - else noMessages - } - val interfaceMethodsNotAtomic = t.methSpecs.flatMap { sig => - error(sig, s"Interface methods cannot be marked as atomic.", sig.spec.isAtomic) - } methodSet.errors(t) ++ error(t, "Interface declaration contains methods annotated with 'mayInit'.", methodsContainMayInit) ++ - interfaceMethodsNotAtomic ++ - sigsWithWildcardMeasuresErrors ++ - sigsWithConditionalMeasuresErrors ++ + t.methSpecs.flatMap(wellDefMethodSig) ++ containsRedeclarations(t) // temporary check } else { isRecursiveInterface diff --git a/src/main/scala/viper/gobra/frontend/info/implementation/typing/ghost/GhostMemberTyping.scala b/src/main/scala/viper/gobra/frontend/info/implementation/typing/ghost/GhostMemberTyping.scala index 3e06f0afc..d8b3f2ccc 100644 --- a/src/main/scala/viper/gobra/frontend/info/implementation/typing/ghost/GhostMemberTyping.scala +++ b/src/main/scala/viper/gobra/frontend/info/implementation/typing/ghost/GhostMemberTyping.scala @@ -7,7 +7,7 @@ package viper.gobra.frontend.info.implementation.typing.ghost import org.bitbucket.inkytonik.kiama.util.Messaging.{Messages, error, noMessages} -import viper.gobra.ast.frontend.{PBlock, PCodeRootWithResult, PExplicitGhostMember, PFPredicateDecl, PFunctionDecl, PFunctionSpec, PGhostMember, PIdnUse, PImplementationProof, PMPredicateDecl, PMember, PMethodDecl, PMethodImplementationProof, PParameter, PPreserves, PReturn, PVariadicType, PWithBody} +import viper.gobra.ast.frontend.{PBlock, PCodeRootWithResult, PCodeRootWithSpec, PExplicitGhostMember, PFPredicateDecl, PFunctionDecl, PFunctionLit, PFunctionSpec, PGhostMember, PIdnUse, PImplementationProof, PMPredicateDecl, PMember, PMethodDecl, PMethodImplementationProof, PMethodSig, PParameter, PPreserves, PReturn, PVariadicType, PWithBody} import viper.gobra.frontend.info.base.SymbolTable.{MPredicateSpec, MethodImpl, MethodSpec} import viper.gobra.frontend.info.base.Type.{InterfaceT, Type, UnknownType} import viper.gobra.frontend.info.implementation.TypeInfoImpl @@ -28,30 +28,15 @@ trait GhostMemberTyping extends BaseTyping { this: TypeInfoImpl => nonVariadicArguments(args) } - private[typing] def wellFoundedIfNeeded(member: PMember): Messages = { + // Ghost functions and methods must be guaranteed to terminate. Pure members, whether ghost or not, are + // checked uniformly in `wellDefPureSpec`, which is why they are excluded here. + private[typing] def wellFoundedIfGhost(member: PMember): Messages = { val spec = member match { case m: PMethodDecl => m.spec case f: PFunctionDecl => f.spec case _ => Violation.violation("Unexpected member type") } - val hasMeasureIfNeeded = - if (spec.isPure || isEnclosingGhost(member)) - config.disableCheckTerminationPureFns || spec.terminationMeasures.nonEmpty - else - true - val needsMeasureError = - error(member, "All pure or ghost functions and methods must have termination measures, but none was found for this member.", !hasMeasureIfNeeded) - needsMeasureError - } - - private[typing] def noConditionalMeasureIfGhostOrPure(member: PMember): Messages = { - val spec = member match { - case m: PMethodDecl => m.spec - case f: PFunctionDecl => f.spec - case _ => return noMessages - } - if (spec.isPure || isEnclosingGhost(member)) - noConditionalMeasureErrors(spec.terminationMeasures) + if (isEnclosingGhost(member) && !spec.isPure) mustTerminateErrors(member, spec) else noMessages } @@ -60,11 +45,8 @@ trait GhostMemberTyping extends BaseTyping { this: TypeInfoImpl => private[typing] def wellDefIfPureMethod(member: PMethodDecl): Messages = { if (member.spec.isPure) { - isSingleResultArg(member) ++ + wellDefPureSpec(member) ++ isSinglePureReturnExpr(member) ++ - isPurePostcondition(member.spec) ++ - pureMembersCannotHavePreserves(member.spec) ++ - nonVariadicArguments(member.args) ++ error(member, pureFunctionsDoNotNeedMayInitMsg, member.spec.mayBeUsedInInit) } else noMessages } @@ -87,15 +69,48 @@ trait GhostMemberTyping extends BaseTyping { this: TypeInfoImpl => private[typing] def wellDefIfPureFunction(member: PFunctionDecl): Messages = { if (member.spec.isPure) { - isSingleResultArg(member) ++ + wellDefPureSpec(member) ++ isSinglePureReturnExpr(member) ++ - isPurePostcondition(member.spec) ++ - pureMembersCannotHavePreserves(member.spec) ++ - nonVariadicArguments(member.args) ++ error(member, pureFunctionsDoNotNeedMayInitMsg, member.spec.mayBeUsedInInit) } else noMessages } + private[typing] def wellDefIfPureClosure(lit: PFunctionLit): Messages = { + if (lit.spec.isPure) { + wellDefPureSpec(lit.decl) ++ + isSinglePureReturnExpr(lit.decl) ++ + error(lit, pureFunctionsDoNotNeedMayInitMsg, lit.spec.mayBeUsedInInit) + } else noMessages + } + + /** + * Well-definedness checks that every pure member must satisfy, whether it is a pure function, a pure method, + * a pure closure or the signature of a pure interface method: exactly one result, pure postconditions, no + * `preserves` clauses, non-variadic arguments, and a termination measure that guarantees termination. Checks + * specific to a particular kind of member (e.g., `mayInit`) are added by the callers. + */ + private[typing] def wellDefPureSpec(member: PCodeRootWithSpec): Messages = { + Violation.violation(member.spec.isPure, "wellDefPureSpec may only be called for a pure member.") + isSingleResultArg(member) ++ + isPurePostcondition(member.spec) ++ + pureMembersCannotHavePreserves(member.spec) ++ + nonVariadicArguments(member.args) ++ + mustTerminateErrors(member, member.spec) + } + + /** + * Well-definedness checks for the signature of an interface method. This is the counterpart of + * `wellDefActualMember` for the members declared by an interface, and imposes the same requirements on ghost + * and pure signatures as the ones imposed on ghost and pure implementations. + */ + private[typing] def wellDefMethodSig(sig: PMethodSig): Messages = { + error(sig, "Interface methods cannot be marked as atomic.", sig.spec.isAtomic) ++ + noWildcardMeasureErrors(sig.spec.terminationMeasures) ++ + (if (sig.spec.isPure) wellDefPureSpec(sig) + else if (sig.isGhost) mustTerminateErrors(sig, sig.spec) + else noMessages) + } + private[typing] def atomicMemberIsWellFormed(member: PMember): Messages = { val (bodyOpt, spec) = member match { case f: PFunctionDecl => (f.body, f.spec) diff --git a/src/test/resources/check_termination/ghost-and-pure-members-need-measures.gobra b/src/test/resources/check_termination/ghost-and-pure-members-need-measures.gobra new file mode 100644 index 000000000..84105060d --- /dev/null +++ b/src/test/resources/check_termination/ghost-and-pure-members-need-measures.gobra @@ -0,0 +1,99 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +package ghost_and_pure_members_need_measures + +// Ghost and pure members must be guaranteed to terminate on every call. Following issue #841, this +// includes the method signatures declared in an interface, which used to be exempt. Unlike the main +// regression test suite, the files in this directory are checked with `disableCheckTerminationPureFns` +// disabled. + +type T struct{ f int } + +// Type error, a pure function must have a termination measure. +//:: ExpectedOutput(type_error) +pure func missingMeasureFunc(a int) int { + return a +} + +// Type error, a pure method must have a termination measure. +//:: ExpectedOutput(type_error) +pure func (t T) missingMeasureMethod(a int) int { + return a +} + +// Type error, a ghost function must have a termination measure. +ghost +//:: ExpectedOutput(type_error) +func missingMeasureGhostFunc(a int) int { + return a +} + +// Type error, a ghost method must have a termination measure. +ghost +//:: ExpectedOutput(type_error) +func (t T) missingMeasureGhostMethod(a int) int { + return a +} + +type I interface { + // Type error, a pure interface method must have a termination measure. + //:: ExpectedOutput(type_error) + pure MissingMeasurePure(a int) int + + // Type error, a ghost interface method must have a termination measure. + //:: ExpectedOutput(type_error) + ghost MissingMeasureGhost(a int) + + // Type error, a conditional measure does not guarantee that a pure interface method terminates. + //:: ExpectedOutput(type_error) + decreases a if a >= 0 + pure ConditionalMeasurePure(a int) int + + // Type error, a conditional measure does not guarantee that a ghost interface method terminates. + ghost + //:: ExpectedOutput(type_error) + decreases a if a >= 0 + ConditionalMeasureGhost(a int) + + decreases + pure HasMeasurePure(a int) int + + ghost + decreases + HasMeasureGhost(a int) + + // An interface method that is neither ghost nor pure need not terminate. + NeedNotTerminate(a int) +} + +func closures() { + // Type error, a pure closure must have a termination measure. + //:: ExpectedOutput(type_error) + cl := pure func g() (x int) { + return 0 + } + _ = cl + + cl2 := decreases + pure func h() (x int) { + return 0 + } + _ = cl2 +} + +decreases +pure func withMeasureFunc(a int) int { + return a +} + +decreases +pure func (t T) withMeasureMethod(a int) int { + return a +} + +ghost +decreases +func withMeasureGhostFunc(a int) int { + return a +} diff --git a/src/test/resources/regressions/features/closures/closures-fail-pure.gobra b/src/test/resources/regressions/features/closures/closures-fail-pure.gobra new file mode 100644 index 000000000..40f11d8e7 --- /dev/null +++ b/src/test/resources/regressions/features/closures/closures-fail-pure.gobra @@ -0,0 +1,86 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +package closures_fail_pure + +// Pure closure literals must satisfy the same requirements as pure functions and pure methods. Note +// that the requirement of carrying a termination measure is gated behind +// `disableCheckTerminationPureFns`, which this test suite enables; that case is covered by the tests +// in `src/test/resources/check_termination` instead. + +func twoResults() { + // Type error, a pure member must have exactly one result parameter. + //:: ExpectedOutput(type_error) + cl := decreases + // Type error, the body of a pure member must be a single return of a pure expression. + //:: ExpectedOutput(type_error) + pure func g() (x int, y int) { + return 1, 2 + } + _ = cl +} + +func noResult() { + // Type error, a pure member must have exactly one result parameter. + //:: ExpectedOutput(type_error) + cl := decreases + // Type error, the body of a pure member must be a single return of a pure expression. + //:: ExpectedOutput(type_error) + pure func g() { + } + _ = cl +} + +func variadicArgs() { + cl := decreases + // Type error, a pure member cannot have variadic parameters. + //:: ExpectedOutput(type_error) + pure func g(a ...int) (x int) { + return 0 + } + _ = cl +} + +func preservesClause() { + // Type error, a pure member cannot have preserves clauses. + //:: ExpectedOutput(type_error) + cl := preserves true + decreases + pure func g() (x int) { + return 0 + } + _ = cl +} + +func impurePostcondition() { + y@ := 0 + // Type error, the postconditions of a pure member must be pure. + //:: ExpectedOutput(type_error) + cl := ensures acc(&y) + decreases + pure func g() (x int) { + return 0 + } + _ = cl +} + +func statementInBody() { + cl := decreases + // Type error, the body of a pure member must be a single return of a pure expression. + //:: ExpectedOutput(type_error) + pure func g() (x int) { + x = 0 + return x + } + _ = cl +} + +// A pure closure literal that satisfies all of the requirements above is accepted. +func wellFormed() { + cl := ensures res == 0 + decreases + pure func g() (res int) { + return 0 + } + assert cl() as g == 0 +} diff --git a/src/test/resources/regressions/features/interfaces/embeddedInterfaces-fail3.gobra b/src/test/resources/regressions/features/interfaces/embeddedInterfaces-fail3.gobra index a77a57d60..056c91cbc 100644 --- a/src/test/resources/regressions/features/interfaces/embeddedInterfaces-fail3.gobra +++ b/src/test/resources/regressions/features/interfaces/embeddedInterfaces-fail3.gobra @@ -10,14 +10,14 @@ type foo interface { } type bar interface { - pure g() + pure g() int } type test int func (x test) f() -func (x test) g() +func (x test) g() int //:: ExpectedOutput(type_error) test implements foo diff --git a/src/test/resources/regressions/features/opaque/opaque-closure-fail1.gobra b/src/test/resources/regressions/features/opaque/opaque-closure-fail1.gobra index 86f1f5272..0fa5b878c 100644 --- a/src/test/resources/regressions/features/opaque/opaque-closure-fail1.gobra +++ b/src/test/resources/regressions/features/opaque/opaque-closure-fail1.gobra @@ -7,11 +7,10 @@ func main() { x@ := 0 // Closure cannot be made opaque //:: ExpectedOutput(type_error) - c := preserves acc(&x) - ensures x == old(x) + n && m == x + c := requires acc(&x) + ensures m == x + n opaque pure func f(n int) (m int) { - x += n; - return x + return x + n } } diff --git a/src/test/resources/regressions/features/termination/interface-sig-measures-fail.gobra b/src/test/resources/regressions/features/termination/interface-sig-measures-fail.gobra new file mode 100644 index 000000000..2427a874b --- /dev/null +++ b/src/test/resources/regressions/features/termination/interface-sig-measures-fail.gobra @@ -0,0 +1,31 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +package interface_sig_measures_fail + +// The well-definedness checks that Gobra performs on a specification are reported on the +// specification itself. This used to crash the type checker for the specifications of interface +// method signatures, because those were the only `PFunctionSpec` nodes without a position. + +type I interface { + // Type error, a specification can contain at most one non-conditional termination measure. + //:: ExpectedOutput(type_error) + decreases + decreases n + M(n int) +} + +type J interface { + // Type error, all termination measures of a specification must have the same type. + //:: ExpectedOutput(type_error) + decreases n if n >= 0 + decreases b if b + N(n int, b bool) +} + +type K interface { + // Type error, wildcard termination measures are not allowed on interface methods. + //:: ExpectedOutput(type_error) + decreases _ + O(n int) +} diff --git a/src/test/resources/regressions/issues/000841.gobra b/src/test/resources/regressions/issues/000841.gobra new file mode 100644 index 000000000..74539cd68 --- /dev/null +++ b/src/test/resources/regressions/issues/000841.gobra @@ -0,0 +1,45 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +package issue000841 + +// Issue #841: pure method signatures declared in an interface must satisfy the same requirements as +// pure functions and pure methods. The requirement of carrying a termination measure is gated behind +// `disableCheckTerminationPureFns`, which this test suite enables; that case is covered by the tests +// in `src/test/resources/check_termination` instead. + +type J interface { + // Type error, a pure member must have exactly one result parameter. + //:: ExpectedOutput(type_error) + decreases + pure NoResult(a int) + + // Type error, a pure member must have exactly one result parameter. + //:: ExpectedOutput(type_error) + decreases + pure TwoResults(a int) (int, int) + + decreases + // Type error, a pure member cannot have variadic parameters. + //:: ExpectedOutput(type_error) + pure Variadic(a ...int) int + + // Type error, a pure member cannot have preserves clauses. + //:: ExpectedOutput(type_error) + preserves a > 0 + decreases + pure Preserves(a int) int + + // Type error, the postconditions of a pure member must be pure. + //:: ExpectedOutput(type_error) + ensures acc(x) + decreases + pure ImpurePostcondition(x *int) int +} + +// A pure method signature that satisfies all of the requirements above is accepted. +type K interface { + ensures res >= a + decreases + pure WellFormed(a int) (res int) +} diff --git a/src/test/scala/viper/gobra/GobraCheckTerminationTests.scala b/src/test/scala/viper/gobra/GobraCheckTerminationTests.scala new file mode 100644 index 000000000..f8464605b --- /dev/null +++ b/src/test/scala/viper/gobra/GobraCheckTerminationTests.scala @@ -0,0 +1,35 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2011-2026 ETH Zurich. + +package viper.gobra + +import org.bitbucket.inkytonik.kiama.util.Source +import viper.gobra.frontend.Config + +/** + * Runs the test files in `src/test/resources/check_termination` with `disableCheckTerminationPureFns` + * disabled, i.e., with the requirement that all ghost and pure members carry a termination measure. + * + * This suite exists because that requirement cannot be tested from the main regression test suite: + * [[GobraTests]] sets `disableCheckTerminationPureFns` to `true`, as adding termination measures to + * all of its test files is still pending work, and an in-file configuration (`// ##(...)`) cannot opt + * out of that setting. In-file configurations are merged into the base configuration by taking the + * disjunction of the boolean flags (see `Config.merge`), so they can only ever turn this flag on. + * + * Only test files about that requirement belong here. Everything else, including the requirements on + * ghost and pure members that hold irrespective of the flag, belongs to the main regression test + * suite, which is where a reader looks for it. + */ +class GobraCheckTerminationTests extends GobraTests { + val checkTerminationPropertyName = "GOBRATESTS_CHECK_TERMINATION_DIR" + + val checkTerminationDir: String = System.getProperty(checkTerminationPropertyName, "check_termination") + + override val testDirectories: Seq[String] = Vector(checkTerminationDir) + + override protected def getConfig(source: Source): Config = + super.getConfig(source).copy(disableCheckTerminationPureFns = false) +} diff --git a/src/test/scala/viper/gobra/GobraTests.scala b/src/test/scala/viper/gobra/GobraTests.scala index f64568540..a016bbccc 100644 --- a/src/test/scala/viper/gobra/GobraTests.scala +++ b/src/test/scala/viper/gobra/GobraTests.scala @@ -57,7 +57,9 @@ class GobraTests extends AbstractGobraTests with BeforeAndAfterAll { cacheParserAndTypeChecker = cacheParserAndTypeChecker, z3Exe = z3Exe, // termination checks in functions are currently disabled in the tests. This can be enabled in the future, - // but requires some work to add termination measures all over the test suite. + // but requires some work to add termination measures all over the test suite. Test files that are about + // that requirement belong to `GobraCheckTerminationTests`, which overrides this setting; an in-file + // configuration cannot, as `Config.merge` only ever turns boolean flags on. disableCheckTerminationPureFns = true, )