On this page:
10.1 Declaring a datatype
10.1.1 Syntactic rules
10.1.2 Names and name spaces
10.1.3 Attributes
10.1.4 Type parameters
10.1.5 Fields shared between constructors
10.1.6 Well-foundedness
10.2 Constructors
10.3 Testers:   e is Ctor
10.4 Field access:   e->f
10.4.1 Selectors are total, but underspecified off their own constructor
10.4.2 Shared field names produce a conditional
10.5 Field update:   e->(f :  = v)
10.5.1 Trap:   updating a field the value does not have
10.6 Statements over datatypes
10.6.1 Field assignment
10.6.2 Unpack
10.7 What the prover is told
10.7.1 The properties you get
10.7.2 Counterexample models
10.8 Polymorphic datatypes and the type encoding
10.8.1 Interaction with /  type  Encoding
10.9 Datatypes, maps and quantifiers
10.9.1 Triggers
10.9.2 Recursive definitions
10.10 Pattern-style specifications
10.11 Standard library datatypes
10.12 Divergences from This is Boogie 2
8.17

10 Algebraic datatypes🔗

A datatype declaration introduces a type together with a fixed set of constructors, each of which has a list of named, typed fields. Datatype values are built by applying a constructor, taken apart with the field selector ->, discriminated with the tester is, and rebuilt with the field-update expression ->(f := e).

Datatypes are entirely absent from This is Boogie 2 (2008): there is no datatype production in the paper’s grammar, no is, no ->, and the paper’s type section (§2) recognises only type constructors, built-in types and type synonyms. Everything in this chapter postdates the paper.

Two facts shape the rest of this chapter.

10.1 Declaring a datatype🔗

Datatype<out DatatypeTypeCtorDecl datatypeTypeCtorDecl>

= "datatype"

  { Attribute }

  Ident

  [ TypeParams ]

  "{"

    Constructors

  "}"

  .

 

Constructors = Constructor { "," Constructor } .

 

Constructor  = Ident "(" [ AttributesIdsTypeWheres ] ")" .

(Source/Core/BoogiePL.atg, productions Datatype, Constructors and Constructor, with the semantic actions elided.)

A datatype declaration is a top-level declaration, on a par with type, const, function, axiom, var, procedure and implementation. It is not terminated by a semicolon — unlike type and there is no ordering requirement: a datatype may mention a datatype declared later in the file.

Because the field list uses the same AttributesIdsTypeWheres production as procedure formals, consecutive fields of the same type may share one type annotation: Point3(x, y, z: int) declares three int fields, and /print expands it back to Point3(x: int, y: int, z: int).

datatype Shape {

  Circle(r: int),

  Rect(w: int, h: int)

}

 

procedure Area(s: Shape) returns (a: int)

  requires s is Rect ==> s->w >= 0 && s->h >= 0;

  ensures  s is Rect ==> a == s->w * s->h;

{

  if (s is Circle) {

    a := 3 * s->r * s->r;

  } else {

    a := s->w * s->h;

  }

}

boogie shapes.bpl

 

Boogie program verifier finished with 1 verified, 0 errors

10.1.1 Syntactic rules🔗

The grammar is stricter than it looks; the following are all rejected.

10.1.2 Names and name spaces🔗

A datatype declaration creates one TypeCtorDecl (a subclass, DatatypeTypeCtorDecl) and one Function per constructor (a subclass, DatatypeConstructor). Consequently:

10.1.3 Attributes🔗

Attributes are accepted on the datatype (before the name) and on each field (before the field name); there is no place to attach an attribute to a constructor.

datatype {:mydt} D { C1({:myfield} f: int), C2() }

procedure P(d: D) { assert d is C1 || d is C2; }

They parse and are stored on the declaration, but no core Boogie feature reads them. In particular, Civl does not: its linear type collector decides whether a datatype carries permissions by looking at the types of the constructor fields, never at attributes attached to them. Treat datatype and field attributes as inert.

10.1.4 Type parameters🔗

Type parameters are written in angle brackets and separated by commas. This is not the syntax used by ordinary type constructors, which take whitespace-separated parameters and no brackets (type Barrel _;). Applying the datatype to arguments, on the other hand, uses the ordinary juxtaposition syntax: Split int bool, Map (One Loc) (Node int).

datatype Split<X, Y> {

  Left(i: X, j: Y),

  Right(i: X, k: Y)

}

 

procedure P(s: Split int bool) returns (t: Split int bool)

{

  t := s->(i := 5);

  assert t->i == 5;

}

 

Boogie program verifier finished with 1 verified, 0 errors

Every constructor of a datatype is typed with all of the datatype’s type parameters, regardless of which of them it actually uses, and its result type is the datatype applied to those parameters. So Left above has signature Left<X, Y>(i: X, j: Y): Split X Y.

