Maps
Go provides the built-in map data structure implementing a hash table.
watched := make(map[string]bool, 10) // optional capacity
// @ assert acc(watched) && len(watched) == 0
Permission to a map is obtained on allocation, e.g. from make
.
The accessibility predicate acc(watched)
is for the entire map.
Individual elements, as in slices, are not addressable.
Holding write permissions, we can add entries.
In specifications, we can check with in
if a key is contained in the map.
watched["Blade Runner"] = true
// @ assert "Blade Runner" in watched && len(watched) == 1
The values can be retrieved with their keys. Note that key elements must be comparable. For example, one cannot use other maps, slices, and functions as keys.
elem, ok := watched["Blade Runner"]
// @ assert ok && elem
// non-existing key
elem, ok := watched["Dune"]
// @ assert !ok && !elem
nil
map
A nil
map is obtained with new
or by not initializing a map variable.
No permission is held for the nil
map and no elements can be added.
Otherwise, it behaves like an empty map.
var rating map[string]int
// @ assert acc(rating, noPerm)
// @ assert len(rating) == 0
r, ok := rating["Dune"]
// @ assert !ok && r == 0
rotten := new(map[string]int)
// @ assert len(*rotten) == 0
We can read from the nil
map as we would from an empty map, even without permission.
For functions that read from a map m
,
the precondition m != nil ==> acc(m, 1/2)
is typically used to support both nil
and non-nil maps.
Range clause for maps
Range loops iterate over the keys and values for a map.
A with
clause must be added (e.g. range m /*@ with visited @*/
).
The ghost variable visited
is a mathematical set (introduced in the next chapter) that contains the keys already visited.
In the following snippet, we build a map literal, with keys representing identifiers and Movie
structs as values.
The function avgRating
computes the average rating of all movies in a map.
We focus on the loop and omit the full functional specification for simplicity.
package movies
type Movie struct {
name string
rating int
}
// @ requires acc(m, 1/2)
// @ requires len(m) > 0
func avgRating(m map[int]Movie) int {
sum := 0
// @ invariant acc(m, 1/2)
// @ invariant len(m) > 0
for _, movie := range m /*@ with visited @*/ {
sum += movie.rating
}
return sum / len(m)
}
func critique() {
nolan := map[int]Movie{
132: {"Oppenheimer", 8},
234: {"Tenet", 7},
432: {"Dunkirk", 9},
}
// @ assert acc(nolan) && len(nolan) == 3
avgRating(nolan) // 8
}
Go does not specify the iteration order over maps (see 1). An entry added during iteration may either be produced or skipped. Gobra prohibits the mutation of maps while iterating.
package main
type Movie struct {
name string
rating int
}
// @ requires acc(m)
func produceSequels(m map[int]Movie) {
// @ invariant acc(m)
for id, movie := range m /*@ with visited @*/ {
m[2*id] = Movie{movie.name + "2", movie.rating - 2} // error
}
}
func main() {
movies := map[int]Movie{
2: {"Jaws", 6},
3: {"Cars", 5},
}
produceSequels(movies)
}
ERROR Assignment might fail.
Permission to m might not suffice.
The output of the Go program is nondeterministic (two samples):
map[2:{Jaws 6} 3:{Cars 5} 4:{Jaws2 4} 6:{Cars2 3} 8:{Jaws22 2}]
map[2:{Jaws 6} 3:{Cars 5} 4:{Jaws2 4} 6:{Cars2 3} 12:{Cars22 1} 24:{Cars222 -1} 48:{Cars2222 -3} 96:{Cars22222 -5} 192:{Cars222222 -7} 384:{Cars2222222 -9}]
Mutation is prevented by exhaling a small constant permission amount to the map before the loop. As a consequence, the wildcard permission amount is insufficient:
// @ requires acc(m, _)
// @ requires len(m) > 0
func wildRating(m map[int]Movie) int {
sum := 0
// @ invariant acc(m, _)
// @ invariant len(m) > 0
for _, movie := range m /*@ with visited @*/ {
sum += movie.rating
}
return sum / len(m)
}
ERROR Range expression should be immutable inside the loop body.
Permission to m might not suffice.
delete
and clear
At the moment, Gobra does not support Go's built-in function delete
for removing map entries or clear
for removing all map entries.