11 The standard library
Boogie ships with three small libraries that are not part of the language but are compiled into the tool as embedded resources and can be pulled into a program on demand. They are the closest thing Boogie has to a prelude: maps lifted pointwise, sets, finite maps, an Option type, a vector datatype, SMT sequences, and the datatypes and primitives that the Civl concurrency layer builds on.
None of this exists in This is Boogie 2 (2008). The paper predates datatypes, and therefore predates every datatype in these files. Everything in this chapter is documented from the library sources (Source/Core/base.bpl, node.bpl, set_size.bpl) and from the C# that gives some of the declarations extra meaning.
The libraries are ordinary Boogie text. Nothing in them is privileged by the
parser: they are parsed, resolved and typechecked exactly like your own
declarations, and they land in the same global namespace. A handful of the
declarations are privileged after that point —
11.1 Loading a library
boogie /help
/lib:<name> Include definitions in library <name>. The file <name>.bpl
must be an included resource in Core.dll. Currently, the
following libraries are supported---base, node.
That is the whole of the tool’s own documentation for the feature. There are three libraries:
/lib:base —
Source/Core/base.bpl, 323 lines. Map combinators, Default, Option, Vec, Seq, the Set_* wrappers, the finite-map datatype Map, and the Civl-oriented types and primitives. /lib:node —
Source/Core/node.bpl, 146 lines. A linked-list Node datatype, the Between/Avoiding reachability predicates and their axiomatisation, and stack/set/queue abstraction functions with their lemmas. /lib:set_size —
Source/Core/set_size.bpl, 29 lines. A cardinality function Set_Size for [T]bool sets, its axioms, and three lemmas.
Nothing is loaded unless you ask for it. With load.bpl containing
procedure p(v: Vec int)
{
assert Vec_Len(v) >= 0;
}
the library names are simply undeclared:
boogie load.bpl
load.bpl(1,15): Error: undeclared type: Vec (replacing with "bool" to continue resolving)
load.bpl(1,15): Error: undeclared type: Vec (replacing with "bool" to continue resolving)
load.bpl(3,9): Error: use of undeclared function: Vec_Len
3 name resolution errors detected in load.bpl
boogie /lib:base load.bpl
Boogie program verifier finished with 1 verified, 0 errors
The name after /lib: is turned into the resource name Core.<name>.bpl and looked up in Boogie.Core.dll. A name that does not resolve produces a message and no declarations:
boogie /lib:vec load.bpl
Error locating library: Core.vec.bpl not found
Nothing else is printed —
The libraries are appended to the program after all command-line .bpl files have been parsed, and everything is resolved together. Consequences:
Dependencies are not resolved for you. node.bpl and set_size.bpl both use declarations from base.bpl, so /lib:node or /lib:set_size on their own produce a wall of resolution errors inside the library file. You must write /lib:base /lib:node. The order of the /lib: switches does not matter.
Repeating a library is harmless. The set of requested libraries is a HashSet, so /lib:base /lib:base loads base.bpl once.
The names are yours to collide with. Declaring your own Default, Vec or Set_Union alongside /lib:base is a duplicate declaration, and the error is reported against the library file.
Library source appears in counterexample traces. Inlined library bodies show up in execution traces with base.bpl line numbers.
Loading node without base, against the same load.bpl, produces 186 errors; here are the first four and the last:
boogie /lib:node load.bpl
node.bpl(1,30): Error: undeclared type: Option (replacing with "bool" to continue resolving)
load.bpl(1,15): Error: undeclared type: Vec (replacing with "bool" to continue resolving)
load.bpl(1,15): Error: undeclared type: Vec (replacing with "bool" to continue resolving)
load.bpl(3,9): Error: use of undeclared function: Vec_Len
...
186 name resolution errors detected in load.bpl
A clash looks like this. With clash.bpl containing
function Default<T>(): T;
procedure p() { }
boogie /lib:base clash.bpl
base.bpl(31,9): Error: more than one declaration of function name: Default
1 name resolution errors detected in clash.bpl
11.1.1 Automatic loading for Civl programs
If any top-level declaration in the program carries a Civl attribute —
The check inspects only the attributes of the top-level declaration itself, not of its formal parameters. A procedure whose only Civl marking is on a parameter therefore does not trigger the automatic load:
procedure P({:linear} x: One int)
{
}
boogie linear-formal.bpl
linear-formal.bpl(1,25): Error: undeclared type: One (replacing with "bool" to continue resolving)
linear-formal.bpl(1,25): Error: undeclared type: One (replacing with "bool" to continue resolving)
2 name resolution errors detected in linear-formal.bpl
Adding /lib:base fixes it. A yield procedure {:layer 1} in the same program would also fix it, because {:layer} sits on the declaration.
11.1.2 Monomorphisation
Boogie’s default type encoding is monomorphic. A program that is already
monomorphic is used as it stands; anything else is run through the
monomorphisation pass, and a program the monomorphiser cannot handle is a fatal
error. Almost every declaration in the libraries is polymorphic —
procedure A<T>(i: T)
{
call A(Some(i));
}
boogie /lib:base mono-cycle.bpl
Unable to monomorphize input program: expanding type cycle detected
This is not something the libraries introduce, though; the same failure occurs for any polymorphic program that grows its type arguments, with no library loaded at all:
procedure A<T>(i: T)
{
var m: [int]T;
call A(m);
}
boogie mono-nolib.bpl
Unable to monomorphize input program: expanding type cycle detected
11.2 Map combinators
base.bpl opens with functions that lift scalar operations pointwise over maps. All of them are declared {:builtin "..."} and are translated directly into SMT array combinators; none of them has a Boogie-level axiomatisation.
function {:builtin "MapConst"} MapConst<T,U>(U): [T]U;
function {:builtin "MapEq"} MapEq<T,U>([T]U, [T]U) : [T]bool;
function {:builtin "MapIte"} MapIte<T,U>([T]bool, [T]U, [T]U) : [T]U;
function {:builtin "MapOr"} MapOr<T>([T]bool, [T]bool) : [T]bool;
function {:builtin "MapAnd"} MapAnd<T>([T]bool, [T]bool) : [T]bool;
function {:builtin "MapNot"} MapNot<T>([T]bool) : [T]bool;
function {:builtin "MapImp"} MapImp<T>([T]bool, [T]bool) : [T]bool;
function {:builtin "MapIff"} MapIff<T>([T]bool, [T]bool) : [T]bool;
function {:inline} MapDiff<T>(a: [T]bool, b: [T]bool) : [T]bool
{
MapAnd(a, MapNot(b))
}
function {:builtin "MapAdd"} MapAdd<T>([T]int, [T]int) : [T]int;
function {:builtin "MapSub"} MapSub<T>([T]int, [T]int) : [T]int;
function {:builtin "MapMul"} MapMul<T>([T]int, [T]int) : [T]int;
function {:builtin "MapDiv"} MapDiv<T>([T]int, [T]int) : [T]int;
function {:builtin "MapMod"} MapMod<T>([T]int, [T]int) : [T]int;
function {:builtin "MapGt"} MapGt<T>([T]int, [T]int) : [T]bool;
function {:builtin "MapGe"} MapGe<T>([T]int, [T]int) : [T]bool;
function {:builtin "MapLt"} MapLt<T>([T]int, [T]int) : [T]bool;
function {:builtin "MapLe"} MapLe<T>([T]int, [T]int) : [T]bool;
function {:inline} Id<T>(t: T): T
{
t
}
Read them as follows, for every index i of the domain type:
MapConst(u)[i] == u —
the constant map. Because the domain type does not appear in the argument list, a bare MapConst(0) usually needs its type fixed by context or by a coercion. MapEq(a, b)[i] == (a[i] == b[i]) —
pointwise equality, producing a bool map, not a bool. MapIte(c, a, b)[i] == (if c[i] then a[i] else b[i]).
MapOr, MapAnd, MapNot, MapImp, MapIff —
pointwise ||, &&, !, ==>, <==> on [T]bool. MapDiff(a, b) == MapAnd(a, MapNot(b)). This is the only combinator in the group that is {:inline} rather than builtin.
MapAdd, MapSub, MapMul, MapDiv, MapMod —
pointwise integer arithmetic on [T]int. MapDiv and MapMod lift SMT-LIB div and mod, which agree with Boogie’s own div and mod (both are Euclidean). MapGt, MapGe, MapLt, MapLe —
pointwise comparison, [T]int to [T]bool. Id —
the identity function, useful as a trigger-carrying wrapper.
procedure MapOps()
{
var a, b: [int]bool;
var m, n: [int]int;
assert MapConst(true) == (MapNot(MapConst(false)): [int]bool);
assert MapAnd(a, b) == MapNot(MapOr(MapNot(a), MapNot(b)));
assert MapAdd(m, n) == MapAdd(n, m);
assert MapIte(a, m, n)[3] == (if a[3] then m[3] else n[3]);
assert MapLe(m, n)[7] == (m[7] <= n[7]);
assert MapDiv(m, n)[3] == m[3] div n[3];
assert MapMod(m, n)[3] == m[3] mod n[3];
assert MapDiff(a, b) == MapAnd(a, MapNot(b));
assert MapEq(m, n)[0] == (m[0] == n[0]);
assert Id(a) == a;
}
boogie /lib:base mapops.bpl
Boogie program verifier finished with 1 verified, 0 errors
Five of those assertions compare whole maps rather than elements. Two of them
(MapDiff and Id) are syntactic identities once the
{:inline} bodies are substituted, but the other three —
11.2.1 Implementation note: the emitted SMT
The builtin names are recognised by the SMT-LIB lineariser, which rewrites them into Z3 array combinators: MapConst becomes (as const <sort>), MapAnd becomes (_ map and), MapAdd becomes (_ map (+ (Int Int) Int)), and so on.
11.2.2 Integer ranges
Three declarations in the middle of base.bpl exist to build integer ranges out of the combinators. They are used by the Vec axioms but are public and generally useful.
const Identity: [int]int;
axiom (forall x: int :: Identity[x] == x);
function {:inline} AtLeast(x: int): [int]bool
{
MapLe(MapConst(x), Identity)
}
function {:inline} Range(from: int, n: int): [int]bool {
MapDiff(AtLeast(from), AtLeast(from + n))
}
AtLeast(x) is the set of integers >= x; Range(from, n) is the half-open interval [from, from+n), and is empty when n <= 0.
procedure Ranges()
{
assert Identity[7] == 7;
assert AtLeast(3)[3] && AtLeast(3)[100] && !AtLeast(3)[2];
assert Range(3, 2) == MapConst(false)[3 := true][4 := true];
assert Range(0, 0) == MapConst(false);
assert Range(5, -1) == MapConst(false);
}
boogie /lib:base ranges.bpl
Boogie program verifier finished with 1 verified, 0 errors
11.3 Default
function Default<T>(): T;
An uninterpreted nullary function, one per instantiated type. It is a function,
so it is deterministic, but no axiom pins down its value: Default():
int is not known to be 0, and Default(): bool is not known to be
false. It exists purely so that the library can say "the padding value" —
procedure DefaultIsAFunction()
{
assert Default(): int == Default(): int;
}
procedure DefaultIsUnknown()
{
assert Default(): int == 0;
}
procedure DefaultBool()
{
assert Default(): bool == false;
}
boogie /lib:base default.bpl
default.bpl(8,3): Error: this assertion could not be proved
Execution trace:
default.bpl(8,3): anon0
default.bpl(13,3): Error: this assertion could not be proved
Execution trace:
default.bpl(13,3): anon0
Boogie program verifier finished with 1 verified, 2 errors
Do not assume anything about Default(); and do not add an axiom fixing it, because a single Default is shared by every use at that type.
11.4 Option, Unit and UnitMap
datatype Option<T> { None(), Some(t: T) }
datatype Unit { Unit() }
type UnitMap K = Map K Unit;
Option is the usual two-constructor option type; the payload accessor is named t, so it is o->t and not o->val. Unit is the one-element type, and UnitMap K is the type synonym for a finite map with Unit values, i.e. a finite set in the Map representation. In Civl code UnitMap K is the standard type of a pool of linear resources.
procedure Options()
{
var o: Option int;
o := None();
assert o is None;
o := Some(4);
assert o is Some && o->t == 4;
assert None(): Option int != Some(0);
}
procedure Units()
{
var u: Unit;
var m: UnitMap int;
assert u == Unit();
m := Map_Empty();
m := Map_Update(m, 1, Unit());
assert Map_Contains(m, 1);
}
boogie /lib:base options.bpl
Boogie program verifier finished with 2 verified, 0 errors
11.5 Sets
Sets are [T]bool maps; the Set_* functions are all {:inline} wrappers around map combinators and map select/update, so they carry no axioms of their own and cost nothing.
function {:inline} Set_Empty<T>(): [T]bool { MapConst(false) }
function {:inline} Set_Contains<T>(a: [T]bool, t: T): bool { a[t] }
function {:inline} Set_IsSubset<T>(a: [T]bool, b: [T]bool): bool
{ MapImp(a, b) == MapConst(true) }
function {:inline} Set_IsDisjoint<T>(a: [T]bool, b: [T]bool): bool
{ Set_Intersection(a, b) == Set_Empty() }
function {:inline} Set_Add<T>(a: [T]bool, t: T): [T]bool { a[t := true] }
function {:inline} Set_Singleton<T>(t: T): [T]bool { Set_Add(Set_Empty(), t) }
function {:inline} Set_Remove<T>(a: [T]bool, t: T): [T]bool { a[t := false] }
function {:inline} Set_Union<T>(a: [T]bool, b: [T]bool): [T]bool { MapOr(a, b) }
function {:inline} Set_Difference<T>(a: [T]bool, b: [T]bool): [T]bool { MapDiff(a, b) }
function {:inline} Set_Intersection<T>(a: [T]bool, b: [T]bool): [T]bool { MapAnd(a, b) }
(The bodies above are reformatted onto one line each; the declarations are otherwise verbatim.)
There is also a choice function, the only part of the set layer with an axiom:
function Choice<T>(a: [T]bool): T;
axiom (forall<T> a: [T]bool :: {Choice(a)} a == MapConst(false) || a[Choice(a)]);
Choice(a) is some member of a whenever a is non-empty, and
completely unconstrained when a is empty. It is deterministic —
procedure SetOps()
{
var a, b: [int]bool;
a := Set_Empty();
assert !Set_Contains(a, 3);
a := Set_Add(a, 3);
a := Set_Add(a, 5);
assert Set_Contains(a, 5);
b := Set_Singleton(5);
assert Set_IsSubset(b, a);
assert !Set_IsSubset(a, b);
assert Set_IsDisjoint(Set_Singleton(1), Set_Singleton(2));
assert Set_Difference(a, b) == Set_Singleton(3);
assert Set_Union(b, Set_Singleton(3)) == a;
assert Set_Intersection(a, b) == b;
assert Set_Remove(a, 5) == Set_Singleton(3);
}
procedure ChoiceFn(a: [int]bool)
requires Set_Contains(a, 42);
{
assert Set_Contains(a, Choice(a));
assert Choice(Set_Singleton(7)) == 7;
}
boogie /lib:base sets.bpl
Boogie program verifier finished with 2 verified, 0 errors
11.5.1 Set cardinality: /lib:set_size
set_size.bpl adds an uninterpreted cardinality function with six axioms and three lemmas. It requires /lib:base.
function Set_Size<T>(a: [T]bool) : int;
axiom (forall<T> a: [T]bool :: 0 <= Set_Size(a));
axiom (forall<T> :: Set_Size(Set_Empty(): [T]bool) == 0);
axiom (forall<T> a: [T]bool, t: T :: {Set_Add(a, t)} Set_Size(Set_Add(a, t)) == if Set_Contains(a, t) then Set_Size(a) else Set_Size(a) + 1);
axiom (forall<T> a: [T]bool, t: T :: {Set_Remove(a, t)} Set_Size(Set_Remove(a, t)) == if Set_Contains(a, t) then Set_Size(a) - 1 else Set_Size(a));
axiom (forall<T> a: [T]bool, b: [T]bool :: {Set_Size(Set_Difference(a, b))} {Set_Size(Set_Intersection(a, b))}
Set_Size(a) == Set_Size(Set_Difference(a, b)) + Set_Size(Set_Intersection(a, b)));
axiom (forall<T> a: [T]bool, b: [T]bool :: {Set_Size(Set_Union(a, b))} {Set_Size(Set_Intersection(a, b))}
Set_Size(Set_Union(a, b)) + Set_Size(Set_Intersection(a, b)) == Set_Size(a) + Set_Size(b));
Note what is not there. Set_Size is non-negative, correct on the empty set, and correct under single-element add/remove and under the inclusion-exclusion identities. There is no axiom saying that a non-empty set has positive size, and no axiom relating subset to size. Note also that the last four axioms are trigger-guarded on Set_Add, Set_Remove, Set_Size(Set_Difference(...)), Set_Size(Set_Intersection(...)) and Set_Size(Set_Union(...)): if none of those terms occurs in your program, they are never instantiated. The subset fact is supplied as a lemma instead:
pure procedure Lemma_SetSize_Add<T>(a: [T]bool, t: T) returns (b: [T]bool);
requires !Set_Contains(a, t);
ensures b == Set_Add(a, t);
ensures Set_Size(b) == Set_Size(a) + 1;
pure procedure Lemma_SetSize_Remove<T>(a: [T]bool, t: T) returns (b: [T]bool);
requires Set_Contains(a, t);
ensures b == Set_Remove(a, t);
ensures Set_Size(b) + 1 == Set_Size(a);
pure procedure Lemma_SetSize_Subset<T>(a: [T]bool, b: [T]bool);
requires Set_IsSubset(a, b);
ensures a == b || Set_Size(a) < Set_Size(b);
These are pure procedure declarations with no body. A body-less procedure
is trusted: its postcondition is assumed at every call site and never
discharged. The lemmas are all sound —
// Given a witness element the strict inequality does follow from the axioms.
procedure StrictFromWitness(a: [int]bool, b: [int]bool, t: int)
requires Set_IsSubset(a, b);
requires Set_Contains(b, t) && !Set_Contains(a, t);
{
assert Set_Size(Set_Intersection(b, a)) == Set_Size(a);
assert Set_Size(Set_Remove(Set_Difference(b, a), t)) >= 0;
assert Set_Size(a) < Set_Size(b);
}
// And a witness always exists when a is a proper subset of b.
procedure WitnessExists(a: [int]bool, b: [int]bool)
requires Set_IsSubset(a, b);
requires a != b;
{
assert (exists t: int :: Set_Contains(b, t) && !Set_Contains(a, t));
}
boogie /lib:base /lib:set_size setsize-derive.bpl
Boogie program verifier finished with 2 verified, 0 errors
The second assert in StrictFromWitness is the whole trick: it puts the term Set_Remove(Set_Difference(b, a), t) in front of the prover, which fires the Set_Remove axiom and, with non-negativity, yields Set_Size(Set_Difference(b, a)) >= 1. Nothing in the axiom set makes that term appear on its own, which is why the lemma exists.
The pure qualifier comes from the grammar,
Pure<ref bool isPure>
= ["pure" (. isPure = true; .)]
.
and may prefix either a procedure or an action. A pure procedure has no state effects and may be called from ordinary procedures, which is what makes it a convenient lemma vehicle.
procedure Sizes()
{
var a, b: [int]bool;
a := Set_Empty();
assert Set_Size(a) == 0;
a := Set_Add(a, 3);
assert Set_Size(a) == 1;
a := Set_Add(a, 3);
assert Set_Size(a) == 1;
b := Set_Add(a, 5);
assert Set_Size(b) == 2;
assert Set_Size(Set_Remove(b, 5)) == 1;
}
procedure Monotone(a: [int]bool, b: [int]bool)
requires Set_IsSubset(a, b);
requires a != b;
{
call Lemma_SetSize_Subset(a, b);
assert Set_Size(a) < Set_Size(b);
}
// Nothing fires the difference/intersection axiom here, so not even the
// non-strict inequality comes out.
procedure NonStrict(a: [int]bool, b: [int]bool)
requires Set_IsSubset(a, b);
{
assert Set_Size(a) <= Set_Size(b);
}
// Mentioning a trigger term is enough for the non-strict inequality.
procedure NonStrictTriggered(a: [int]bool, b: [int]bool)
requires Set_IsSubset(a, b);
{
assert Set_Size(Set_Intersection(b, a)) == Set_Size(a);
assert Set_Size(a) <= Set_Size(b);
}
// The strict inequality still does not come out: nothing puts a term in
// front of the prover that would give a non-empty set a positive size.
procedure StrictWithoutLemma(a: [int]bool, b: [int]bool)
requires Set_IsSubset(a, b);
requires a != b;
{
assert Set_Size(Set_Intersection(b, a)) == Set_Size(a);
assert Set_Size(a) < Set_Size(b);
}
boogie /lib:base /lib:set_size setsize.bpl
setsize.bpl(29,3): Error: this assertion could not be proved
Execution trace:
setsize.bpl(29,3): anon0
setsize.bpl(47,3): Error: this assertion could not be proved
Execution trace:
setsize.bpl(46,3): anon0
Boogie program verifier finished with 3 verified, 2 errors
11.6 Vectors
Vec is the library’s array/list type. It is the part of the library most likely to be reached for by non-Civl users.
11.6.1 Representation
datatype Vec<T> { Vec(contents: [int]T, len: int) }
A vector is a pair of an infinite integer-indexed map and a length. The elements are contents[0] through contents[len-1]. Vec is an ordinary Boogie datatype, so Vec(m, n) is a legal expression for any map m and any integer n, and v->contents and v->len are legal field accesses.
11.6.2 The canonicalisation axioms
axiom {:ctor "Vec"} (forall<T> x: Vec T :: {x->len}{x->contents} MapIte(Range(0, x->len), MapConst(Default()), x->contents) == MapConst(Default()));
axiom {:ctor "Vec"} (forall<T> x: Vec T :: {x->len} x->len >= 0);
Unpacked, the first axiom says: overwriting the live range [0, len) with Default() leaves a map that is Default() everywhere. That is, for every index i outside [0, len), contents[i] == Default(). The second says lengths are non-negative.
These are the axioms that make vectors behave. They give you, for free:
procedure OutOfRange(v: Vec int)
{
assert Vec_Nth(v, -1) == Default();
assert Vec_Nth(v, Vec_Len(v)) == Default();
assert Vec_Len(v) >= 0;
}
procedure EmptyIsUnique(v: Vec int)
requires Vec_Len(v) == 0;
{
assert v == Vec_Empty();
}
procedure Degenerate(v: Vec int, i: int, j: int)
requires !(0 <= i && i < Vec_Len(v) && 0 <= j && j < Vec_Len(v));
{
assert Vec_Swap(v, i, j) == v;
}
procedure RemoveEmpty(v: Vec int)
requires Vec_Len(v) == 0;
{
assert Vec_Remove(v) == v;
}
procedure SliceDegenerate(v: Vec int, i: int, j: int)
requires !(0 <= i && i < j && j <= Vec_Len(v));
{
assert Vec_Slice(v, i, j) == Vec_Empty();
}
boogie /lib:base vec-canonical.bpl
Boogie program verifier finished with 5 verified, 0 errors
They also give vectors extensionality: two vectors with the same length whose in-range elements agree are equal, because their contents maps agree everywhere.
Vectors are built with Vec_Empty, Vec_Append, Vec_Update, Vec_Concat, Vec_Slice, Vec_Swap and Vec_Remove, all of which maintain the representation these axioms describe. If you need an operation the library does not have, define it in terms of those. Reading v->contents and v->len directly is fine.
The {:ctor "Vec"} attribute on both axioms is dead. It was read by an earlier version of the monomorphiser and the code that read it was deleted in commit 76b6a373; no C# in the current tree looks at "ctor". The same vestigial attribute appears on every axiom in node.bpl.
11.6.3 Vector operations
function {:inline} Vec_Empty<T>(): Vec T
{
Vec(MapConst(Default()), 0)
}
function {:inline} Vec_Append<T>(v: Vec T, x: T) : Vec T
{
Vec(v->contents[v->len := x], v->len + 1)
}
function {:inline} Vec_Update<T>(v: Vec T, i: int, x: T) : Vec T
{
if (0 <= i && i < v->len) then Vec(v->contents[i := x], v->len) else v
}
function {:inline} Vec_Nth<T>(v: Vec T, i: int): T
{
v->contents[i]
}
function {:inline} Vec_Len<T>(v: Vec T): int
{
v->len
}
function {:inline} Vec_Contains<T>(v: Vec T, i: int): bool
{
0 <= i && i < Vec_Len(v)
}
(Regrouped: in base.bpl Vec_Concat and Vec_Slice sit between Vec_Len and Vec_Swap, and Vec_Contains comes last of all. The declarations themselves are verbatim.) Every one of them is {:inline}, so they disappear before the VC is built and cost nothing at the prover.
Vec_Empty() —
the unique vector of length 0. Vec_Append(v, x) —
appends at index len, length +1. Total. Vec_Update(v, i, x) —
replaces element i. Out of range it is the identity, not an error. Vec_Nth(v, i) —
element i. Out of range it is Default(), not an error. Vec_Len(v) —
the length. Vec_Contains(v, i) —
despite the name, this is a check on the index, not on an element: it is 0 <= i && i < Vec_Len(v). This is a genuine trap for readers who expect a membership test.
function {:inline} Vec_Swap<T>(v: Vec T, i: int, j: int): Vec T
{
(
var cond := 0 <= i && i < v->len && 0 <= j && j < v->len;
Vec(v->contents[i := v->contents[if (cond) then j else i]][j := v->contents[if (cond) then i else j]], v->len)
)
}
function {:inline} Vec_Remove<T>(v: Vec T): Vec T
{
(
var cond, new_len := 0 < v->len, v->len - 1;
Vec(v->contents[new_len := if (cond) then Default() else v->contents[new_len]], if (cond) then new_len else v->len)
)
}
Both are written so that the degenerate case reduces to the identity while
staying canonical —
Vec_Swap(v, i, j) —
swaps i and j; the identity if either index is out of range. Vec_Remove(v) —
drops the last element; the identity on the empty vector. There is no Vec_Remove at an index, and no Vec_Insert; Test/inst/vector.bpl builds remove, swap_remove, reverse, append, contains and index_of on top of these primitives, and is the best worked example of vector proofs in the tree.
procedure Build()
{
var v: Vec int;
v := Vec_Empty();
assert Vec_Len(v) == 0;
v := Vec_Append(v, 10);
v := Vec_Append(v, 20);
v := Vec_Append(v, 30);
assert Vec_Len(v) == 3;
assert Vec_Nth(v, 0) == 10;
assert Vec_Nth(v, 2) == 30;
assert Vec_Contains(v, 2);
assert !Vec_Contains(v, 3);
v := Vec_Update(v, 1, 99);
assert Vec_Nth(v, 1) == 99 && Vec_Len(v) == 3;
v := Vec_Update(v, 7, 0);
assert Vec_Len(v) == 3 && Vec_Nth(v, 1) == 99;
v := Vec_Remove(v);
assert Vec_Len(v) == 2;
v := Vec_Swap(v, 0, 1);
assert Vec_Nth(v, 0) == 99 && Vec_Nth(v, 1) == 10;
}
boogie /lib:base vec-basics.bpl
Boogie program verifier finished with 1 verified, 0 errors
11.6.4 Concat and Slice
function {:inline} Vec_Concat<T>(v1: Vec T, v2: Vec T): Vec T
{
Vec(
(lambda {:pool "Concat"} i: int ::
if (i < 0) then Default()
else if (0 <= i && i < Vec_Len(v1)) then Vec_Nth(v1, i)
else if (Vec_Len(v1) <= i && i < Vec_Len(v1) + Vec_Len(v2)) then Vec_Nth(v2, i - Vec_Len(v1))
else Default()),
Vec_Len(v1) + Vec_Len(v2)
)
}
function {:inline} Vec_Slice<T>(v: Vec T, i: int, j: int): Vec T
{
(
var cond := 0 <= i && i < j && j <= v->len;
Vec(
(lambda {:pool "Slice"} k: int :: if (cond && 0 <= k && k < j - i) then Vec_Nth(v, k + i) else Default()),
if (cond) then j - i else 0
)
)
}
Vec_Concat(v1, v2) is the concatenation. Vec_Slice(v, i, j) is the
half-open slice [i, j), and yields Vec_Empty() whenever the range is
degenerate (i < 0, i >= j, or j > Vec_Len(v)) —
Both build their contents with a lambda rather than a chain of updates, and both tag the lambda with {:pool "Concat"} / {:pool "Slice"}. Those names are instantiation pools: Boogie’s lambda-instantiation machinery will only instantiate the lambda at index terms that have been added to the pool. Simple shape facts come out without help:
type Element;
procedure SplitJoin(A: Vec Element, i: int)
requires 0 <= i && i < Vec_Len(A);
{
assert Vec_Concat(Vec_Slice(A, 0, i), Vec_Slice(A, i, Vec_Len(A))) == A;
}
procedure SliceShape(A: Vec Element, i: int, j: int)
requires 0 <= i && i < j && j <= Vec_Len(A);
{
assert Vec_Len(Vec_Slice(A, i, j)) == j - i;
assert Vec_Nth(Vec_Slice(A, i, j), 0) == Vec_Nth(A, i);
assert Vec_Len(Vec_Slice(A, 5, 5)) == 0;
}
procedure ConcatShape(A: Vec Element, B: Vec Element)
{
assert Vec_Len(Vec_Concat(A, B)) == Vec_Len(A) + Vec_Len(B);
}
boogie /lib:base vec-slice.bpl
Boogie program verifier finished with 3 verified, 0 errors
Harder equalities between slice/concat terms usually do not, and need indices
fed into the pool with {:add_to_pool "Slice", e} on an
assert or assume —
11.6.5 Extensionality: Vec_Ext
// extensionality lemma to be used explicitly by the programmer
procedure Vec_Ext<T>(A: Vec T, B: Vec T) returns (i: int);
ensures A == B || Vec_Len(A) != Vec_Len(B) || Vec_Nth(A, i) != Vec_Nth(B, i);
A body-less —
You do not need it for the elementwise-equality-implies-equality direction —
type Element;
procedure WithoutExt(A: Vec Element, B: Vec Element, i: int, e: Element)
requires 0 <= i && i < Vec_Len(A);
requires Vec_Len(A) == Vec_Len(B);
requires Vec_Nth(A, i) == Vec_Nth(B, Vec_Len(B) - 1);
requires Vec_Concat(Vec_Slice(A, 0, i), Vec_Slice(A, i + 1, Vec_Len(A))) == Vec_Slice(B, 0, Vec_Len(B) - 1);
{
var A', B': Vec Element;
A' := Vec_Append(A, e);
B' := Vec_Swap(Vec_Append(B, e), Vec_Len(B) - 1, Vec_Len(B));
assert Vec_Concat(Vec_Slice(A', 0, i), Vec_Slice(A', i + 1, Vec_Len(A')))
== Vec_Slice(B', 0, Vec_Len(B') - 1);
}
procedure WithExt(A: Vec Element, B: Vec Element, i: int, e: Element)
requires 0 <= i && i < Vec_Len(A);
requires Vec_Len(A) == Vec_Len(B);
requires Vec_Nth(A, i) == Vec_Nth(B, Vec_Len(B) - 1);
requires Vec_Concat(Vec_Slice(A, 0, i), Vec_Slice(A, i + 1, Vec_Len(A))) == Vec_Slice(B, 0, Vec_Len(B) - 1);
{
var A', B': Vec Element;
var x: int;
A' := Vec_Append(A, e);
B' := Vec_Swap(Vec_Append(B, e), Vec_Len(B) - 1, Vec_Len(B));
call x := Vec_Ext(Vec_Concat(Vec_Slice(A', 0, i), Vec_Slice(A', i + 1, Vec_Len(A'))),
Vec_Slice(B', 0, Vec_Len(B') - 1));
assert {:add_to_pool "Slice", x}
Vec_Concat(Vec_Slice(A', 0, i), Vec_Slice(A', i + 1, Vec_Len(A')))
== Vec_Slice(B', 0, Vec_Len(B') - 1);
}
boogie /lib:base vec-ext.bpl
vec-ext.bpl(14,3): Error: this assertion could not be proved
Execution trace:
vec-ext.bpl(11,6): anon0
Boogie program verifier finished with 1 verified, 1 error
The two procedures are identical apart from the Vec_Ext call and the pool
annotation. In real proofs you often need several offsets of the witness —
11.7 Sequences
type {:builtin "Seq"} Seq _;
function {:builtin "seq.empty"} Seq_Empty<T>(): Seq T;
function {:builtin "seq.len"} Seq_Len<T>(a: Seq T): int;
function {:builtin "seq.++"} Seq_Concat<T>(a: Seq T, b: Seq T): Seq T;
function {:builtin "seq.unit"} Seq_Unit<T>(v: T): Seq T;
function {:builtin "seq.nth"} Seq_Nth<T>(a: Seq T, i: int): T;
function {:builtin "seq.extract"} Seq_Extract<T>(a: Seq T, pos: int, length: int): Seq T;
An alternative to Vec that hands the work to the solver’s own sequence theory instead of axiomatising it in Boogie. Seq T maps onto Z3’s Seq sort and the six functions onto seq.empty, seq.len, seq.++, seq.unit, seq.nth and seq.extract. These are not part of the SMT-LIB standard, so a program using Seq is tied to a solver that implements the sequence theory.
procedure SeqBasics()
{
var s: Seq int;
s := Seq_Concat(Seq_Unit(1), Seq_Unit(2));
assert Seq_Len(s) == 2;
assert Seq_Nth(s, 0) == 1;
assert Seq_Nth(s, 1) == 2;
assert Seq_Len(Seq_Empty(): Seq int) == 0;
assert Seq_Extract(s, 1, 1) == Seq_Unit(2);
}
boogie /lib:base seq.bpl
Boogie program verifier finished with 1 verified, 0 errors
Trade-offs against Vec: sequences are decided (incompletely) by the solver and need no manual pool steering, but out-of-range Seq_Nth is unspecified rather than Default(), and there is no Seq analogue of the canonicalisation axioms. There are no library functions to convert between Vec and Seq. Test/sequences/ holds two small programs, each in a version that declares the {:builtin} sequence functions by hand at a fixed element type (intseq.bpl, intseq_datatype.bpl) and a version that gets the polymorphic ones from /lib:base (intseq_lib.bpl, intseq_datatype_lib.bpl). There is no Vec version there.
11.8 Finite maps
datatype Map<T,U> { Map(dom: [T]bool, val: [T]U) }
A finite map is a domain set plus a total value map. It has the same shape of
representation invariant as Vec —
function {:inline} Map_WellFormed<T,U>(a: Map T U): bool
{
a->val == MapIte(a->dom, a->val, MapConst(Default()))
}
Constructing a non-well-formed Map is legal; you simply lose extensionality until you assume well-formedness:
// Map is a free datatype: nothing forces val to be Default() outside dom.
procedure NotWellFormed(a: Map int int, b: Map int int)
requires a->dom == b->dom;
requires (forall t: int :: Set_Contains(a->dom, t) ==> Map_At(a, t) == Map_At(b, t));
{
assert a == b;
}
procedure WellFormed(a: Map int int, b: Map int int)
requires Map_WellFormed(a) && Map_WellFormed(b);
requires a->dom == b->dom;
requires (forall t: int :: Set_Contains(a->dom, t) ==> Map_At(a, t) == Map_At(b, t));
{
assert a == b;
}
boogie /lib:base fmap-wf.bpl
fmap-wf.bpl(6,3): Error: this assertion could not be proved
Execution trace:
fmap-wf.bpl(6,3): anon0
Boogie program verifier finished with 1 verified, 1 error
11.8.1 Operations
function {:inline} Map_Empty<T,U>(): Map T U
{
Map(MapConst(false), MapConst(Default()))
}
function {:inline} Map_Singleton<T,U>(t: T, u: U): Map T U
{
Map_Update(Map_Empty(), t, u)
}
function {:inline} Map_Contains<T,U>(a: Map T U, t: T): bool
{
Set_Contains(a->dom, t)
}
function {:inline} Map_IsDisjoint<T,U>(a: Map T U, b: Map T U): bool
{
Set_IsDisjoint(a->dom, b->dom)
}
function {:inline} Map_At<T,U>(a: Map T U, t: T): U
{
a->val[t]
}
function {:inline} Map_Remove<T,U>(a: Map T U, t: T): Map T U
{
Map(Set_Remove(a->dom, t), a->val[t := Default()])
}
function {:inline} Map_Update<T,U>(a: Map T U, t: T, u: U): Map T U
{
Map(Set_Add(a->dom, t), a->val[t := u])
}
function {:inline} Map_Swap<T,U>(a: Map T U, t1: T, t2: T): Map T U
{
(var u1, u2 := Map_At(a, t1), Map_At(a, t2); Map_Update(Map_Update(a, t1, u2), t2, u1))
}
function {:inline} Map_Extract<T,U>(a: Map T U, t: [T]bool): Map T U
{
Map(t, MapIte(t, a->val, MapConst(Default())))
}
function {:inline} Map_Exclude<T,U>(a: Map T U, t: [T]bool): Map T U
{
Map(Set_Difference(a->dom, t), MapIte(t, MapConst(Default()), a->val))
}
function {:inline} Map_Union<T,U>(a: Map T U, b: Map T U): Map T U
{
Map(Set_Union(a->dom, b->dom), MapIte(a->dom, a->val, b->val))
}
Points worth noting:
Map_At(a, t) does not require t to be in the domain; it is a raw lookup in val, and on a well-formed map it returns Default() outside the domain.
Map_Extract(a, t) takes t as its new domain verbatim, even if t is not a subset of a->dom —
keys in t but not in a->dom end up in the result’s domain with value Default(). The Map_Split primitive below is the checked version. Map_Union(a, b) resolves conflicts in favour of a.
Map_Remove and Map_Exclude both write Default() into the removed slots, so they preserve well-formedness.
procedure MapOps()
{
var m: Map int bool;
m := Map_Empty();
assert !Map_Contains(m, 1);
assert Map_WellFormed(m);
m := Map_Update(m, 1, true);
m := Map_Update(m, 2, false);
assert Map_Contains(m, 2);
assert Map_At(m, 1) == true;
assert Map_Collector(m) == Set_Add(Set_Singleton(1), 2);
assert Map_Remove(m, 1) == Map_Singleton(2, false);
assert Map_IsDisjoint(Map_Singleton(9, true), m);
assert Map_Extract(m, Set_Singleton(1)) == Map_Singleton(1, true);
assert Map_Exclude(m, Set_Singleton(1)) == Map_Singleton(2, false);
assert Map_Union(Map_Singleton(1, true), Map_Singleton(2, false)) == m;
assert Map_Swap(m, 1, 2) == Map_Update(Map_Update(m, 1, false), 2, true);
}
boogie /lib:base fmap.bpl
Boogie program verifier finished with 1 verified, 0 errors
11.8.2 Permission collectors
function {:inline} Map_Collector<T,U>(a: Map T U): [T]bool
{
a->dom
}
function {:inline} Map_Collector_Empty<T,U,P>(a: Map T U): [P]bool
{
MapConst(false)
}
These two are not really for user code; the Civl linear type checker instantiates them by name to compute the permission set of a linear Map variable. Map_Collector is used when the permission type is the map’s key type; Map_Collector_Empty is used for every other permission type, which is the implementation’s way of saying that permissions held inside a map’s values are not tracked ("permission collection for values stored in Map is unbounded and is not being done"). One_Collector below plays the same role for One.
11.9 Civl-oriented types and primitives
The tail of base.bpl exists to support Civl’s linear type system. These
declarations are usable in ordinary Boogie programs too —
11.9.1 One, Cell, Tag, Loc
/// singleton set
datatype One<T> { One(val: T) }
function {:inline} One_Collector<T>(a: One T): [One T]bool
{
Set_Singleton(a)
}
/// singleton map
datatype Cell<T,U> { Cell(key: One T, val: U) }
type Loc;
datatype Tag<V> { Tag(loc: Loc, val: V) }
function {:inline} Tags<V>(loc: Loc, xs:[V]bool): [One (Tag V)]bool {
(lambda x: One (Tag V):: x->val->loc == loc && Set_Contains(xs, x->val->val))
}
One T wraps a single value so it can carry a linear permission: the permission set of a One T value is the singleton containing it. It is the "I exclusively own this T" type.
Cell T U pairs an owned key with a value —
a one-entry map. Its field names are key and val. Loc is an uninterpreted type of abstract locations, the standard choice for addresses of dynamically allocated objects. One Loc is then "ownership of one location".
Tag V pairs a Loc with a value, so that one fresh Loc can mint a whole family of distinct linear tokens. Tags(loc, xs) is the set of One (Tag V) tokens for a given location and a set of values.
11.9.2 The linear primitives
/// linear primitives
pure procedure Move<T>({:linear_in} u: T, {:linear_out} v: T);
pure procedure {:inline 1} Map_MakeEmpty<K,V>() returns ({:linear} m: Map K V)
{
m := Map_Empty();
}
pure procedure One_Get<K>({:linear} path: UnitMap K, {:linear_out} l: K);
pure procedure One_Put<K>({:linear} path: UnitMap K, {:linear_in} l: K);
pure procedure Map_Get<K,V>({:linear} path: Map K V, {:linear_out} k: K) returns ({:linear} v: V);
pure procedure Map_Put<K,V>({:linear} path: Map K V, {:linear_in} k: K, {:linear_in} v: V);
pure procedure Map_Split<K,V>({:linear} path: Map K V, k: [K]bool) returns ({:linear} l: Map K V);
pure procedure Map_Join<K,V>({:linear} path: Map K V, {:linear_in} l: Map K V);
pure procedure Path_Load<V>(path: V) returns (v: V);
pure procedure Path_Store<V>(path: V, v: V);
These ten names (together with Loc_New, Tag_New and Tags_New
below) are hard-coded in the C# as CivlPrimitives.LinearPrimitives. Their
declarations are almost empty: the real semantics is that
LinearRewriter replaces each call to one of them with a short
sequence of assert and assignment commands. The rewriting is applied to
every implementation whose procedure is not a
yield procedure —
The first argument of One_Get, One_Put, Map_Get, Map_Put, Map_Split, Map_Join, Path_Load and Path_Store is a path expression: a variable, optionally followed by field accesses and map indexings. It is assigned through, so it must be assignable. Passing a procedure’s own in-parameter is rejected:
type Res;
// The path argument is assigned through, so it must be assignable.
pure procedure Take({:linear} path: Map (One Res) int, {:linear_out} k: One Res)
returns ({:linear} v: int)
{
call v := Map_Get(path, k);
}
boogie /lib:base lin-path-illegal.bpl
lin-path-illegal.bpl(7,2): Error: primitive assigns to input variable: path
1 type checking errors detected in lin-path-illegal.bpl
A path may also not go through the val field of a One, nor through the dom field of a Map:
type Res;
procedure ThroughDom({:linear_in} p: UnitMap (One Res), a: One Res)
{
var {:linear} pool: UnitMap (One Res);
var b: bool;
pool := p;
call b := Path_Load(pool->dom[a]);
}
boogie /lib:base lin-path-illegal2.bpl
lin-path-illegal2.bpl(8,2): Error: illegal path expression at position 0
1 type checking errors detected in lin-path-illegal2.bpl
Every step of a legal path is checked at verification time: indexing the val field of a Map generates "map lookup failed" (the assertion is Map_Contains of the index) and projecting a multi-constructor datatype generates "field lookup failed" (a disjunction of is-tests over the constructors that have the field).
The rewritings, verbatim from LinearRewriter:
call Move(u, v) rewrites to assert u == v; (message "Move failed"). Both arguments must be plain variables.
call One_Get(path, l) rewrites to assert Set_Contains(path->dom, l); ("One_Get failed") then path->dom := Set_Remove(path->dom, l);
call One_Put(path, l) rewrites to path->dom := Set_Add(path->dom, l); —
no check. call v := Map_Get(path, k) rewrites to assert Map_Contains(path, k); ("Map_Get failed") then v := Map_At(path, k); path := Map_Remove(path, k);
call Map_Put(path, k, v) rewrites to path := Map_Update(path, k, v);, preceded by assume !Map_Contains(path, k); when k is a variable of a linear type.
call l := Map_Split(path, k) rewrites to assert Set_IsSubset(k, path->dom); ("Map_Split failed") then l := Map_Extract(path, k); path := Map_Exclude(path, k);
call Map_Join(path, l) rewrites to path := Map_Union(path, l); —
no check; disjointness comes from linearity. call v := Path_Load(path) rewrites to v := path; plus the path checks. path must have an ordinary (non-linear) type.
call Path_Store(path, v) rewrites to path := v; plus the path checks.
Map_MakeEmpty has no rewriting: like Loc_New, Tag_New and
Tags_New it is {:inline 1} with a real body, and
LinearRewriter passes the call through untouched. Those four are still
primitives —
type Res;
procedure Demo({:linear_in} r: One Res)
{
var {:linear} m: Map (One Res) int;
var {:linear} r': One Res;
var v: int;
r' := r;
call m := Map_MakeEmpty();
call Map_Put(m, r', 42);
assert Map_Contains(m, r');
call v := Map_Get(m, r');
assert v == 42;
assert !Map_Contains(m, r');
}
boogie /lib:base lin-map.bpl
Boogie program verifier finished with 1 verified, 0 errors
Note the direction of the linear annotations on Map_Get: the key is {:linear_out} (ownership of the key comes back to the caller) and the returned value is {:linear}. Getting this backwards produces a type error about out-parameter 0.
When the check fails you get the primitive’s own message, and the inlined library body appears in the trace:
type Res;
procedure Demo({:linear_in} r: One Res)
{
var {:linear} m: Map (One Res) int;
var {:linear} r': One Res;
var v: int;
r' := r;
call m := Map_MakeEmpty();
call v := Map_Get(m, r'); // m is empty
}
boogie /lib:base lin-get-fail.bpl
lin-get-fail.bpl(11,3): Error: Map_Get failed
Execution trace:
lin-get-fail.bpl(9,6): anon0
base.bpl(277,5): inline$Map_MakeEmpty_2739_23$0$anon0
lin-get-fail.bpl(9,6): anon0$1
Boogie program verifier finished with 0 verified, 1 error
The pool operations, on a UnitMap:
type Res;
procedure Pool({:linear_in} p: UnitMap (One Res), a: One Res, b: One Res)
requires Map_Contains(p, a);
{
var {:linear} pool: UnitMap (One Res);
var {:linear} half: UnitMap (One Res);
var {:linear} x: One Res;
pool := p;
call One_Get(pool, a);
assert !Map_Contains(pool, a);
x := a;
call One_Put(pool, x);
assert Map_Contains(pool, a);
call half := Map_Split(pool, Set_Singleton(a));
assert Map_Contains(half, a) && !Map_Contains(pool, a);
call Map_Join(pool, half);
assert Map_Contains(pool, a);
}
boogie /lib:base lin-pool.bpl
Boogie program verifier finished with 1 verified, 0 errors
type Res;
procedure GetMissing({:linear_in} p: UnitMap (One Res), a: One Res)
{
var {:linear} pool: UnitMap (One Res);
pool := p;
call One_Get(pool, a);
}
procedure SplitTooMuch({:linear_in} p: UnitMap (One Res), s: [One Res]bool)
{
var {:linear} pool: UnitMap (One Res);
var {:linear} half: UnitMap (One Res);
pool := p;
call half := Map_Split(pool, s);
}
boogie /lib:base lin-fail.bpl
lin-fail.bpl(7,3): Error: One_Get failed
Execution trace:
lin-fail.bpl(6,8): anon0
lin-fail.bpl(15,3): Error: Map_Split failed
Execution trace:
lin-fail.bpl(14,8): anon0
Boogie program verifier finished with 0 verified, 2 errors
And a two-step path through a Map of Cells, showing both the successful load/store and the path check:
type Res;
procedure Paths({:linear_in} p: Map (One Res) (Cell Res int), a: One Res)
requires Map_Contains(p, a);
{
var {:linear} m: Map (One Res) (Cell Res int);
var v: int;
m := p;
call v := Path_Load(m->val[a]->val);
call Path_Store(m->val[a]->val, v + 1);
call v := Path_Load(m->val[a]->val);
assert v == Map_At(p, a)->val + 1;
}
procedure BadPath({:linear_in} p: Map (One Res) (Cell Res int), a: One Res)
{
var {:linear} m: Map (One Res) (Cell Res int);
var v: int;
m := p;
call v := Path_Load(m->val[a]->val); // a may not be in m
}
boogie /lib:base lin-path.bpl
lin-path.bpl(22,29): Error: map lookup failed
Execution trace:
lin-path.bpl(21,5): anon0
Boogie program verifier finished with 1 verified, 1 error
11.9.3 Surprise: Map_Put assumes freshness of a linear key
The assume !Map_Contains(path, k) inserted by Map_Put for a linear key
is not visible anywhere in base.bpl, and it is not an assertion —
type Res;
// Map_Put with a linear key ASSUMES the key was not already in the map.
procedure PutLinearKey({:linear_in} p: Map (One Res) int, {:linear_in} r: One Res)
{
var {:linear} m: Map (One Res) int;
var {:linear} r': One Res;
m := p;
r' := r;
call Map_Put(m, r', 1);
assert !Map_Contains(p, r);
}
// With an ordinary key type no such assumption is made.
procedure PutOrdinaryKey({:linear_in} p: Map int int, k: int)
{
var {:linear} m: Map int int;
m := p;
call Map_Put(m, k, 1);
assert !Map_Contains(p, k);
}
boogie /lib:base lin-put-fresh.bpl
lin-put-fresh.bpl(22,3): Error: this assertion could not be proved
Execution trace:
lin-put-fresh.bpl(20,5): anon0
Boogie program verifier finished with 1 verified, 1 error
The first procedure verifies. This is the mechanism that makes allocation work (see the next subsection), but it is worth knowing that it exists.
11.9.4 Allocation: Loc_New, Tag_New, Tags_New
pure procedure {:inline 1} Loc_New() returns ({:linear} {:pool "Loc_New"} l: One Loc)
{
assume {:add_to_pool "Loc_New", l} true;
}
pure procedure {:inline 1} Tag_New() returns ({:linear} {:pool "Loc_New"} l: One Loc, {:linear} tag: One (Tag Unit))
{
assume {:add_to_pool "Loc_New", l} true;
tag := One(Tag(l->val, Unit()));
}
pure procedure {:inline 1} Tags_New<V>(vals: [V]bool) returns ({:linear} {:pool "Loc_New"} l: One Loc, {:linear} tags: UnitMap (One (Tag V)))
{
assume {:add_to_pool "Loc_New", l} true;
tags := Map(Tags(l->val, vals), MapConst(Unit()));
}
These bodies contain no freshness assumption of any kind. The assume ... true is a no-op whose only purpose is the {:add_to_pool} annotation, which registers the new location in the "Loc_New" instantiation pool. Two calls to Loc_New therefore do not give you two distinct locations:
procedure Fresh()
{
var {:linear} l1: One Loc;
var {:linear} l2: One Loc;
call l1 := Loc_New();
call l2 := Loc_New();
assert l1 != l2;
}
procedure Tagging()
{
var {:linear} l: One Loc;
var {:linear} t: One (Tag Unit);
call l, t := Tag_New();
assert t->val->loc == l->val;
}
procedure ManyTags(vals: [int]bool)
{
var {:linear} l: One Loc;
var {:linear} ts: UnitMap (One (Tag int));
call l, ts := Tags_New(vals);
assert (forall v: int :: vals[v] ==> Map_Contains(ts, One(Tag(l->val, v))));
}
boogie /lib:base lin-loc.bpl
lin-loc.bpl(8,3): Error: this assertion could not be proved
Execution trace:
lin-loc.bpl(6,3): anon0
base.bpl(292,3): inline$Loc_New$0$anon0
base.bpl(292,3): inline$Loc_New$1$anon0
lin-loc.bpl(6,3): anon0$2
Boogie program verifier finished with 2 verified, 1 error
Freshness in real code comes from somewhere else: the linear type system guarantees that permissions of simultaneously-available linear variables are disjoint, and Civl injects the corresponding assume at procedure entry, at loop headers and after parallel calls (not after ordinary calls). More practically, the idiom used throughout Test/civl/ is to allocate and immediately insert:
call one_loc := Loc_New();
call Map_Put(list->nodes, one_loc, node);
at which point the Map_Put rewriting supplies
assume !Map_Contains(list->nodes, one_loc) —
11.9.5 Move, Copy, Assume, Assert
/// Helpers
pure procedure Copy<T>(v: T) returns (v': T);
ensures v' == v;
pure procedure Assume(b: bool);
ensures b;
pure action Assert(b: bool)
{
assert b;
}
Copy(v) returns a value equal to v. Neither its parameter nor its result is marked linear, so it is the way to take an ordinary, duplicable snapshot of a value you hold linearly.
Assume(b) is a body-less procedure with ensures b. It is assume b spelled as a call, and it is exactly as dangerous.
Assert(b) is a pure action —
an action, not a procedure — so it can only be called from an action body. Calling it from an ordinary procedure gives Error: a procedure may only call other procedures. Move(u, v) asserts u == v and transfers the permission from u to v. Its purpose is to satisfy a {:linear_out} out-parameter with a value you computed into a local. Both arguments must be plain variables.
procedure Copying(x: Vec int) returns (y: Vec int)
{
call y := Copy(x);
assert y == x;
}
// Assume has no body and an unconditional postcondition, so it assumes anything.
procedure Assuming()
{
call Assume(false);
assert 1 == 2;
}
// Without the call to Assume the same assertion fails.
procedure NotAssuming()
{
assert 1 == 2;
}
boogie /lib:base helpers.bpl
helpers.bpl(17,3): Error: this assertion could not be proved
Execution trace:
helpers.bpl(17,3): anon0
Boogie program verifier finished with 2 verified, 1 error
The Move idiom, and the restriction on its arguments:
type Res;
// The Move idiom: hand a freshly split sub-map to a {:linear_out} parameter.
pure action Extract({:linear_in} pool: UnitMap (One Res), {:linear_out} l: UnitMap (One Res))
returns ({:linear} pool': UnitMap (One Res))
{
var _l: UnitMap (One Res);
assert Set_IsSubset(l->dom, pool->dom);
pool' := pool;
call _l := Map_Split(pool', l->dom);
call Move(_l, l);
}
// Move requires both arguments to be plain variables.
pure action BadMove({:linear_in} pool: UnitMap (One Res), {:linear_out} l: UnitMap (One Res))
{
call Move(pool, Map_Empty());
}
boogie /lib:base move.bpl
move.bpl(17,2): Error: argument at position 1 must be a variable
move.bpl(17,2): Error: only variable can be passed to linear parameter: Map_Empty_3201_2239()
2 type checking errors detected in move.bpl
11.9.6 Map well-formedness is free in Civl
One more thing the Civl pipeline does for you: for every available linear variable of type Map K V, it assumes Map_WellFormed at procedure entry, at loop headers and after parallel calls. Outside Civl you get nothing:
procedure Plain({:linear} a: Map int int)
{
assert Map_WellFormed(a);
}
yield procedure {:layer 1} Yielding({:layer 1} {:linear} a: Map int int)
{
assert {:layer 1} Map_WellFormed(a);
}
boogie /lib:base fmap-civl-wf.bpl
fmap-civl-wf.bpl(3,3): Error: this assertion could not be proved
Execution trace:
fmap-civl-wf.bpl(3,3): anon0
Boogie program verifier finished with 2 verified, 1 error
11.10 The node library
/lib:node adds a singly-linked-list node type, a reachability axiomatisation in the style of Nelson’s Between predicate, and three abstraction functions with framing lemmas. It requires /lib:base.
11.10.1 Node, Between and Avoiding
datatype Node<T> { Node(next: Option Loc, val: T) }
function Between<T>(f: [One Loc]Node T, x: Option Loc, y: Option Loc, z: Option Loc): bool;
function Avoiding<T>(f: [One Loc]Node T, x: Option Loc, y: Option Loc, z: Option Loc): bool;
function {:inline} BetweenSet<T>(f:[One Loc]Node T, x: Option Loc, z: Option Loc): [Loc]bool
{
(lambda y: Loc :: Between(f, x, Some(y), z))
}
A node has a next: Option Loc (with None() as null) and a payload
val. The heap is modelled as a map f: [One Loc]Node T —
Between(f, x, y, z) —
following next from x, you reach y, and you reach it at or before z. Avoiding(f, x, y, z) —
following next from x, you reach y without first passing through z. BetweenSet(f, x, z) —
the set of Locs on the path from x up to z, as a [Loc]bool. Both ends are included: if x is Some(u) and z is reachable from it then u is in the set, and if z is Some(w) and reachable then w is too.
Both predicates are uninterpreted and are characterised by fourteen axioms. In
source order they are: reflexive (Between(f, x, x, x)),
step (one next hop is reachable), reach (reachability
decomposes through one hop), cycle (a self-loop reaches only itself),
sandwich (Between(f, x, y, x) implies x == y),
order1 and order2 (the reachable set from a point is totally
ordered, and a three-point Between splits into two two-point ones),
transitive1, transitive2 and transitive3, one extra
axiom that the source itself annotates —
// This axiom is required to deal with the incompleteness of the trigger for the reflexive axiom.
// It cannot be proved using the rest of the axioms.
axiom {:ctor "Node"} (forall<T> f: [One Loc]Node T, u: Option Loc, x: Option Loc ::
{Between(f, u, x, x)}
Between(f, u, x, x) ==> Between(f, u, u, x));
—
// update
axiom {:ctor "Node"} (forall<T> f: [One Loc]Node T, u: Option Loc, v: Option Loc, x: Option Loc, p: Loc, q: Node T ::
{Avoiding(f[One(p) := q], u, v, x)}
Avoiding(f[One(p) := q], u, v, x) <==>
(Avoiding(f, u, v, Some(p)) && Avoiding(f, u, v, x)) ||
(Avoiding(f, u, Some(p), x) && Some(p) != x && Avoiding(f, q->next, v, Some(p)) && Avoiding(f, q->next, v, x))
);
Every one of these axioms carries the vestigial {:ctor "Node"} attribute, which as noted above nothing reads.
type X;
procedure Reflexive(f: [One Loc]Node X, x: Option Loc)
{
assert Between(f, x, x, x);
}
procedure Step(f: [One Loc]Node X, x: Loc)
{
assert Between(f, Some(x), f[One(x)]->next, f[One(x)]->next);
}
procedure Sandwich(f: [One Loc]Node X, x: Option Loc, y: Option Loc)
requires Between(f, x, y, x);
{
assert x == y;
}
procedure Transitive(f: [One Loc]Node X, x: Option Loc, y: Option Loc, z: Option Loc)
requires Between(f, x, y, y);
requires Between(f, y, z, z);
{
assert Between(f, x, z, z);
}
procedure AvoidingLink(f: [One Loc]Node X, x: Option Loc, y: Option Loc, z: Option Loc)
{
assert Between(f, x, y, z) <==> Avoiding(f, x, y, z) && Avoiding(f, x, z, z);
}
procedure Reachable(f: [One Loc]Node X, x: Option Loc, y: Loc)
requires Between(f, x, Some(y), None());
{
assert BetweenSet(f, x, None())[y];
}
// Both ends of the range are in BetweenSet.
procedure EndpointIncluded(f: [One Loc]Node X, x: Option Loc, w: Loc)
requires Between(f, x, Some(w), Some(w));
{
assert BetweenSet(f, x, Some(w))[w];
}
procedure StartIncluded(f: [One Loc]Node X, u: Loc, z: Option Loc)
requires Between(f, Some(u), z, z);
{
assert BetweenSet(f, Some(u), z)[u];
}
boogie /lib:base /lib:node node-between.bpl
Boogie program verifier finished with 8 verified, 0 errors
11.10.2 InDomain
function {:inline} InDomain<V>(nodes: Map (One Loc) (Node V), start: Option Loc): bool {
Between(nodes->val, start, start, None()) &&
(forall x: Loc:: Between(nodes->val, start, Some(x), None()) ==> Set_Contains(nodes->dom, One(x)))
}
"The list starting at start is null-terminated and every node on it is in the finite map’s domain." This is the standard well-formedness precondition for the abstraction lemmas below.
11.10.3 Stack, set and queue abstractions
The library provides three abstraction functions with the same shape. Each comes as an uninterpreted function XAbs, an inlined one-step unfolding XAbsDef, and two lemmas.
/// Stack abstraction
function StackAbs<V>(start: Option Loc, nodes: Map (One Loc) (Node V)): Vec V;
function {:inline} StackAbsDef<V>(start: Option Loc, nodes: Map (One Loc) (Node V)): Vec V {
if start == None() then
Vec_Empty() else
(var n := Map_At(nodes, One(start->t)); Vec_Append(StackAbs(n->next, nodes), n->val))
}
pure procedure StackAbsLemma<V>(start: Option Loc, nodes: Map (One Loc) (Node V));
requires Between(nodes->val, start, start, None());
requires InDomain(nodes, start);
ensures StackAbs(start, nodes) == StackAbsDef(start, nodes);
pure procedure StackFrameLemma<V>(start: Option Loc, nodes: Map (One Loc) (Node V), nodes': Map (One Loc) (Node V));
requires Set_IsSubset(nodes->dom, nodes'->dom);
requires MapIte(nodes->dom, nodes->val, MapConst(Default())) ==
MapIte(nodes->dom, nodes'->val, MapConst(Default()));
requires Between(nodes->val, start, start, None());
requires InDomain(nodes, start);
ensures StackAbs(start, nodes) == StackAbs(start, nodes');
StackAbs —
the list as a Vec V, last node first: the recursion appends the current node’s value after the abstraction of the tail, so the head of the list ends up at the end of the vector. SetAbs —
the set of payloads, as a [V]bool. QueueAbs(in_queue, start, nodes) —
takes an accumulator and appends as it walks forwards, so the head of the list ends up first.
The key design point: StackAbs is an uninterpreted function, and StackAbsDef is its intended defining equation, but nothing connects them until you call StackAbsLemma. This avoids putting a recursive definition directly into the background theory. The frame lemma says the abstraction is unchanged when the map grows, provided the values on the old domain are unchanged.
type X;
procedure Abs(nodes: Map (One Loc) (Node X), l: Loc, x: X)
requires Map_Contains(nodes, One(l));
requires Map_At(nodes, One(l)) == Node(None(), x);
requires InDomain(nodes, Some(l));
{
call StackAbsLemma(Some(l), nodes);
call StackAbsLemma(None(), nodes);
assert StackAbs(Some(l), nodes) == Vec_Append(Vec_Empty(), x);
call SetAbsLemma(Some(l), nodes);
call SetAbsLemma(None(), nodes);
assert SetAbs(Some(l), nodes) == Set_Singleton(x);
}
procedure WithoutLemma(nodes: Map (One Loc) (Node X), l: Loc, x: X)
requires Map_Contains(nodes, One(l));
requires Map_At(nodes, One(l)) == Node(None(), x);
requires InDomain(nodes, Some(l));
{
assert StackAbs(Some(l), nodes) == Vec_Append(Vec_Empty(), x);
}
boogie /lib:base /lib:node node-abs.bpl
node-abs.bpl(21,3): Error: this assertion could not be proved
Execution trace:
node-abs.bpl(21,3): anon0
Boogie program verifier finished with 1 verified, 1 error
Note that you need one lemma call per list node visited, including the
terminating None() step —
11.10.4 The lemmas are trusted unless you discharge them
All six *AbsLemma and *FrameLemma declarations in node.bpl are body-less pure procedures, so by default their postconditions are assumed. They can be proved. Test/civl/large-samples/node-lemmas.bpl supplies implementations for all six by writing a recursive pure procedure that walks the list, using the Between axioms to argue that the recursion terminates:
implementation StackAbsLemma<V>(start: Option Loc, nodes: Map (One Loc) (Node V))
{
var absStack: Vec V;
call absStack := StackAbsCompute(start, nodes, nodes);
}
boogie /lib:base /lib:node node-lemmas.bpl
Boogie program verifier finished with 9 verified, 0 errors
If you use /lib:node in a program where soundness matters, copying that file in is the way to turn the assumptions into proofs.
11.11 Divergences from This is Boogie 2
The standard library postdates This is Boogie 2 entirely, so there is nothing in the paper to contradict. One paper claim is nonetheless relevant to using the library:
Section 4.1: "Maps do not necessarily satisfy extensionality", with b[j := b[j]] == b given as an example of something that need not hold. Under the current SMT-array encoding it does hold, and the entire Set_* and Map_* layer relies on it.
11.12 Where to find real usage
Test/monomorphize/vector.bpl —
Vec basics, loops over vectors, recursion through a datatype containing Vec. Test/inst/vector.bpl —
the serious Vec_Concat / Vec_Slice / Vec_Ext proofs, with pool annotations. Test/datatypes/set.bpl —
Set_Size. Test/sequences/ —
Seq, both hand-declared and via /lib:base. Test/civl/samples/alloc-mem.bpl —
Cell, UnitMap, One_Get / One_Put in a small allocator. Test/civl/large-samples/treiber-stack.bpl, large-samples/lc-list.bpl, large-samples/msq.bpl and samples/two-queues.bpl —
Loc, Loc_New, Map_Get / Map_Put, Path_Load / Path_Store and the node library in anger. Test/civl/large-samples/node-lemmas.bpl —
proofs of the node.bpl lemmas. Test/civl/samples/bluetooth.bpl —
Move, Map_Split and Lemma_SetSize_Subset together.