Basic Specifications

How can we differentiate between a correct and a faulty implementation? Before we can prove that a Go program is correct, we must first make it clear what it means for the program to be correct. In this section, we explain how to do this by associating a contract with each function in the program. The contract for a function has two main components: preconditions and postconditions: Preconditions describe under which conditions a function may be called. Postconditions describe what conditions hold whenever a function returns.

For example, we give a contract for the function Abs that computes the absolute value of a x int32. Mathematically speaking, Abs(x) should return \( |x| \). The postcondition res >= 0 && (res == x || res == -x) states that if x is negative, it must be negated, and otherwise the same value must be returned. We could test the expected result for some cases like Abs(-5) == 5, Abs(0) == 0, and Abs(1) == 1, but is this enough?

//@ ensures res >= 0 && (res == x || res == -x)
func Abs(x int32) (res int32) {
    if x < 0 {
        return -x
    } else {
        return x
    }
}

There is a subtle bug, which Gobra detects with overflow checking enabled:

ERROR Expression -x might cause integer overflow.

By two's complement arithmetic there is one more negative integer and negating the smallest int32 causes overflow: Abs(-2147483648) returns -2147483648 which would clearly violate the postcondition that the result is non-negative. We complete the contract for Abs with a precondition, such that we cannot call Abs for this value:

const MinInt32 = -2147483648  // -1<<32
//@ requires x != MinInt32
//@ ensures res >= 0 && (res == x || res == -x)
func Abs(x int32) (res int32)

Gobra uses a modular verification approach. Each function is verified independently and all we know about a function at call sites is its specification. This is crucial for scaling verification to large projects.

It is the programmer's job to write the specification, as well as any proof annotations, and it is Gobra's job to check that the proof that a function satisfies its contract is correct. These checks happen statically. In the next sections, we explain how to prove that a function satisfies its contract and how callers of this function may rely on its contract.

Preconditions with requires

Preconditions are added with the keyword requires before a function declaration. In Go programs, we write Gobra annotations in special line comments starting with //@.

Gobra checks that a function's preconditions hold for every call of that function and reports an error if it cannot prove it.

Gobra assumes the preconditions hold at the beginning of the function's body.

Let us exemplify this with the absolute value example:

package abs

const MinInt32 = -2147483648

//@ requires x != MinInt32
//@ ensures res >= 0 && (res == x || res == -x)
func Abs(x int32) (res int32) {
    if x < 0 {
        return -x
    } else {
        return x
    }
}

func client1() {
    v1 := Abs(3)  // v1 == 3
    v2 := Abs(-2) // v2 == 2
    v3 := Abs(MinInt32) // error
}

//@ requires a > 0 && b < 0
func client2(a, b int32) {
    v4 := Abs(a)
    v5 := Abs(b) // error
}
ERROR Precondition of call Abs(MinInt32) might not hold. 
Assertion x != -2147483648 might not hold.
ERROR Precondition of call Abs(b) might not hold. 
Assertion x != MinInt32 might not hold.

The function client1 calls Abs with constant values. For the calls Abs(3) and Abs(-2) the preconditions hold, since clearly 3 != MinInt32 and -2 != MinInt32. This check fails for Abs(MinInt32). The function client2 calls Abs with its arguments constrained by another precondition a > 0 && b < 0. Here the precondition holds for the call Abs(a), since a > 0 implies a != MinInt32. We get another error for Abs(b), as the only information Gobra has about b at this point is b < 0 which does not exclude b != MinInt32.

Please note that the errors are reported at the location of the call since the caller is violating the contract of the function.

Preconditions a > 0 && b < 0 joined by the logical AND can be split into multiple lines. We can write the contract for client2 equivalently as:

//@ requires a > 0
//@ requires b < 0
func client2(a, b int32)

assert

Gobra can be instructed to perform checks with the assert statement.

Gobra checks that the assertion holds and reports an error if it cannot prove it.

The first assertion passes in the following program since it can be inferred from the precondition.

//@ requires a > 0 && b < 0
func client3(a, b int32) {
    //@ assert a > b
    //@ assert b > -10 // error
}
ERROR Assert might fail. 
Assertion b > -10 might not hold.

Postconditions with ensures

Postconditions are added with the keyword ensures before a function declaration. By convention, they are written after any preconditions.

Gobra checks that a function's postconditions hold whenever the function returns and reports an error if it cannot prove it.

In the absolute value example, we have already seen the precondition res >= 0 && (res == x || res == -x). Assertions are included in the program to show the information Gobra has at the respective program locations. At the beginning of the function, the precondition holds. Depending on the branch taken, different constraints hold for x. In this example, the postcondition must be proven to hold at both return locations. After the calls to Abs, the caller can assert the postcondition.

const MinInt32 = -2147483648

//@ requires x != MinInt32
//@ ensures res >= 0 && (res == x || res == -x)
func Abs(x int32) (res int32) {
    //@ assert x != MinInt32
    if x < 0 {
        //@ assert x != MinInt32 && x < 0
        return -x
    } else {
        //@ assert x != MinInt32 && !(x < 0)
        return x
    }
}

func client4() {
    v1 := Abs(3)
    //@ assert v1 >= 0 && (v1 == 3 || v1 == -3)
    //@ assert v1 == 3
    v2 := Abs(-2)
    //@ assert v2 >= 0 && (v2 == -2 || v2 == -(-2))
    //@ assert v2 == 2
}

If we exchange the bodies of the if statement, the function WrongAbs does not satisfy its contract:

//@ requires x != MinInt32
//@ ensures res >= 0 && (res == x || res == -x)
func WrongAbs(x int32) (res int32) {
    if x < 0 {
        return x
    } else {
        return -x
    }
}
ERROR Postcondition might not hold. 
Assertion res >= 0 might not hold.

Default Precondition and Postcondition

If no precondition is specified by the user, it defaults to true.

If no postcondition is specified by the user, it defaults to true.

func foo() int

is equivalent to

//@ requires true
//@ ensures true
func foo() int

Since true always holds, the precondition true does not restrict when a function can be called. A postcondition true provides no information about the return values.

Quiz