|
|
17 June 2023, Tutorial, PLDI 2023
More than the tutorial abstract promised!
:=
, if
, while
, class
, new
, method
function
, datatype
, match
.dfy
method Main() {
print "hello, Dafny\n";
}
dafny run Hello.dfy
method SimpleMethod(x: int, b: bool) {
var y := x + 3;
print y, " ", b, "\n";
}
method MethodWithResults(x: int) returns (r: int, s: int) {
r := x;
s := x + 3;
}
function MyFunction(x: int, b: bool): int {
x + 3
}
predicate IsInstructor(name: string) {
name == "Rustan"
}
bool
, int
, …)
type T = U
(type synonym)
type T
(abstract type)
datatype
codatatype
newtype
class
trait
type T = u: U | P(u)
(subset type)BuiltinType ::=
// primitive types
| bool | int | real | char
| bv0 | bv1 | bv2 | ...
| object
| ORDINAL
// collection types
| set<Type> | iset<Type> | multiset<Type>
| seq<Type>
| map<Type, Type> | imap<Type, Type>
// arrow types
| () -> Type | (Type) -> Type | (Type, Type) -> Type | ...
| () --> Type | (Type) --> Type | (Type, Type) --> Type | ...
| () ~> Type | (Type) ~> Type | (Type, Type) ~> Type | ...
// tuple types
| () | (Type, Type) | (Type, Type, Type) | ...
// array types
| array<Type> | array2<Type> | array3<Type> | ...
// others
| nat | string
false
, true
!
&&
, ||
==>
, <==
<==>
A && B ==> C && D
means the same thing as (A && B) ==> (C && D)
predicate HasMimimum(s: set<int>) {
exists m ::
m in s &&
forall x :: x in s ==> m <= x
}
&&
and ||
predicate IsIndexOfMinimum(i: int, s: seq<int>) {
&& 0 <= i < |s|
&& forall j :: 0 <= j < |s| ==> s[i] <= s[j]
}
predicate BadBullets(A: bool, B: bool, C: bool, D: bool) {
&& A
&& B ==> C // warning: did you forget parentheses?
&& D
}
_
800_555_1212
0x8000_0000
0 <= i < j < 100
type nat = x: int | 0 <= x
newtype
2.0
, 37.2
'D'
, '\n'
, '\U{10_aBcD}'
type string = seq<char>
&
, |
, ^
<<
, >>
.RotateLeft
, .RotateRight
Wider operators have lower binding power
newtype MyInt = int
newtype int32 = x: int | -0x8000_0000 <= x < 0x8000_0000
newtype byte = x | 0 <= x < 256
newtype Primes = x | IsPrime(x)
predicate IsPrime(x: int) {
2 <= x && forall y :: 2 <= y < x ==> x % y != 0
}
newtype
is to define a type that is separate from the base type, but with
the values and operations (possibly constrained) of the base type
newtype
must satisfy the constraint
newtype
gives the compiler the freedom to represent the type and its base type differently
method Examples() {
var a: int;
var b: int32;
var c: bv32;
a := 25;
b := 25;
c := 25;
var d := 25;
var e;
e := 25 as int32;
}
method Combine(a: set<real>, b: set<real>)
returns (c: set<real>)
{
c := a + b;
assert |c| <= |a| + |b|;
c := c + {2.0, 37.2};
}
method Shuffle(s: seq<char>) returns (r: string) {
var n := |s| / 2;
r := s[n..] + ['a', 'b', 'c'] + s[..n] + "xyz";
var c := r[5];
r := r[2..6];
}
method Squares(n: nat) {
// set of first n natural numbers:
var s := set i | 0 <= i < n;
// set of first n squares:
var t := set i | 0 <= i < n :: i * i;
// map from first n natural numbes to their squares:
var m := map i | 0 <= i < n :: i * i;
// map to first n natural numbes from their squares:
var w := map i | 0 <= i < n :: i * i := i;
// sequence of first n squares:
var q := seq(n, i => i * i);
}
datatype List<X> = Nil | Cons(head: X, tail: List<X>)
datatype Rainbow = L | G | B | T | Q | I | A
datatype Expr =
| Const(value: int)
| Variable(string)
| Plus(0: Expr, 1: Expr)
datatype Record = Record(name: string, age: nat)
|
is optional
method Print(expr: Expr) {
match expr
case Const(value) =>
print value;
case Variable(name) =>
print name;
case Plus(left, right) =>
Print(left);
print " + ";
Print(right);
}
It is an error if a match
is reached and no case
applies
method Print(expr: Expr) {
if expr.Const? {
print expr.value;
} else if expr.Variable? {
var Variable(name) := expr;
print name;
} else {
Print(expr.0);
print " + ";
Print(expr.1);
}
}
method Double(expr: Expr) {
if expr.Const? {
var u := expr.(value := 2 * expr.value);
}
}
datatype Macklemore = Macklemore(rhyme: string)
Simple wrapper constructors are compiled away
Tuple types are built-in, anonymous datatypes
// If you wrote them yourself, you'd write:
datatype Tuple0 = Tuple0()
datatype Tuple2<X, Y> = Tuple2(0: X, 1: Y)
datatype Tuple3<X, Y, Z> = Tuple3(0: X, 1: Y, 2: Z)
// ...
// The built-in ones have this special syntax:
datatype () = ()
datatype (X, Y) = (0: X, 1: Y)
datatype (X, Y, Z) = (0: X, 1: Y, 2: Z)
// ...
Trivia question:
Dafny has two built-in unit types (that is, types with exactly one value). What are they?
bv0
and ()
A codatatype represents possibly infinite structures
codatatype Stream<X> = Next(head: X, tail: Stream<X>)
codatatype DeepTree<X> =
| Leaf
| Node(left: DeepTree<X>, value: X, right: DeepTree<X>)
(aka “refinement types” or “predicate subtypes”)
type Even = x: int | x % 2 == 0
Intermediate subexpressions are typically typed like the base type, so the constraint need not hold.
method Examples(x: Even, i: int) {
var y: Even := (1 + x) + 1;
if i % 2 == 0 {
var z: Even := i;
}
}
datatype IntList =
| End(value: int)
| Point(value: int, next: IntList)
predicate IsIncreasing(ii: IntList) {
ii.Point? ==>
&& ii.value <= ii.next.value
&& IsIncreasing(ii.next)
}
type IncreasingList = ii: IntList | IsIncreasing(ii)
datatype IncreasingList_ =
| End(value: int)
| Point(value: int, next: IncreasingList_)
predicate Valid(ii: IncreasingList_) {
ii.Point? ==>
&& ii.value <= ii.next.value
&& Valid(ii.next)
}
type IncreasingList = ii: IncreasingList_ | Valid(ii)
datatype IncreasingList_ =
| End(value: int)
| Point(value: int, next: IncreasingList_)
{
predicate Valid() {
Point? ==>
&& value <= next.value
&& next.Valid()
}
}
type IncreasingList = ii: IncreasingList_ | ii.Valid()
class Cell {
var data: int
}
class Person {
const name: string
var age: nat
constructor (name: string, age: nat) {
this.name := name;
this.age := age;
}
constructor Newborn(name: string) {
this.name := name;
age := 0;
}
}
method Client() {
var c := new Cell;
c.data := 17;
var p0 := new Person("Manilow, Barry", 80);
var p1 := new Person.Newborn("Mumford, Sunny");
}
class Cell<X> {
var data: X
var usageLog: seq<string>
constructor (x: X) {
usageLog := [];
AppendToLog("creation"); // not allowed
data := x;
}
method AppendToLog(event: string) {
usageLog := usageLog + [event];
}
}
class Cell<X> {
var data: X
var usageLog: seq<string>
constructor (x: X) {
usageLog := [];
data := x;
new;
AppendToLog("creation");
}
method AppendToLog(event: string) {
usageLog := usageLog + [event];
}
}
class CyclicList<X> {
var data: X
var next: CyclicList<X>
constructor (x: X) {
data := x;
next := this;
new;
}
method Merge(that: CyclicList<X>) {
this.next := that;
that.next := this;
}
}
Every reference type, for example
class Cell { /*...*/ }
gives rise to two types:
Cell?
is a nullable type, consisting of the allocated Cell
objects and the value null
Cell
is the corresponding non-null type, defined automatically as a subset type:type Cell = c: Cell? | c != null
method Arrays() {
var arr: array<int>;
var matrix: array2<real>;
arr := new int[25];
matrix := new real[100, 20];
arr[3] := 123;
matrix[4, 5] := 3.75;
}
method Squares(n: nat) returns (s: seq<int>) {
var arr := new int[n](i => i * i);
s := arr[0..arr.Length];
s := arr[..arr.Length];
s := arr[0..];
s := arr[..];
arr := new int[3] [2, 3, 5];
arr := new int[] [2, 3, 5];
arr := new [] [2, 3, 5];
}
trait Animal extends object {
const name: string
var age: nat
method Print()
}
class Tiger extends Animal {
constructor (firstName: string) {
name := firstName + " the Tiger";
age := 0;
}
method Print() {
print name, " is ", age, " years old\n";
}
}
Dafny distinguishes between expressions and statements, and, in an analogous way, between functions and methods.
function | method |
---|---|
body is an expression | body is a statement |
deterministic | nondeterministic |
recursion, no loops | recursion and loops |
cannot modify state | can modify state |
let bindings (immutable) | local variables (mutable) |
declares heap dependencies (reads ) | declares heap modifications (modifies ) |
terminates | can be declared as possinbly nonterminating |
In most cases, syntax of corresponding expression/statement constructs are the same.
(Exception: ternary if
-then
-else
expression versus if
statement.)
function Triple(x: int): int {
var double := x + x;
x + double
}
method MTriple(x: int) returns (r: int) {
var y := 4 * x;
y := y + 2 * x;
r := y / 2;
}
function Length<X>(list: List<X>): nat {
match list
case Nil => 0
case Cons(_, tail) => 1 + Length(tail)
}
method MLength<X>(list: List<X>) returns (len: nat) {
match list
case Nil =>
len := 0;
case Cons(_, tail) =>
len := MLength(tail);
len := len + 1;
}
function Length<X>(list: List<X>): nat {
if list == Nil then
0
else
1 + Length(list.tail)
}
method MLength<X>(list: List<X>) returns (len: nat) {
if list == Nil {
len := 0;
} else {
len := MLength(list.tail);
len := len + 1;
}
}
function Length<X>(list: List<X>): nat
// auto-accumulator tail-recursive function
{
if list == Nil then
0
else
1 + Length(list.tail)
}
method MLength<X>(list: List<X>) returns (len: nat) {
len := 0;
var xs := list;
while xs != Nil {
len := len + 1;
xs := xs.tail;
}
}
function NestedPattern(list: List<int>): int {
match list
case Nil => 0
case Cons(5, _) => 1
case Cons(x, Nil) => 2 + x
case Cons(_, Cons(7, _)) => 3
case _ => 4
}
function DisjunctivePattern(rainbow: Rainbow): int {
match rainbow
case L => 0
case G | B => 1
case _ => 2
}
method M(x: int, y: int) returns (r: int, s: int) {
r := x + y;
if x < y {
r := r + 5;
} else if x == 100 {
s := 10;
return;
} else {
return r + 5, 10;
}
s := 10;
}
function F(x: int): (bool, real)
method M(x: int) returns (b: bool, r: real)
method Caller() {
var pair: (bool, real);
var b: bool;
var r: real;
pair := F(10);
b, r := M(10);
var (bb, rr) := F(10);
}
_
, '
, and ?
_
var
) and assign it at the same time
:=
, :|
, :-
method Examples() {
var x;
x := 10;
var y := 10;
}
method MLength<X>(list: List<X>) returns (len: nat) {
len := 0;
var list := list;
while list != Nil {
len := len + 1;
list := list.tail;
}
}
method Simultaneous() returns (r: int) {
var i, v: bv32, b := 0, 0, true;
var j;
i, j := 100, i + 2;
return i + j;
}
function SimultaneousLets(): int {
var i, v: bv32, b := 0, 0, true;
var i, j := 100, i + 2;
i + j
}
In lists of variable declarations, :
binds stronger than ,
method Swap<X>(a: array<X>, i: int, j: int) {
a[i], a[j] := a[j], a[i];
}
method Example(a: array<int>, i: int, x: int) {
if i != 3 || x == 5 {
a[i], a[3] := 5, x;
}
}
It is an error for a simultaneous assignment to give different values to the same l-value
method Zero(x: nat) returns (z: nat) {
z := x;
while z != 0 {
z := z / 2;
}
}
method InitUp(n: nat) returns (arr: array<int>) {
arr := new int[n];
for i := 0 to n {
arr[i] := i;
}
}
method InitDown(n: nat) returns (arr: array<int>) {
arr := new int[n];
for i := n downto 0 {
arr[i] := i;
}
}
for i := lo to hi
requires lo <= hi
for i := hi downto lo
requires lo <= hi
for ... to
post-increments the loop index
for ... downto
pre-decrements the loop indexmethod Example() returns (arr: array<int>) {
arr := new int[5];
var m, n := 0, 5;
for i := m to n {
arr[i] := i;
m, n := m - 123, n + 123;
}
}
for
-loop bounds are determined on entry to the loopmethod SadMax(x: int, y: int) returns (max: int) {
if x < y {
return x;
} else {
return y;
}
}
method Max(x: int, y: int) returns (max: int) {
if
case x <= y =>
return x;
case y <= x =>
return y;
}
method MoreThanMax(x: int, y: int) returns (m: int) {
if {
case x <= y =>
var local := x + x;
m := local - x;
case y <= x =>
m := y;
}
return m + 4;
}
match
statements and match
expressionsmethod Gcd(x: nat, y: nat) returns (a: nat) {
a := x;
var b := y;
while
case a < b =>
b := b - a;
case b < a =>
a := a - b;
}
method ArrayFill<X>(arr: array<X>, x: X) {
forall i | 0 <= i < arr.Length {
arr[i] := x;
}
}
method Rotate<X>(arr: array<X>) {
forall i | 0 <= i < arr.Length {
arr[(i + 1) % arr.Length] := arr[i];
}
}
forall
statement is an aggregate statement, not a loop
method Partition(arr: array<int>) {
var pivot :| 1 <= pivot < arr.Length - 1;
// ...
}
method Find(arr: array<int>, x: int) returns (i: int) {
i :| 0 <= i < arr.Length && arr[i] == x;
}
There must exist a value for the bound variables
method MaybeFind(arr: array<int>, x: int) returns (r: nat) {
if exists i :: 0 <= i < arr.Length && arr[i] == x {
var i :| 0 <= i < arr.Length && arr[i] == x;
return i;
}
return arr.Length;
}
method BindingIf(arr: array<int>, x: int) returns (r: nat) {
if i :| 0 <= i < arr.Length && arr[i] == x {
return i;
}
return arr.Length;
}
module Library {
method Sort(arr: array<int>)
}
module Client {
import Library
method Example() {
var arr := new [] [10, 3, 8];
Library.Sort(arr);
}
}
module Library {
method Sort(arr: array<int>)
}
module Client {
import L = Library
method Example() {
var arr := new [] [10, 3, 8];
L.Sort(arr);
}
}
module Library {
newtype int32 = x | -0x8000_0000 <= x < 0x8000_0000
}
module Client {
import opened Library
function Difference(x: int32, y: int32): int32 {
y - x
}
}
module Library {
export
reveals Tree, Singleton
datatype Tree = Leaf(int) | Node(Tree, Tree)
function Singleton(x: int): Tree // ...
function Combine(a: Tree, b: Tree): Tree // ...
}
module Client {
import Library
type DoubleTree = (Library.Tree, Library.Tree)
}
module Library {
export Public
reveals Tree, Singleton
export Friends
reveals Tree, Singleton, Combine
// ...
}
module Client {
import Library`Public
type DoubleTree = (Library.Tree, Library.Tree)
}
module Library {
export Public
reveals Tree, Singleton
export Friends extends Public
reveals Combine
// ...
}
module Client {
import Library`Public
type DoubleTree = (Library.Tree, Library.Tree)
}
module Library {
export
reveals Tree, Singleton
export Friends extends Library
reveals Combine
// ...
}
module Client {
import Library
type DoubleTree = (Library.Tree, Library.Tree)
}
module Library {
export
reveals Tree, Singleton
provides Combine
datatype Tree = Leaf(int) | Node(Tree, Tree)
function Singleton(x: int): Tree {
Leaf(x)
}
function Combine(a: Tree, b: Tree): Tree {
Node(a, b)
}
}
A program is not just for the compiler
method M(a: int, ghost b: int)
returns (x: int, ghost y: int)
{
ghost var u := a + b;
u := u + 3;
return 2 * a, 2 * u;
}
method M(a: int, ghost b: int)
returns (x: int, ghost y: int)
{
ghost var u := a + b;
u := u + 3;
return 2 * a, 2 * u;
}
function F(a: int, ghost b: int): (int, ghost int)
{
ghost var u := a + b;
ghost var u := u + 3;
(2 * a, ghost 2 * u)
}
method M(a: int, ghost b: int)
returns (x: int, ghost y: int)
{
var u := a + b;
u := u + 3;
return 2 * a, 2 * u;
}
function F(a: int, ghost b: int): (int, ghost int)
{
var u := a + b;
var u := u + 3;
(2 * a, ghost 2 * u)
}
class Cell<X> {
var data: X
ghost var usageLog: seq<string>
constructor (x: X) {
usageLog := [];
data := x;
new;
AppendToLog("creation");
}
ghost method AppendToLog(event: string) {
usageLog := usageLog + [event];
}
}
datatype IncreasingList_ =
| End(value: int)
| Point(value: int, next: IncreasingList_)
{
ghost predicate Valid() {
Point? ==>
&& value <= next.value
&& next.Valid()
}
}
type IncreasingList = ii: IncreasingList_ | ii.Valid()
newtype int32 = x: int | -0x8000_0000 <= x < 0x8000_0000
method Abs(x: int32) returns (r: int32)
requires x != -0x8000_0000
ensures 0 <= r
{
if x < 0 {
return -x;
} else {
return x;
}
}
ghost predicate ContainsSymbols(expr: Expr) {
|| expr.Variable?
|| (expr.Plus? &&
(ContainsSymbols(expr.0) || ContainsSymbols(expr.1)))
}
function Evaluate(expr: Expr): int
requires !ContainsSymbols(expr)
{
match expr
case Const(value) => value
case Plus(left, right) => Evaluate(left) + Evaluate(right)
}
So, Dafny has partial functions
function Sum(list: List<int>): int
decreases list
{
match list
case Nil => 0
case Cons(x, tail) => x + Sum(tail)
}
method IterativeSum(list: List<int>) returns (s: int) {
s := 0;
var list := list;
while list != Nil
decreases list
{
s, list := s + list.head, list.tail;
}
}
method Combine(s: seq<int>) returns (r: int)
ensures r % 2 == 0
{
r := 0;
for i := 0 to |s|
invariant r % 2 == 0
{
var x := s[i];
r := r + if x % 2 == 0 then x else x + x;
}
}
method Increase(cell: Cell<int>)
modifies cell
{
cell.data := cell.data + 15;
}
method Increase(cell: Cell<int>)
modifies cell
ensures old(cell.data) < cell.data
{
cell.data := cell.data + 15;
}