10.1.5 Fields shared between constructors🔗

The same field name may appear in more than one constructor of a datatype. When it does, all occurrences must have the same type, after normalising through each constructor’s result type:

datatype Split<X, Y> {

  Left(i: X, j: Y),

  Right(i: Y, k: X)

}

shared4.bpl(3,8): Error: type mismatch between field i and identically-named field in constructor Left

1 type checking errors detected in shared4.bpl

The message does not print the expected type; the constructor it names is always the first constructor that declares the field, in declaration order, and it is that constructor’s field type every later one is compared against (DatatypeTypeCtorDecl.Typecheck). Sharing a field name changes the meaning of -> and of field update; see Field access: e->f and Field update: e->(f := v).

10.1.6 Well-foundedness🔗

After resolution, Program.CheckDatatypesWellFounded computes the least set of datatypes that have at least one constructible constructor, where a constructor is constructible if every field type is constructible, a map type is constructible if its index and result types are, a datatype is constructible if it is already in the set, and every other type is constructible. Any datatype not in the fixpoint is an error.

In practice this means: every datatype needs a reachable base case, and a recursive occurrence hidden under a map type does not count as a base case.

datatype Stream { Cons(hd: int, tl: Stream) }

(0,-1): Error: Datatype declarations are not well-founded: Stream

1 name resolution errors detected in wf1.bpl

datatype T { T(f: [int]T) }

(0,-1): Error: Datatype declarations are not well-founded: T

1 name resolution errors detected in wf2.bpl

Note that the error is reported with the pseudo-position (0,-1) and lists all offending datatypes at once, so a mutual cycle produces one message naming the whole group. The shipped test Test/datatypes/cycle.bpl reports List1, List2, Foo, List3, List4 in a single error.

Mutual recursion is fine as long as the group has a base case, and a map-typed recursive field is fine as long as the datatype has some other constructible constructor:

datatype Rec { R(f: [int]Rec, g: int), Base() }

procedure P(r: Rec) { assert r is R || r is Base; }

 

Boogie program verifier finished with 1 verified, 0 errors

10.2 Constructors🔗

A constructor is an ordinary Boogie function, so it is applied with the ordinary call syntax and obeys the ordinary type-inference and arity rules. Nullary constructors still need the empty argument list, both at declaration and at use; Red on its own is an undeclared identifier.

datatype Color { Red(), Green() }

procedure P() { assert Red != Green; }

e3.bpl(2,23): Error: undeclared identifier: Red

e3.bpl(2,30): Error: undeclared identifier: Green

2 name resolution errors detected in e3.bpl

Type parameters are inferred from the arguments as for any polymorphic function. If a constructor mentions a type parameter in none of its fields, the parameter is unconstrained at that call site and Boogie warns and picks int:

datatype Opt<T> { None(), Some(v: T) }

procedure P() { assert None() is None; }

phantom2.bpl(2,30): Warning: type parameter T is ambiguous, instantiating to int

 

Boogie program verifier finished with 1 verified, 0 errors

In a context that fixes the type, inference succeeds silently:

datatype Opt<T> { None(), Some(v: T) }

 

procedure P() returns (a: Opt int, b: Opt bool)

{

  a := None();

  b := None();

  assert a is None && b is None;

}

 

Boogie program verifier finished with 1 verified, 0 errors

Arity and argument-type errors are reported by the generic function machinery (these two lines come from the errs.bpl program shown in full in Field access: e->f):

errs.bpl(8,9): Error: wrong number of arguments to function: A1 (0 instead of 1)

errs.bpl(9,12): Error: invalid type for argument 0 in application of A1: bool (expected: int)

Because constructors are functions, they may appear anywhere a function application may: in axioms, in triggers, in {:define} function bodies, in lambdas, and as procedure arguments.

10.3 Testers: e is Ctor🔗

Power = IsConstructor [ "**" Power ] .

 

IsConstructor

= UnaryExpression

  [ "is" Ident ]

  .

e is C has type bool and holds when e was built by constructor C. Its left operand must have a datatype type and C must be a constructor of that datatype. The next three lines, and every other field_access_type_error.bpl excerpt in this chapter, come from the shipped test Test/datatypes/field_access_type_error.bpl, which collects the datatype-related type errors in one file:

field_access_type_error.bpl(8,14): Error: is-constructor must be applied to a datatype, int is not a datatype

field_access_type_error.bpl(13,14): Error: is-constructor must be applied to a datatype, T is not a datatype

field_access_type_error.bpl(22,14): Error: datatype Perm does not have a constructor with name Middle

Precedence. is sits between UnaryExpression and Power in the expression grammar. It therefore binds tighter than **, than the multiplicative and additive operators, than the relational operators, than ==, and than the boolean connectives, and looser than ->, [] and prefix !/-. So

