Ghost code

Often, it is useful to introduce ghost state and ghost code, i.e., additional state and code that is used for specification and verification purposes, but which is not meant to be compiled and executed. Ghost code cannot change the visible behavior of a program, i.e., it cannot assign to non-ghost variables or cause the program to not terminate.

Ghost code is considered by Gobra but it is ignored during compilation. When writing Gobra annotations in Go source files, the syntax // @ makes it clear that the line is treated as a comment by Go and is not included in the program.

Ghost parameters

Consider the function Max that returns the maximum element of an array, with the signature:

// @ ensures forall i int :: 0 <= i && i < len(arr) ==> arr[i] <= res
// @ ensures exists i int :: 0 <= i && i < len(arr) ==> arr[i] == res
func Max(arr [10]int) (res int)

Verifying this function would be challenging because of the existential quantifier. Additionally, the postcondition gives clients only the maximal value.

With a ghost out parameter idx, we can return the index of the maximal value. The updated contract specifies, that the maximal value is at index idx. We update idx with a ghost statement to maintain the invariant that it is the index of the maximal value in the prefix of the array already iterated over. As Max has two out parameters now, clients must assign the ghost return value to a ghost location.

const N = 10

// @ ensures forall i int :: 0 <= i && i < len(arr) ==> arr[i] <= res
// @ ensures 0 <= idx && idx < len(arr) && arr[idx] == res
func Max(arr [N]int) (res int /*@ , ghost idx int @*/) {
    res = arr[0]
    // @ invariant 0 <= i0 && i0 <= N
    // @ invariant forall j int :: 0 <= j && j < i0 ==> arr[j] <= res
    // @ invariant 0 <= idx && idx < N && arr[idx] == res
    for i, a := range arr /*@ with i0 @*/ {
        if a > res {
            res = a
            // @ ghost idx = i
        }
    }
    return res /*@ , idx @*/
}

func client() {
    arr := [10]int{0, 2, 4, 8, 10, 1, 3, 5, 7, 9}
    // @ assert arr[4] == 10
    // @ ghost var idx int
    m, /*@ idx @*/ := Max(arr)
    // @ assert arr[idx] == m
    // @ assert m == 10
}

Ghost erasure property

Recall that ghost code cannot change the visible behavior of a program. Hence, ghost code cannot modify non-ghost locations. For example, the ghost statement x = 1 causes an error since x is a normal variable:

    var x int
    // @ ghost x = 1
    // ...
ERROR ghost error: only ghost locations can be assigned to in ghost code

To make a statement ghost, add ghost before it. Although not all statements can appear in ghost code. For example, there is no ghost return statement, because it can change the visible behavior of a program:

func loop() {
    // @ ghost return
    for {}
}
ERROR ghost error: expected ghostifiable statement

Termination of ghost functions

Ghost functions must be proven to terminate. Calling a non-terminating ghost function could change the visible behavior of a program. For example, the function boo does not terminate with ghost code but returns immediately without ghost code..

/*@
ghost
func inf() {
    for {}
}
@*/

// @ decreases
func boo() int {
    // @ ghost inf()
    return 42
}
ERROR loop occurring in ghost context does not have a termination measure
    for {}
 ^

More ghost entities

In general, Gobra allows marking the following entities as ghost:

  • in and out parameters of functions and methods
  • functions and methods where all in- and out-parameters are implicitly ghost
  • variables ghost var x int = 42
  • statements

We will continue using ghost code in the following chapters.