780 likes | 986 Views
Fundamentals of Perfect Developer. A one-day hands-on tutorial. Outline. Session 1: Getting Started Configuring Perfect Developer and creating a project Expressions, types, constants, functions, properties Classes, data, invariants, functions, schemas, constructors
E N D
Fundamentals of Perfect Developer A one-day hands-on tutorial
Outline • Session 1: Getting Started • Configuring Perfect Developer and creating a project • Expressions, types, constants, functions, properties • Classes, data, invariants, functions, schemas, constructors • Session 2: Going Further • Interfacing to a Java front-end • Sequences and recursion • Session 3: Refining methods and data • Statement lists, statement types, loops, nested refinement. • Internal data, retrieve functions • Session 4: Inheritance • Derived and deferred classes • Defining and redefining inherited methods
Configuration • Configure Perfect Developer to use the chosen editor • Load the Project Manager • Select Options…Editor • Browse to the editor executable file • Select the editor type • Configure the editor to recognise Perfect Developer files • See instructions in the Editor Customizations folder of the Perfect Developer installation
Creating a project • Click on the New toolbar icon • Browse to a writable folder, enter a project name, click Save and then OK • Create a new file called Examples • Add text: property (a, b, c: int) pre a < b, b < c assert a < c; • Save the file • Save the project and click the Verify tool
bool char int real set of X bag of X seq of X map of (X -> Y) Some predefined classes Set, bag, sequence and map types are finite collections. See Quick Reference or Language Reference Manual for more details (e.g. commonly-used members)
Expressions • All the usual arithmetic operators • a < b < c means what you would expect it to • a / b (integer) and a % b have precondition b > 0 • a / b(integer) rounds towards - • a .. b yields the sequence {a, a+1, a+2 … b} • #c yields the cardinality or length of the collection c • a # c yields the number of occurrences of a in c
Booleans, Conditionals etc. • Boolean connectives: & | ==> <== <==> • Conditional expression: ( [a > 0]: a, [b > 0]: b, []: 0) • Let-declaration: ( let t ^= a - b; t * t ) • Embedded assertion: ( assert a > 0, b > 0; a / b ) • The whole shebang: ( let t ^= a - b; assert t > 0; [t > 10]: 1, []: 10 / t )
Constructor calls • A constructor call yields a value of the specified type • Without parameters: seq of int{} MyClass{} • With parameters: seq of int{a, b, c} MyClass{42} • Some constructors have preconditions that must be met int{s} is ok when s = "123" but not when s = "lemon" ! [precondition is: ~s.empty & (forall c::s :- c.isDigit)]
Quantifiers etc. • If c is of type set of T, bag of T or seq of T,and p(x) is any Boolean expression involving x: Expression Return type forall x::c :- p(x) bool forall x: T :- p(x) exists x::c :- p(x) bool exists x: T :- p(x) that x::c :- p(x) T any x::c :- p(x) those x::c :- p(x) set / seq / bag of T for x::c yield v(x) set / seq / bag of type of v for those x::c :- p(x) yield v(x)
Declaring constants and functions • Declaring a constant: const four: int ^= 2 + 2; const smallPrimes: seq of int ^= those x::2..100 :- ~(exists y::2..<x :- x % y = 0); const two ^= 2; • Declaring a function: function half(x: int): int pre x > 0^= x / 2; Type can be omitted in simple cases Precondition (if needed)
Declaring properties • Use property declarations to express theorems:property assert half(four) = two; property (x: int) pre x > 0, x % 2 = 0 assert half(x) < x, (let h ^= half(x); h + h = x); Implicit universal quantification over the parameters Givens to be assumed Consequences to be proved
Exercise 1: express the following • All the integers from 0 to 100 inclusive, in ascending order. Verify that your solution contains 42 but does not contain 101. • The integer j (which is known to be greater than 0) divides the integer i exactly. • The squares of all the prime numbers from 2 to 100 inclusive. • The highest integer in a set S of integers that is known to be non-empty
Declaring enumerations class Currency ^= enum unspecified, euro, pound, USdollarend;const localCurrency ^= pound@Currency;… localCurrency.toString …
Declaring a Class class Money^= Variables declared here form the abstract data model of the class Invariants here constrain the data These methods and constructors are for use by confined and/or interface methods and constructors Access redeclarations allow abstract variables to be directly accessed from outside the class These methods and constructors can be called from outside the class Only the interface section is mandatory abstract variables, invariants, methods, constructors internal [never mind for now] confined [never mind for now] interface access redeclarations, methods, constructors, properties end;
Declaring data and invariants Variable amt is of type int Variable ccy is of type Currency abstract var amt: int, ccy: Currency; invariant amt = 0 | ccy ~= unspecified@Currency; Restriction on the values of amt and ccy
Functions and Operators No “()” if no parameters Name Return type function worthHaving: bool^= amt > 0;function plus(m: Money): Money pre m.ccy = ccy ^= Currency{amt + m.amt, ccy}assert result.ccy = ccy;operator (n: int) * : Money ^= Currency{amt * n, ccy}; Result expression Optional precondition Optional postassertion Operator declarations are the same as function declarations apart from the header Use nonmember prefix for methods & properties with no self object
Declaring Schemas Parameter is modified No self object No “()” if no parameters nonmember schema swap(a!, b!: Money) pre a.ccy = b.ccypostchange a,b satisfy a’=b, b’=aassert a.plus(b) = b’.plus(a’); schema !inflate(howMuch: int) pre 0 < howMuch < 200post amt! = (amt * howMuch)/100; Postcondition includes frame This one modifies instance variables Short for: change amt satisfy amt’= (amt * howMuch)/100
Declaring Constructors Note parameter list in “{}” Postcondition must initialise allinstance variables* build{a: int, c: Currency}pre a > 0post amt! = a, ccy! = c;build{!amt: int}post ccy! = euro@Currency;build{} ^= Money {0, unspecified@Currency}; Initialise instance variable directly from the parameter Short for:change amt satisfy amt’= a We do use “{}” if no parameters This constructor is defined in terms of another one *except for variables whose when-guards are false
Using access redeclarations • abstract variables may be redeclared in the interface: function v1;makes v1 readable selector v2;makes v2 readable and writable • Making a variable writable is generally a bad idea • Except for “convenience” classes, e.g. class pair • Making a variable of a complicated type readable is generally a bad idea • Because we can’t then change its representation easily • Constants may be redeclared as nonmember functions • Use access redeclarations sparingly!
Exercise 2: Specification(followed by coffee break) • You have been provided with a Perfect specification of class Money • Try to verify it (there will be 3 verification errors) • Fix the specification to remove the verification errors • Verify that multiplying a Money object by 2 is equivalent to adding it to itself • Declare a “+” operator that works like the “plus” function except that if the amount of either operand is zero, we don’t care what the corresponding currency is
Using Perfect with a graphical UI Java front-end Perfect back-end class MyAppimplements ActionListener class Application ^=interface constructor calls build{} MyApp() when pressed calls function f (…) Button 1 when pressed calls Button 2 schema ! s (…) Best to avoid preconditionsin methods called from Java!
Using Perfect with a graphical UI • Declare a Perfect class Application • Declare interface functions/schemas to call from Java • Declare an interface constructor to call from Java • In the graphical interface code: • Import Application.java • Instantiate a single Application object during initialisation • Call member functions/schemas when buttons are pressed • Convert parameter types as necessary • We have provided you with a sample • In file TutorialExample.java
Exercise 3: Build the sample program • Open project TutorialExample.pdp • Verify the project • Click the Build tool icon • Check for error messages • Locate and run output\App.jar • Try making your own changes to Application.pd • e.g. print some of the expressions you wrote earlier [tips follow…]
Tips • To use constants and functions from Examples.pd: • Add file Examples.pd to the project • Add to Application.pd: import "Examples.pd"; • You can convert any expression to a string • By calling .toString on it • To make your application robust: • Verify your version of Application.pd • Don’t add any preconditions to the methods called from Java!
Sequences and Strings • The standard collection types are: set of X, bag of X, seq of X (where X is any type you like) string seq of char • Members of class seq of X include: head tail front back append(x) prepend(x) ++(s) # (x)in take(n) drop(n) slice(offset, length) isndec isninc permndec permninc isOrdered(cp) !sort(cp) findFirst(x) findLast(x) • Useful global functions include: flatten(ss: seq of seq of X) interleave(ss, s) • See the Library Reference for details
Recursive and templated functions Indicates that T can be any type function reverse(s: seq of class T): seq of Tdecrease #s ^= ( [#s <= 1]: s, []: reverse(s.front).prepend(s.last) ); Recursion variant Recursive call
Recursion variants • General form is: decrease e1, e2, e3 … • e1, e2 … are of int, bool, char or an enumeration type • The variant must decrease on each recursive call • Either e1 must decrease • Or e1 stays the same and e2 decreases • Or e1 and e2 stay the same and e3 decreases… • Integer components must not go negative
Exercise 4: Sequences Specify the following functions: • numLeadingSpaces(s: string): nat • returns the index of the first character in s that is not a space, or the length of s if it is all spaces • removeLeadingSpaces(s: string): string • returns s with any leading spaces removed • firstWord(s: string): string • returns the leading characters of s up to but not including the first space character in s • splitIntoWords(s: string): seq of string • splits the sentence s into individual words(hint: use recursion!)
Refinement • There are three types of refinement in Perfect: • Refining result expressions to statement lists • Refining postconditions to statement lists • Refining abstract data to implementation data • When you refine the abstract data of a class, you normally need to refine the method specifications as well • So we will start with refining specifications
Refining specifications • Specification refinement serves these purposes: • To implement a specification where Perfect Developer fails to • To implement a specification more efficiently • To take account of data refinement in affected methods • You can refine these sorts of specification: • Expressions that follow the “^=” symbol • Postconditions • To refine a specification, append to it: viastatement-listend
Some simple refinements function square(x: int): int^= x^2viavalue x*xend; schema swap(a!, b!: class X)post a!= b, b!= avialet temp ^= a; a! = b; b! = tempend; value statement returns a value from the via..end Semicolon separates and sequences the statements A postcondition can be used as a statement
Nested Refinements • You can refine not just method specifications but also: • Postcondition statements • Let-declarations in statement lists function fourth(x: int): int ^= x^4vialet x2 ^= x^2viavalue x*xend;value x2*x2 end; value yielded by the inner via..end value yielded by the outer via..end
Types of Statement Exactly the same as in bracketed expressions • Let-declaration • Assertion • Variable declaration • Postcondition • pass statement • If-statement • value and done statements • Loop statement • Block statement Same as in postconditions Omit the “post” keyword! Does nothing
If-statement Guard if [c in `a`..`z`]: isAletter! = true; valid! = true; [c in `0`..`9`]: isAletter! = false; valid! = true; []: valid! = false fi Statement list Optional “else” part [] fi means the same as[]: pass fi
“value” statement function max(a,b,c: class X): Xsatisfyresult >= a & result >= b & result >= c & (result=a | result=b | result=c)viaif [a > b]:value max(a, c); []:value max(b, c) fi end; Every path in an expression refinement must end at a value statement
“done” statement schema max(a!,b,c: class X)postchange a satisfy a’ >= a & a’ >= b & a’ >= c & (a’= a | a’= b | a’= c)viaif [a > b]: a!= max(a, c);done; [] fi; a!= max(b, c) end; A postcondition refinement may contain one or more done statements Implicit done statement here
Loop statement Start of loop statement // Calculate a^b var rslt: int! = 1; loop var j: nat! = 0; change rsltkeep rslt’ = a^j’until j’= bdecrease b - j’; rslt! * b, j! + 1 end; Loop variable declaration List of what the loop can change Loop invariant list Termination condition Loop variant Loop body End of loop statement
Loop statement loop local variable declarations (optional) change list (optional) invariant termination condition (optional) variant body statements end • If no change list is given, only the local variables can be changed • If no termination condition is given, the loop terminates when the variant can decrease no more
Designing loop invariants • Variables in loop invariants may be primed or unprimed • Primed = current values at the start of an iteration • Unprimed = value before the loop started • The invariant is the only source of information about current values of changing variables • The state when the loop completes is given by: invariant & until-part • The invariant should comprise: • A generalisation of the state that the loop is meant to achieve; • Additional constraints needed to make the invariant, until-part, variant and body well-formed
Example of invariant design • Given s: seq of int we wish to achieve total’= + over s • Generalise this to tot’= + over s.take(j’) for some loop counter j • When j = #s then the generalisation becomes the required state because s.take(#s) = s • This generalisation forms part of the invariant • But s.take(j’) has precondition 0 <= j ’<= #s • So we must either add this precondition as an earlier invariant… • Or as a type constraint in the declaration of j
Loop example (incorrect) var totl: int! = 0; loop var j: int! = 0; change totlkeep totl’= + overs.take(j’)until j’= #sdecrease #s - j’; totl! + s[j], j! + 1 end; Problem! These expressions are not universally well-formed
Loop example (version 1) var totl: int! = 0; loop var j: int! = 0; change totlkeep0 <= j’<= #s, totl’= + overs.take(j’)until j’ = #sdecrease #s - j’; totl! + s[j], j! + 1 end; Added this extra invariant at the start This is now well-formed This is also well-formed (provided the until-condition is false)
Loop example (version 2) Added this type constraint var totl: int! = 0; loop var j:(int in 0..#s)! = 0; change totlkeep totl’= overs.take(j’)until j’= #sdecrease #s - j’; totl! + s[j], j! + 1 end; This is now well-formed This is also well-formed (provided the until-condition is false)
Refining recursion to loops function rev(s: seq of int): seq of int decrease #s^= ([s.empty]: s, []: rev(s.tail).append(s.head)) via var rslt: seq of int! = seq of int{}; loop var j:(nat in 0..#s)! = 0; change rsltkeep rslt’= rev(s.take(j’)) until j’= #sdecrease #s - j’; rslt! = rslt.prepend(s[j]), j! + 1 end; value rsltend;
Refining recursion to loops • Is the preceding example correct? • Probably! • But Perfect Developer cannot verify it! • The definition builds the result from front to back • Using append • The implementation builds the result from back to front • Using prepend • They are equivalent only because of associativity (a ++ b) ++ c = a ++ (b ++ c) reverse(x.tail).append(x.head) = reverse(x.front).prepend(x.last) • To prove this we need an inductive prover!
Refining recursion to loops function rev(s: seq of int): seq of int decrease #s^= ([s.empty]: s, []: rev(s.tail).append(s.head)) via var rslt: seq of int! = seq of int{}; loop var j:(nat in 0..#s)! = #s; change rsltkeep rslt’= rev(s.drop(j’)) until j’= 0decrease j’; j! - 1, rslt! = rslt.append(s[j’])end; value rsltend;
Loops: a summary • Getting the invariant correct is critical • It must describe the relationships between all variables changed by the loop (including the local loop variables) • Its main part is a generalisation of the desired state after the loop • When the until condition is satisfied, the generalisation must reduce to the desired state • You may also need to include constraints on variables • To make expressions in the loop well-formed • If refining a recursive definition, make sure that the loop builds the result in the same order as the definition
Exercise 5: Method refinement • Refine the following specifications function min2(x, y: int): intsatisfyresult <= x,result <= y,result = x | result = y; function findFirst(s: seq of int, x: int): intsatisfy 0 <= result <= #s,result = #s | s[result] = x,forall j::0..<result :- s[j] ~= x; • Function numLeadingSpaces from exercise 4 • Function splitIntoWords from exercise 4
Data refinement • When designing a class, we should always use the simplest possible abstract data model • Avoid redundancy! • Don’t be concerned with efficiency at this stage! • The methods are specified in terms of this model • This keeps the specifications simple! • The data should not be directly accessible from outside • So we can change the implementation of the data without changing the class interface • [Except for very simple classes like pair]