d is C1 && d->f == 0

parses as (d is C1) && ((d->f) == 0) which is what you want — and

d->f is C2

parses as (d->f) is C2. But because prefix ! is part of UnaryExpression, negation binds tighter than is, and the natural-looking form is a type error:

datatype D { C1(f: int), C2(g: int) }

procedure P(d: D) { assert !d is C1; }

prec1.bpl(2,27): Error: invalid argument type (D) to unary operator !

1 type checking errors detected in prec1.bpl

Write !(d is C1). Binding tighter than arithmetic is equally surprising in the other direction — 1 + d is C1 groups as 1 + (d is C1):

datatype D { C1(f: int), C2(g: int) }

procedure P(d: D) { assert 1 + d is C1 == 2; }

arith.bpl(2,29): Error: invalid argument types (int and bool) to binary operator +

1 type checking errors detected in arith.bpl

There is no chaining: the is suffix is optional but not repeatable.

datatype D { C1(f: int), C2(g: int) }

procedure P(d: D) { assert d is C1 is C2; }

chain.bpl(2,36): error: ";" expected

1 parse errors detected in chain.bpl

10.4 Field access: e->f🔗

ArrayExpression

= AtomExpression

  { "[" ... "]"

    |

      "->"

      (

        Ident

        |

        "(" Ident ":=" Expression ")"

      )

  }

  .

e->f selects the field named f. The -> and [] suffixes are in the same repetition of the same production, so selection, subscripting and update compose freely and associate to the left, and any AtomExpression including a function application, an old, a parenthesised expression — can be the receiver.

datatype Inner { Inner(v: int) }

datatype Outer { Outer(m: [int]Inner, n: int) }

function mk(i: int): Outer;

var g: Outer;

 

procedure P(o: Outer, i: int)

  modifies g;

{

  assert o->m[i]->v == o->m[i]->v;         // selection, subscript, selection

  assert mk(i)->n == mk(i)->n;             // f(y)->z

  assert (o->(n := 5))->n == 5;            // update, then select

  g := o->(n := 7)->(n := 8);              // chained updates

  assert g->n == 8 && old(g)->n == old(g)->n;

}

 

Boogie program verifier finished with 1 verified, 0 errors

Resolution of ->f is deferred to type checking, because the field’s meaning depends on the datatype of the receiver, which is only known then. Type checking requires the receiver’s type to expand to a datatype and the datatype to declare the field:

datatype A { A1(f: int), A2() }

datatype B { B1(g: int) }

 

procedure P(a: A, b: B)

{

  assert a is B1;        // constructor of another datatype

  assert a->g == 0;      // field of another datatype

  assert A1() == a;      // wrong arity

  assert A1(true) == a;  // wrong argument type

}

errs.bpl(6,14): Error: datatype A does not have a constructor with name B1

errs.bpl(7,12): Error: datatype A does not have a field with name g

errs.bpl(8,9): Error: wrong number of arguments to function: A1 (0 instead of 1)

errs.bpl(9,12): Error: invalid type for argument 0 in application of A1: bool (expected: int)

4 type checking errors detected in errs.bpl

The two remaining diagnostics come from applying -> to a non-datatype:

field_access_type_error.bpl(7,12): Error: field-access must be applied to a datatype, int is not a datatype

field_access_type_error.bpl(12,12): Error: field-access must be applied to a datatype, T is not a datatype

10.4.1 Selectors are total, but underspecified off their own constructor🔗

A selector is a total function. s->w is well-defined even when s is a Circle; it just denotes an unspecified value. It is the same unspecified value for the same s selectors are functions, not nondeterministic reads — but nothing else is known about it.

datatype Shape { Circle(r: int), Rect(w: int, h: int) }

 

procedure P(s: Shape)

{

  // selector of the "wrong" constructor is underspecified, not an error

  assume s is Circle;

  assert s->w == s->w;   // reflexivity holds

  assert s->w == 0;      // but nothing else is known

}

e1.bpl(8,3): Error: this assertion could not be proved

Execution trace:

    e1.bpl(6,3): anon0

 

Boogie program verifier finished with 0 verified, 1 error

This is a real source of vacuous-looking specifications: a postcondition like ensures r->hd == 0; on a value that turns out to be Nil constrains nothing useful. Guard field accesses with is in specifications where the constructor is not otherwise determined.

10.4.2 Shared field names produce a conditional🔗

When a field name occurs in several constructors, e->f expands to a conditional over the testers. Concretely, Boogie2VCExpr starts from the accessor of the first constructor that declares f and wraps each later one in an if:

datatype D { C1(f: int, g: int), C2(f: int, h: bool) }

 

procedure P(d: D) returns (e: D)

{

  e := d->(f := 7);

  assert e->f == 7;

  assert (d is C1) == (e is C1);

  assert (d is C2) == (e is C2);

}

The verification condition (from /proverLog:) shows the shape exactly — e->f became (ite (is-C2 e) (f#C2 e) (f#C1 e)):

(assert (not

 (=> (= (ControlFlow 0 0) 5) (let ((anon0_correct  (=> (= e@0 (ite (is-C2 d) (C2 7 (|h#C2| d)) (C1 7 (|g#C1| d)))) (and (=> (= (ControlFlow 0 2) (- 0 4)) (= (ite (is-C2 e@0) (|f#C2| e@0) (|f#C1| e@0)) 7)) (=> (= (ite (is-C2 e@0) (|f#C2| e@0) (|f#C1| e@0)) 7) (and (=> (= (ControlFlow 0 2) (- 0 3)) (= (is-C1 d) (is-C1 e@0))) (=> (= (is-C1 d) (is-C1 e@0)) (=> (= (ControlFlow 0 2) (- 0 1)) (= (is-C2 d) (is-C2 e@0))))))))))

(let ((PreconditionGeneratedEntry_correct  (=> (= (ControlFlow 0 5) 2) anon0_correct)))

PreconditionGeneratedEntry_correct)))

))

The first constructor that declares f is the fall-through branch, so for a value of a constructor that does not declare f at all, e->f silently reads that constructor’s selector — again a total but unconstrained value.

10.5 Field update: e->(f := v)🔗

e->(f := v) is an expression of the same type as e, denoting e with field f replaced by v. It is pure syntactic sugar: FieldUpdate is expanded by FieldAccess.Update into a constructor application whose other arguments are field selections of the original value. The type checker requires the right-hand side to match the field’s type:

field_access_type_error.bpl(61,11): Error: right-hand side in field update with wrong type: bool (expected: int)

For a field declared in exactly one constructor C(f1, ..., fn), the expansion is C(e->f1, ..., v, ..., e->fn). For a shared field the expansion is a conditional, as shown in Field access: e->f: d->(f := 7) became (ite (is-C2 d) (C2 7 (h#C2 d)) (C1 7 (g#C1 d))).

10.5.1 Trap: updating a field the value does not have🔗

Because the expansion always produces a constructor application, and because the fall-through case is the first constructor that declares the field, updating a field on a value of a constructor that does not declare that field silently changes the constructor. There is no error, no warning, and no proof obligation.

datatype D { C1(f: int, g: int), C2(x: bool) }

 

procedure P(d: D) returns (e: D)

  requires d is C2;

{

  e := d->(f := 7);

  assert e is C1;      // the update silently changed the constructor

}

 

Boogie program verifier finished with 1 verified, 0 errors

The verification condition confirms that e is literally (C1 7 (g#C1 d)), built out of an unconstrained g#C1 read of a C2 value:

(assert (not

 (=> (= (ControlFlow 0 0) 3) (let ((anon0_correct  (=> (and (= e@0 (C1 7 (|g#C1| d))) (= (ControlFlow 0 2) (- 0 1))) (is-C1 e@0))))

(let ((PreconditionGeneratedEntry_correct  (=> (and (is-C2 d) (= (ControlFlow 0 3) 2)) anon0_correct)))

PreconditionGeneratedEntry_correct)))

))

If you rely on a field update preserving the constructor, assert or assume the tester explicitly.

10.6 Statements over datatypes🔗

10.6.1 Field assignment🔗

LabelOrAssign

= Ident

  ( ...

  | { MapAssignIndex | FieldAccess }

    { "," Ident { MapAssignIndex | FieldAccess } }

    ":=" { Attribute } Expression { "," Expression } ";"

  )

  .

 

FieldAccess = "->" Ident .

An assignment left-hand side is an identifier followed by any sequence of map indexings and field selections. Note that the left-hand side form is ->Ident only; the parenthesised update form ->(f := e) is an expression and cannot appear on the left.

x->f := e; is shorthand for x := x->(f := e);, and the desugaring composes down an arbitrary path.

datatype Point { Point(x: int, y: int) }

datatype Line  { Line(from: Point, to: Point) }

 

var grid: [int]Point;

 

procedure P(i: int)

  modifies grid;

{

  var l: Line;

  var p: Point;

 

  p := Point(1, 2);

  p->x := 10;                 // field assignment

  assert p == Point(10, 2);

 

  l := Line(p, p);

  l->from->y := 5;            // nested field assignment

  assert l->from == Point(10, 5);

  assert l->to == p;

 

  grid[i]->x := 3;            // field of a map element

  assert grid[i]->x == 3;

}

 

Boogie program verifier finished with 1 verified, 0 errors

Maps and fields interleave in either order:

datatype Inner { Inner(v: int) }

datatype Outer { Outer(m: [int]Inner, n: int) }

 

procedure P(o: Outer, i: int) returns (o': Outer)

{

  o' := o;

  o'->m[i] := Inner(7);          // map inside a datatype field

  assert o'->m[i]->v == 7;

  o'->m[i]->v := 8;              // and back down into the element

  assert o'->m[i] == Inner(8);

  assert o'->n == o->n;

}

 

Boogie program verifier finished with 1 verified, 0 errors

Since the ultimate target of the assignment is the root variable (FieldAssignLhs.DeepAssignedVariable delegates to the receiver), a field assignment to a global requires that global in the modifies clause:

datatype Point { Point(x: int, y: int) }

var g: Point;

 

procedure P()

{

  g->x := 1;

}

famod.bpl(6,7): Error: command assigns to a global variable that is not in the enclosing procedure's modifies clause: g

1 type checking errors detected in famod.bpl

A field left-hand side may be one of several targets of a simultaneous assignment:

datatype Point { Point(x: int, y: int) }

 

procedure P() returns (p: Point, q: Point)

{

  p, q->x := Point(0, 0), 1;      // several lhss, one of them a field

  assert p == Point(0, 0) && q->x == 1;

}

 

Boogie program verifier finished with 1 verified, 0 errors

but it may not be an output of a call (invalid CallParams) nor an operand of havoc, which both take bare identifiers.

10.6.2 Unpack🔗

LabelOrAssign

= Ident

  ( ...

  | "(" Idents ")" ":=" { Attribute } Expression ";"

  | ...

  )

  .

C(x1, ..., xn) := e; is the unpack command: it asserts that e was built with C and then assigns each field of e to the corresponding variable. UnpackCmd.ComputeDesugaring produces exactly

assert e is C;

x1, ..., xn := e->f1, ..., e->fn;

where the assertion carries the description "the precondition for unpack could not be proved".

datatype Shape { Circle(r: int), Rect(w: int, h: int) }

 

procedure P(s: Shape) returns (a: int)

  requires s is Rect;

{

  var w, h: int;

  Rect(w, h) := s;            // unpack

  a := w * h;

}

 

procedure Q(s: Shape) returns (a: int)

{

  var r: int;

  Circle(r) := s;             // no guarantee that s is a Circle

  a := r;

}

unpack.bpl(14,13): Error: the precondition for unpack could not be proved

Execution trace:

    unpack.bpl(14,13): anon0

 

Boogie program verifier finished with 1 verified, 1 error

Restrictions, all checked:

Putting unpack and field assignment together, this procedure

datatype Shape { Circle(r: int), Rect(w: int, h: int) }

 

procedure P(s: Shape) returns (t: Shape)

  requires s is Rect;

  ensures t->w == s->w + 1;

{

  var w, h: int;

  Rect(w, h) := s;

  t := s;

  t->w := w + 1;

}

produces this verification condition, in which every datatype operation has become an SMT-LIB constructor, selector or tester application:

(assert (not

 (=> (= (ControlFlow 0 0) 4) (let ((anon0_correct  (and (=> (= (ControlFlow 0 2) (- 0 3)) (is-Rect s)) (=> (is-Rect s) (=> (and (and (= w@0 (|w#Rect| s)) (= h@0 (|h#Rect| s))) (and (= t@0 (Rect (+ w@0 1) (|h#Rect| s))) (= (ControlFlow 0 2) (- 0 1)))) (= (|w#Rect| t@0) (+ (|w#Rect| s) 1)))))))

(let ((PreconditionGeneratedEntry_correct  (=> (and (is-Rect s) (= (ControlFlow 0 4) 2)) anon0_correct)))

PreconditionGeneratedEntry_correct)))

))

10.7 What the prover is told🔗

Boogie emits no axioms for datatypes. TypeDeclCollector deliberately skips DatatypeConstructors when declaring functions and returns early for datatype sorts; instead SMTLibProcessTheoremProver.PrepareDataTypes groups the datatypes into strongly connected components of the "field type mentions" graph and emits one declare-datatypes per component. For the Shape example above:

(declare-datatypes ((T@Shape 0)) (((Circle (|r#Circle| Int) ) (Rect (|w#Rect| Int) (|h#Rect| Int) ) ) ))

The naming scheme, useful when reading a /proverLog: file:

Name collisions with user declarations are handled by the prover namer, which appends @@n; a user function genuinely called f#C (# is a legal identifier character in Boogie) coexists with the selector f#C without trouble.

All datatypes in the program are declared, whether or not the implementation being verified mentions them, and /prune does not change this:

datatype Used { U(v: int) }

datatype Unused { W(v: int) }

procedure P(u: Used) { assert u is U; }

(declare-datatypes ((T@Unused 0)) (((W (|v#W| Int) ) ) ))

(declare-datatypes ((T@Used 0)) (((U (|v#U| Int) ) ) ))

10.7.1 The properties you get🔗

Everything below follows from the SMT datatype theory, so it holds without any user axioms:

datatype Shape { Circle(r: int), Rect(w: int, h: int) }

 

// distinctness

procedure Distinct(x: int, y: int, z: int) { assert Circle(x) != Rect(y, z); }

 

// injectivity

procedure Injective(a: int, b: int) { assert Rect(a, b) == Rect(b, a) ==> a == b; }

 

// exhaustiveness

procedure Exhaustive(s: Shape) { assert s is Circle || s is Rect; }

 

// surjectivity of each constructor

procedure Surjective(s: Shape) { assert s is Rect ==> s == Rect(s->w, s->h); }

 

// finiteness of an enumeration

procedure Enumeration() {

  assert (forall s: Shape :: s is Circle || s is Rect);

}

 

// selectors are total functions, but unconstrained off their own constructor

procedure Total() { assert Circle(1)->w == Circle(1)->w; }

procedure Unconstrained() { assert Circle(1)->w == Circle(2)->w; }

axioms.bpl(22,29): Error: this assertion could not be proved

Execution trace:

    axioms.bpl(22,29): anon0

 

Boogie program verifier finished with 6 verified, 1 error

Only the last procedure fails, as it should. Recursive datatypes additionally get acyclicity for free:

datatype List { Nil(), Cons(hd: int, tl: List) }

 

procedure P(xs: List)

{

  assert Cons(1, xs) != xs;               // acyclicity

  assert Cons(1, Nil()) != Cons(2, Nil()); // injectivity

  assert xs is Nil || xs is Cons;          // exhaustiveness

  assert !(xs is Nil && xs is Cons);       // constructors are disjoint

  assert xs is Cons ==> xs == Cons(xs->hd, xs->tl);  // surjectivity of constructors

}

 

Boogie program verifier finished with 1 verified, 0 errors

What you do not get is an induction principle. Any property that needs induction over a recursive datatype has to be proved by other means — a recursive pure procedure whose postcondition is the inductive hypothesis is the idiom used by the standard library and the shipped tests.

10.7.2 Counterexample models🔗

Datatype values appear in /mv: output as constructor applications:

datatype Shape { Circle(r: int), Rect(w: int, h: int) }

procedure P(s: Shape) { assert s is Circle; }

boogie /mv:- model.bpl

model.bpl(2,25): Error: this assertion could not be proved

Execution trace:

    model.bpl(2,25): anon0

*** MODEL

s -> (Rect 4 5)

ControlFlow -> {

  0 0 -> 3

  0 2 -> (- 1)

  0 3 -> 2

  else -> (- 1)

}

tickleBool -> {

  false -> true

  true -> true

  else -> true

}

*** STATE <initial>

  s -> (Rect 4 5)

*** END_STATE

*** END_MODEL

 

Boogie program verifier finished with 0 verified, 1 error

10.8 Polymorphic datatypes and the type encoding🔗

declare-datatypes declares sorts of arity 0, so a polymorphic datatype cannot be sent to the prover as such. SMTLibProverContext.DeclareType throws "Polymorphic datatypes are not supported" if one ever reaches it. Boogie avoids that by monomorphising: for each type instantiation actually used, the monomorphiser creates a fresh, ground DatatypeTypeCtorDecl with fresh constructor names.

datatype Box<T> { Box(v: T) }

 

procedure P<A>(a: A) returns (b: Box A)

{

  b := Box(a);

  assert b->v == a;

  assert b is Box;

}

 

Boogie program verifier finished with 1 verified, 0 errors

The prover sees a monomorphic instance whose name carries a disambiguating suffix:

(declare-sort T@A_46 0)

(declare-datatypes ((T@Box_68 0)) (((Box_68 (|v#Box_68| T@A_46) ) ) ))

10.8.1 Interaction with /typeEncoding🔗

ExecutionEngine decides as follows, immediately after type checking:

So a monomorphic datatype program is happily verified under /typeEncoding:p, because the option is overridden before the datatype check is reached — this is the Shape program from Declaring a datatype:

boogie /typeEncoding:p shapes.bpl

 

Boogie program verifier finished with 1 verified, 0 errors

while a polymorphic one is not:

boogie /typeEncoding:p poly.bpl

Datatypes only supported with monomorphic encoding

The same message appears for /typeEncoding:a. Note that the message is printed as a fatal error with no source location, and that the actual trigger is "the program needed monomorphisation and has datatypes", not "the program has polymorphic datatypes".

10.9 Datatypes, maps and quantifiers🔗

Datatypes compose with maps in both directions. A datatype may be a map’s domain or range, and a map may be the type of a datatype field.

datatype Key { Key(id: int, tag: bool) }

 

// a datatype may be used as a map domain and as a map range

var table: [Key]int;

 

procedure P(k: Key)

  modifies table;

{

  table[k] := 1;

  table[Key(k->id, !k->tag)] := 2;

  assert table[k] == 1;                       // the two keys are distinct

  assert (forall j: int :: table[Key(j, k->tag)] == old(table)[Key(j, k->tag)] || j == k->id);

}

 

procedure Q() returns (m: [int]Key)

{

  m := (lambda i: int :: Key(i, true));       // lambda producing datatype values

  assert m[3] == Key(3, true);

  assert m[3]->id == 3;

}

 

Boogie program verifier finished with 2 verified, 0 errors

A map-typed field becomes an SMT Array inside the datatype under the default array theory, and an uninterpreted sort under /useArrayAxioms; both work.

datatype Env { Env(vars: [int]int, n: int) }

 

procedure P(e: Env) returns (e': Env)

  ensures e'->vars[0] == 42;

{

  e' := e->(vars := e->vars[0 := 42]);

}

 

Boogie program verifier finished with 1 verified, 0 errors

The two encodings differ only in the sort used for the field:

(declare-datatypes ((T@Env 0)) (((Env (|vars#Env| (Array Int Int)) (|n#Env| Int) ) ) ))

(declare-sort |T@[Int]Int| 0)

(declare-datatypes ((T@Env 0)) (((Env (|vars#Env| |T@[Int]Int|) (|n#Env| Int) ) ) ))

10.9.1 Triggers🔗

Constructor applications, selector applications and tester applications are all legal trigger terms; each is a genuine SMT function application, so each becomes a :pattern.

datatype Box { Box(v: int) }

 

function f(Box): int;

 

axiom (forall b: Box :: { f(b) } f(b) == b->v);

axiom (forall i: int :: { Box(i) } f(Box(i)) == i);

 

procedure P(b: Box)

{

  assert f(b) == b->v;

  assert f(Box(3)) == 3;

}

 

Boogie program verifier finished with 1 verified, 0 errors

datatype Box { Box(v: int) }

function g(int): int;

axiom (forall b: Box :: { b->v } g(b->v) == 0);

procedure P(b: Box) { assert g(b->v) == 0; }

 

Boogie program verifier finished with 1 verified, 0 errors

which emits

(assert (forall ((b T@Box) ) (! (= (g (|v#Box| b)) 0)

 :qid |trig2bpl.3:15|

 :skolemid |0|

 :pattern ( (|v#Box| b))

)))

and testers work too:

datatype D { C1(f: int), C2() }

function h(D): int;

axiom (forall d: D :: { d is C1 } d is C1 ==> h(d) == 0);

procedure P(d: D) { assume d is C1; assert h(d) == 0; }

 

Boogie program verifier finished with 1 verified, 0 errors

Beware that a selector used as a trigger fires on every value of the datatype, since selectors are total; a tester used as a trigger fires on every term the solver has produced a tester for. Both are much more permissive than they look.

10.9.2 Recursive definitions🔗

There is no match construct and no built-in recursion. The idiom for a recursive function over a datatype is an uninterpreted function plus one axiom per constructor, triggered on the constructor application:

datatype List { Nil(), Cons(hd: int, tl: List) }

 

function len(l: List): int;

axiom len(Nil()) == 0;

axiom (forall h: int, t: List :: { len(Cons(h, t)) } len(Cons(h, t)) == 1 + len(t));

 

procedure Unfold()

{

  assert len(Cons(1, Cons(2, Nil()))) == 2;   // unfolds by triggering

}

 

procedure NeedsInduction(l: List)

{

  assert len(l) >= 0;                          // no induction principle: not provable

}

len.bpl(14,3): Error: this assertion could not be proved

Execution trace:

    len.bpl(14,3): anon0

 

Boogie program verifier finished with 1 verified, 1 error

A {:define} function can pattern-match with is and -> but cannot recurse — definitions are expanded, and cycles are rejected:

datatype Shape { Circle(r: int), Rect(w: int, h: int) }

 

function {:define} area(s: Shape): int

  { if s is Circle then 3 * s->r * s->r else s->w * s->h }

 

procedure P()

{

  assert area(Rect(2, 3)) == 6;

  assert area(Circle(2)) == 12;

}

 

Boogie program verifier finished with 1 verified, 0 errors

datatype List { Nil(), Cons(hd: int, tl: List) }

 

function {:define} sum(l: List): int

  { if l is Nil then 0 else l->hd + sum(l->tl) }

 

procedure P()

{

  assert sum(Cons(1, Cons(2, Nil()))) == 3;

}

define.bpl(3,19): Error: Call cycle detected among functions: sum

10.10 Pattern-style specifications🔗

Boogie has no pattern matching, so specifications are written with testers as guards and selectors as projections. The two rules of thumb are: guard every selector whose constructor is not already determined, and state the constructor of a result explicitly when it matters.

datatype Expr {

  Lit(n: int),

  Add(l: Expr, r: Expr),

  Neg(e: Expr)

}

 

function eval(x: Expr): int;

 

axiom (forall n: int :: { eval(Lit(n)) } eval(Lit(n)) == n);

axiom (forall l, r: Expr :: { eval(Add(l, r)) } eval(Add(l, r)) == eval(l) + eval(r));

axiom (forall e: Expr :: { eval(Neg(e)) } eval(Neg(e)) == -eval(e));

 

// A one-step simplifier, specified in "pattern style" with testers and selectors.

procedure Simplify(x: Expr) returns (y: Expr)

  ensures eval(y) == eval(x);

  ensures x is Add && x->l is Lit && x->l->n == 0 ==> y == x->r;

{

  if (x is Add && x->l is Lit && x->l->n == 0) {

    y := x->r;

  } else {

    y := x;

  }

}

 

Boogie program verifier finished with 1 verified, 0 errors

The unpack command is the specification-side counterpart of a single-case match: it makes the constructor a proof obligation rather than a silently underspecified read. Prefer

Rect(w, h) := s;

over

w, h := s->w, s->h;

when s is expected to be a Rect: the first form fails loudly if it is not.

10.11 Standard library datatypes🔗

The libraries loaded with /lib: declare several datatypes that most datatype code ends up using. From base.bpl:

datatype Option<T> { None(), Some(t: T) }

datatype Vec<T> { Vec(contents: [int]T, len: int) }

datatype Map<T,U> { Map(dom: [T]bool, val: [T]U) }

datatype One<T> { One(val: T) }

datatype Cell<T,U> { Cell(key: One T, val: U) }

datatype Unit { Unit() }

datatype Tag<V> { Tag(loc: Loc, val: V) }

and from node.bpl:

datatype Node<T> { Node(next: Option Loc, val: T) }

They behave like any other datatype; the operations over them are described in The standard library.

// Option, Vec, Map, One, Cell and Unit come from the base library.

procedure P(o: Option int) returns (n: int)

  ensures o is Some ==> n == o->t;

  ensures o is None ==> n == 0;

{

  if (o is None) { n := 0; } else { n := o->t; }

}

boogie /lib:base lib.bpl

 

Boogie program verifier finished with 1 verified, 0 errors

Civl uses datatypes heavily for linear permissions: One is the primitive permission carrier, and a user datatype is treated as carrying permissions when one of its constructor fields has a permission-carrying type. The shipped tests Test/datatypes/list-reversal-iterative.bpl, list-reversal-recursive.bpl and node-client.bpl are worked examples; see Civl: concurrency and refinement.

10.12 Divergences from This is Boogie 2🔗

This is Boogie 2 does not describe datatypes in any form. In particular:

The paper’s idiom for a closed enumeration (§2.0, example (0)) is an uninterpreted type, unique constants and an explicit closure axiom. That idiom is also where the difference bites hardest. With today’s default prover options the closure axiom has no trigger and smt.mbqi is off, so it never fires, while the datatype’s exhaustiveness is a native property of the sort:

// A closed enumeration, the pre-datatype way: an uninterpreted type,

// unique constants, and an explicit closure axiom.

type RGBColor;

const unique red: RGBColor;

const unique green: RGBColor;

const unique blue: RGBColor;

axiom (forall ce: RGBColor :: ce == red || ce == green || ce == blue);

 

// The datatype equivalent needs neither the constants nor the axiom.

datatype Color { Red(), Green(), Blue() }

 

procedure P(c: Color, x: RGBColor)

{

  assert x == red || x == green || x == blue;

  assert c is Red || c is Green || c is Blue;

  assert Red() != Green();

}

enum.bpl(14,3): Error: this assertion could not be proved

Execution trace:

    enum.bpl(14,3): anon0

 

Boogie program verifier finished with 0 verified, 1 error

Only the first assertion — the one about the paper’s encoding — fails, because the axiom is emitted without a :pattern:

(assert (forall ((ce T@RGBColor) ) (!  (or (or (= ce red) (= ce green)) (= ce blue))

 :qid |enumbpl.7:15|

 :skolemid |0|

)))

(Triggering is a general Boogie topic, not a datatype one; the point here is only that the datatype assertions need no help.) Note also that the paper’s finite keyword no longer exists in the grammar: type finite RGBColor; now parses as the declaration of a unary type constructor named finite.

Everything in this chapter should therefore be read as an addition to the paper rather than a refinement of it.