Introducing Perfect

advertisement
Commercial advantages
Perfect Developer is an object oriented software development tool for producing software that is
mathematically proven to be correct.
Although the developers of safety-critical software have for some years been building software systems
that can be mathematically shown to be correct, the techniques used have been difficult to learn and too
time-consuming to be applied to less critical software. With Perfect Developer, this has changed.
Perfect Developer can be used to produce a functioning prototype quickly, so that, at an early stage in
the development process, a model of the system can be demonstrated to users to check that their
requirements have been correctly captured. This means that misunderstandings with users can be
rectified promptly, before large development costs have been incurred.
High productivity: The advanced automated reasoning used in the prover means that continual user
intervention is not needed. Further, the automatic refinement capabilities greatly decrease the amount
of manual refinement that has to be carried out.
Correct and continued use of Perfect Developer will substantially reduce the amount of testing and
debugging activity needed, leading to lower costs and shorter time-to-market.
Perfect Developer is easier to use than other formal methods tools, because it does not require all its
users to have advanced mathematical skills. The Perfect Developer tool has its own specification
language, called Perfect, which has the look and feel of popular O-O programming languages.
Perfect Developer strongly supports object-oriented development. However, object-oriented
development is not mandated.
Models can be imported from leading UML tools, if desired.
Automatic correct code generation Final code can be generated in Java or C++, allowing integration
with other components written in those languages.
Overview of development
To use Perfect Developer, you construct a model. If it helps you, you can start from a UML structural
method.
You specify the model and the requirements, using Perfect Developer's own language, Perfect, to write
your specification.
To prove that the software is correct, Perfect Developer attempts to verify the model against the
requirements. Any refinement you did is verified against the model.
Verification is performed by generating proof obligations (also called verification conditions). With
Perfect Developer, proving these obligations is an automated process. You set the process going and
leave it to itself.
Once the specification has been proved to be correct, either you refine the specification to code or you
1
let Perfect Developer generate the code automatically. You never write code except as a refinement
to the specification.
Now you can compile and run your prototype, for the user's inspection and comments. You know with
certainty that the prototype is correct according to the requirements stated, but users are notoriously
poor at expressing their requirements at the first time of trying. The requirements originally given may
need to be modified and the process repeated until the user accepts the prototype.
At this stage, you can consider manual refinements in the interests of system efficiency. We suggest
profiling to determine where effort is best applied. Of course, the manual refinements to the
specification must be verified in their turn.
The system thus developed is to a considerable extent self-documenting.
2
Development in more detail
Construction of an O-O software system begins with creating a model of the system. Recent years have
seen the rise of the Unified Modeling Language (UML), a graphical notation for describing the
architecture of a software system and how it interacts with users or other systems.
To begin specification and implementation of a system in Perfect, you need to produce a Perfect
skeleton identifying the following:
3
1. the main classes of the system;
2. the names and signatures (parameter lists) of interface methods and constructors of those
classes;
3. the requirements that the system must meet;
4. any type declarations needed to support the above.
There are many ways of identifying the candidate classes in a problem. For example, you can consider
each noun in the problem description as a potential object or class. If you are not familiar with this
process, we recommend you consult a good book on the subject.
If you perform initial modelling using UML, you can generate the Perfect main classes and their
method signatures by importing the UML class diagrams; however, UML does not describe formal
requirements, so you will need to add these.
So, to use Perfect Developer, you construct a model, using UML if that helps you.
You specify the model and the requirements, using Perfect Developer's own language, Perfect, to write
your specification.
Then either you refine the specification to code or you let Perfect Developer do it for you. You never
write code except as a refinement to the specification.
To prove that the software is correct, Perfect Developer attempts to verify the model against the
requirements. Any refinement you did is verified against the model.
Verification is performed by generating verification conditions (also called proof obligations). With
Perfect Developer, proving these is an automated process. You set the process going and leave it to
itself.
The prover proves (or informs you that it cannot prove) that the specification is correct according to the
requirements that it has been given, and that the code is correct according to the specification. For a
correct program, typical percentages of proof obligations successfully discharged are in the very high
nineties.
Verifying specifications
Verification is performed by generating proof obligations, or verification conditions, as they are also
called. With Perfect Developer, proving these obligations is an automated process. You set the
process going and leave it to itself.
In the basic tutorials, we explain how to structure a class and how to write method specifications. We
introduce you to the principle of using preconditions to specify what needs to hold when a method is
called, and we show you how to use the verification facility of Perfect Developer to make sure that the
specification doesn't involve something untoward, such as dividing by zero or indexing off the end of a
sequence.
This means that if you have written a program specification entirely in Perfect and verified it without
errors, you can be fairly sure that if you go on to generate code and run the program, it won't crash. We
can't absolutely promise that it won't crash, as tools such as compilers, linkers and others involved in
building the executable program are outside our control.
So how do we ensure that we got the specification right? Traditional techniques for checking
4
specifications include peer review and testing. Of course, testing doesn't check the specification as
such; it checks whether code that purports to implement the specification behaves - for the test data
provided - in accordance with that specification.
With Perfect Developer you can verify a specification against its expected behaviour. If you are able
to express all the known user requirements as expected behaviour, and successfully verify the
specification against it, you will have gone a long way towards showing that the specification meets the
requirements.
Verifying specifications against expected behaviour is better than testing in the following respects:



instead of testing with a necessarily finite set of data, you can verify against an infinite (or very
large) range of possible data;
you can verify a specification that hasn't been implemented yet, so it can be done much earlier
in the development process;
you don't have to construct a test harness - you just express the expected behaviour along with
the specifications;
but does have one disadvantage:

verifying a specification does not detect errors in the other steps (design, implement, compile,
link etc.). The design and implementation steps can be verified by Perfect Developer, but you
will still need testing to verify that the compile and link steps have not introduced errors (at
least, until compilers and linkers are developed using Perfect Developer!).
Software development practice has shown that the earlier an error is introduced and the later it is
detected, the more expensive it is to rectify. This means that a specification error that is not detected
until testing (which is only possible when all the other steps have been completed) is a very expensive
error indeed. It's much better to verify the specification early on.
To verify a specification against expected behaviour using Perfect Developer:
1. Declare the expected behaviour using post-assertions and property declarations;
2. Run 'Verify'.
The post-assertions and property declarations are placed in the Perfect source alongside the
specification, so you don't need to take any special measures to ensure that declaration of expected
behaviour remains part of the project.
When should you use post-assertions, and when should you use property declarations? As a general
guide:


Behaviour that is associated primarily with calls to one particular method should be declared as
a post-assertion attached to that method;
Other sorts of expected behaviour should be declared as a property of the class concerned; or, if
it doesn't fit in any one class, as a global property.
Dealing with Verification Problems
*** You need to enable Javascript for this page to work fully. ***
So you've written your specification and asked Perfect Developer to verify it. After several minutes, the
system has successfully discharged most of the proof obligations; but it pronounces that it finds a small
number of obligations either unprovable or too hard. What should you do?
5
Firstly
don't panic.
Press the green button to start calming down, and read on:
It is quite normal to find that a small percentage of obligations is not proven, so that you need to work
at achieving 100% proof.
How to start?
Remember that a proof obligation may go unproven for either of two reasons:


either it may be untrue under some conditions
or it may be true, but beyond the current capability of Perfect Developer's prover.
So your first task should be to work out for yourself whether you think the obligation is provable in
principle from the information available.
How would you persuade a fellow developer that the proof obligation represents a universal truth?
Remember that when you are faced with a failed proof obligation within a particular class method, the
only things that you (or the prover) can assume when the method is called are that:




The method precondition is satisfied;
The parameters to the method satisfy their type constraints;
The class invariant is satisfied;
The class variables satisfy their type constraints.
If you decide it isn't provable, you need to correct your specification or implementation. Often the
correction may be as simple as adding a new precondition, class invariant or post-assertion that you
now realize you were assuming all along, but which had not previously been stated.
On the other hand, if you believe that the obligation is provable, then you need to help Perfect
Developer find the proof. You do this by splitting the proof into smaller obligations.
Techniques to use:
The following techniques may be used to simplify or split proof obligations, making them easier for
both you and Perfect Developer to understand.
When declaring preconditions, assertions, class invariants and loop invariants, declare multiple terms in preference
to a conjunction
Whenever you declare a precondition, assertion, class invariant, loop invariant or 'satisfy' condition,
Perfect allows you to provide multiple expressions (separated by commas) after the appropriate
keyword. This is almost equivalent to bracketing each expression and then combining them using the
'&' operator. So declaring:
invariant #s ~= 0, s.head ~= `a`;
means the same as:
invariant #s ~= 0 & s.head ~= `a`;
However, there is one important difference. When multiple comma-separated terms are used, each term
will give rise to a separate proof obligation; whereas if you use '&', the entire invariant will be treated
as a single term and give rise to a single proof obligation.
6
It is better to obtain multiple obligations instead of a single, complex obligation. Not only are the
individual obligations slightly easier to prove, but if the system fails to prove one of them, the problem
term is pinpointed.
Use exact types and declare classes 'final' except where you need to extend them
Consider the fragments:
...x: T;
... x.f(p) ...
...y: from T; ... y.f(p) ...
In the first case, the call x.f(p) is statically bound and the prover can expand the call using the result
expression defined in the declaration of f. In the second case, the call y.f(p) is dynamically bound
(unless f is declared final in class T or one of its ancestors) and the prover cannot expand the call
unless the precise type of y is known at the point of call. The best that the prover can do is to assume
any postassertion that was provided in the declaration of f applicable to class T.
If you do need to have a hierarchy of classes derived from class T, you must ensure that sufficient
information is declared in the postassertions of all member functions, operators, selectors and schemas
to make the required properties provable when these methods are called. Otherwise, it is simplest to
declare classes final and not to use from.
Use references only where you need to
References in a specification or implementation make verification much harder. Only use references
where you genuinely need two or more references to the same object, such that any change to the
object through one reference will become visible through the other(s).
Where there is more than one reference to the same type in some context, consider carefully whether
such references should ever be able to refer to the same object. Where the answer is no, add a
precondition, class invariant or assertion stating that the references are not equal.
Use intermediate assertions in implementations and expressions
If you have a long implementation and the system fails to prove that the implementation satisfies the
specification or yields the correct return value, consider whether you can help the prover by using
assertions to state conditions that should be true in between statements. The prover will attempt to
prove each such assertion, and each assertion (whether proven or not) will be assumed correct when
attempting to prove obligations generated for subsequent statements.
If the location of a proof failure falls immediately after a conditional, try moving the proof obligation into each
branch of the conditional
Consider the following:
schema !foo
post ...
via
...
if [cond]: ... ;
[]: ... ;
fi
end;
7
Suppose the prover reports: "Unable to prove: Specification satisfied at end of implementation" for this
schema. Because the implementation ends with a conditional statement, we can split this proof
obligation into a separate obligation for each branch, like this:
schema !foo
post ...
via
...
if [cond]: ... ; done;
[]: ... ; done;
fi
end;
At any done statement, Perfect Developer will generate the proof obligation that the current state
satisfies the postcondition. So we will get one proof obligation for each branch of the if statement,
instead of one for the state after executing the if statement.
Here are some variations on this theme, starting with the one we have just illustrated:
Failing obligation
Context
Action
Specification satisfied at
end of implementation
Schema implementation
ending in if..fi, or done
immediately after if..fi
Insert "done;" at the end of each
branch of the if..fi
Class invariant satisfied at As above
end of implementation (of
a schema or constructor)
As above
Return value satisfies
specification
value statement with a
conditional expression
Replace "value ([g1]: e1, [g2]: e2,
...)" by "if [g1]: value e1; [g2]:
value e2; ... fi"
Assertion valid
assert statement
following if..fi
Put a copy of the assert statement
at the end of each branch of the
if..fi
Precondition satisfied
Method call following
if..fi
Determine the exact precondition
from the warning message, then
append a corresponding assert
statement to each branch of the
if..fi
Class invariant satisfied at As above
method call
Determine the exact class invariant
from the warning message, then
append a corresponding assert
statement to each branch of the
if..fi
If all else fails, try using proof lists in assertions
Any assertion (whether an embedded assertion or a post-assertion) can have a proof list attached like
this:
8
assert e1 proof p1; p2; ... pn end
Each element in the proof list may be either an assertion or a let-declaration. The final element may
also be a conditional proof (which is like a conditional statement, except that each branch is a guarded
proof list instead of a guarded statement list). See the Language Reference Manual for full details.
The assertions within a proof list are treated as lemmas that the system tries to prove, assuming all
previous lemmas in the process. The prover will then attempt to prove the original expression (e1 in
this example) assuming all the lemmas.
What it's all about
Perfect Developer is an object oriented software development tool for producing software that is
mathematically proven to be correct.
Although the developers of safety-critical software have for some years been building software systems
that can be mathematically shown to be correct, the techniques used have been difficult to learn and too
time-consuming to be applied to less critical software. "Formal methods" tend to be mathematicianfriendly, which is just fine if you love maths; not so good otherwise. The great thing about Perfect
Developer is that you don't need to be a mathematician to use it.
To use Perfect Developer, you construct a model. You use its own language, Perfect, to write your
specifications. You specify the model and the requirements, then you either refine the specification to
code or let Perfect Developer do it for you. You never write code except as a refinement to the
specification.
To prove that the software is correct, Perfect Developer attempts to verify the model against the
requirements. Any refinement you did is verified against the model.
Verification is performed by generating proof obligations. Proof obligations are also called verification
conditions. Proving these obligations is an automated process.
If you are not familiar with object oriented development ...
... then we suggest you read Introduction to Object Oriented Development before
continuing.
... but if you can do O-O development in your sleep ...
... then have a look at the differences in Perfect.
Verified Design By Contract
If Design-by-Contract is new to you, then by all means have a quick glance through this section now,
but there's no need to try to take in everything. It will become clearer by the end of this set of tutorials.
9
Design By Contract
If you have been brought up on Design By Contract (e.g. as supported by Eiffel), then you have already
taken a step towards Verified Design By Contract provided by Perfect. But there are some important
differences between DBC and VDBC.
First of all, in VDBC contracts are expressed in terms of an abstract data model, which may be very
different from the eventual implementation model. The abstract data model is the simplest possible way
of describing the essence of the data that is stored, avoiding any kind of redundancy or overspecification.
Second, in VDBC contracts are complete. In other words, the contract provides everything the client
needs to know about what the method does.
A corollary to this is that in Perfect, postconditions are complete; that is, they specify precisely what
variables are changed and how they are changed. In contrast, an Eiffel postcondition merely specifies
something that must be true when the method returns.
In order to provide for partial method specifications that are inherited even when a method is
overridden, Perfect provides an additional construct: the postassertion. A postassertion specifies
everything the client can assume even in the presence of polymorphism.
To summarize: in Perfect:


A method postcondition is a construct that says precisely which variables have changed and the
conditions satisfied by the final values of those variables. It is not a Boolean expression - the
syntax for postconditions is quite separate from the syntax for expressions. A Perfect
postcondition has no equivalent in Eiffel (because, in Eiffel, to determine precisely what the
method does, you have to look at the statements making up the method body). If you redefine
(i.e. override) the method in a derived class, the Perfect postcondition is not inherited - you
always supply a new one (just as in Eiffel, you would supply a new method body).
A method postassertion is a Boolean expression stating something that must be true when the
method finishes. It must be a provable consequence of the postcondition. When you redefine the
method in a derived class, the postassertion is inherited (and must be respected by the new
postcondition). You can override the postassertion if you like, but the new postassertion must
respect (i.e. imply) the old one. A Perfect postassertion performs a similar role to an Eiffel
postcondition.
Hello (Java) world!
Important! This walk-through guide is only for those of you who are using a Windows platform, and
you need to have Java 1.4 SDK or later already installed.
If the following doesn't work as indicated, it is likely to be because your installation isn't correctly
configured. Please refer to the Knowledge Base in the Support section of the website for help with this:
Direct links to the Knowledge Base guides (each opens in a separate window)

Creating a Java application using Perfect Developer ...
10

Interfacing a Java graphical user interface to a Perfect application
Now try the following steps:
Run Perfect Developer. from either the Desktop short-cut or Start/Programs...
You'll see a typical graphical user interface (GUI).
We call this GUI the Perfect Developer Project Manager.
In the Project Manager, select File/Open project and browse to HelloWorld. If you
installed the software to the default location, this should be found at C:\Program Files\Escher
Technologies\Perfect Developer\Examples\HelloWorld.
You should be able to see a file called Hello.pdp - the pdp extension means it is a Perfect Developer
Project.
Double-click hello.pdp to open the project.
Locate the Build button on the tool-bar, and click it.
Perfect Developer will now build the project, giving
you a running commentary as it does so. Finally you
should see an alert box with a message “Job
completed with no problems detected”.
Click OK to clear the message box away.
To run the generated Java program, go to the
CommandPrompt, browse to HelloWorld as above,
and then to the output subdirectory. You should be able to see a file called hello.jar.
Run this file by entering java -jar hello.jar
Having reached
this stage, you can
be sure that your
installation is
configured
correctly!
11
The next step is to change the example a little.
Now go back to the Project Manager.
Find the line C:\Program Files\Escher Technologies\Perfect Developer\Examples\HelloWorld\Main.pd
Double-click it. This will open the source file in whatever text editor you are using. The default is
Notepad.
Alter the text of the message, save it, and return to the Project Manager. Rebuild the project.
If you get an error, double-clicking on the error will take you to the line where the error has been
identified.
Experiment for a while!
Introducing Perfect
A Perfect source file contains identifiers, reserved words, symbols, literals and various kinds of white
space. Here we will introduce the forms of identifiers allowed, the related details that you will need to
know, and the significance of the symbols used.
Rules about identifiers
In Perfect, identifiers are case-sensitive and consist of any number of letters, digits and the underscore
character.


The first character may not be a digit.
There is no limit on the length of an identifier.
However, if you are compiling to C++, bear in mind that some C++ implementations place a limit on
the number of significant characters, particularly for identifiers that are declared in one module and
referred to in another.
Many object-oriented developers use identifiers that start with an uppercase letter for classes, and
identifiers starting with lowercase letters for variables, methods and parameters. You may use this
convention if you wish, even though it is always clear in Perfect whether or not an identifier refers to a
class.
Words not allowed as identifiers!
You may not use a reserved word, such as value or change, as an identifier. A complete list of reserved
12
words is given in Chapter 3.6 of the Language Reference Manual (opens in a new window), which you might
wish to keep open while you're working through the tutorial.
White space
Spaces, newlines (i.e. carriage return and/or linefeed characters) and tab characters are known
collectively as white space. Apart from one exception, white space serves only to separate adjacent
tokens (i.e. reserved words, identifiers and symbols) and it is otherwise ignored. The exception is that
newline also terminates a comment.
Symbols used in Perfect
//
Introduces a comment that terminates at the end of the line. This is the only form of comment available in
Perfect.
;
is used as a separator between declarations and/or statements. A semicolon after the final declaration or
statement is not needed but is permitted. When statements are separated by semicolon, they are executed
sequentially.
,
is used as a separator between expressions, between postconditions and between statements. When
statements are separated by comma, they are executed concurrently. Likewise, postconditions separated
by comma are satisfied concurrently.
^=
is pronounced is defined as and means exactly that. For example, const myFavoriteNumber ^= 3 defines
the constant myFavoriteNumber as being the value 3.
:
is pronounced of type. It is followed by a type expression and declares the preceding entity to be of that
type. For example, function square(x: int): int declares a function called square that takes a parameter x
of type int and returns another int.
::
is pronounced belonging to (or just in). It is followed by an expression of a collection type and declares
the entity to range over the values in that collection. Only declarations of bound variables may have this
form.
For example, x::s means that x ranges over the elements of collection s.
always appears between a bound variable declaration and a predicate. In a forall expression it is
pronounced it is the case that; in an exists, any, that or those expression it is pronounced such that.
:For example, forall x::myScores :- x > 50 means forall x in myScores, it is the case that x is greater than
50.
!
Exclamation mark, or bang, ! means that a change of value occurs in the entity (variable, parameter or
self) that precedes it (or occasionally follows it).
13
For example, myAccount!withdraw(someMoney, success!) invokes the method withdraw on the variable
myAccount, with parameters someMoney and success, changing the values of variables myAccount and
success in the process.
Prime ' means take the final value of the expression that precedes it instead of the initial value.
For example, the expression x' = x + 1 says that the final value of x is one greater than its initial value.
'
Similarly, the expression myAccount'.balance < myAccount.balance says that calling the function
balance on the final value of myAccount yields a lower value than calling the same function on the initial
value (this might be an unfortunate consequence of calling the withdraw method!).
Asperand @ is used between a name and a class name to indicate that the meaning of the name is to be
found in the specified class.
@
So, the term overdrawn@AccountStatus refers to a constant or a function called overdrawn declared in
class AccountStatus. The equivalent in Java would be AccountStatus.overdrawn, or in C++
AccountStatus::overdrawn.
&
Ampersand & means logical and.
|
means logical or.
~
Tilde ~ means logical not. It can also be used as a prefix to a comparison operator, or to the in operator,
reversing the meaning.
For example, ~< means not less than, ~= means not equal, and ~in means not in.
~~
does not mean double negation (which would be pointless), it is the compare operator. Use it to compare
two values, yielding one of below@rank, same@rank, above@rank.
= means equality, not assignment.
=
||
So a = b is a Boolean expression comparing the values of a and b, not a statement assigning the value of
b to a.
|| is used between two type expressions to unite those types (i.e. create a type that accepts all the values of
either component type).
For example, a variable of type int || bool accepts values true and false as well as 1, 2, 3 ... .
#
Octothorpe, or hash, # is used for various operators that count a number of elements in the built-in
collection types
For example, in evaluating the length of a sequence.
14
++
++ is a binary operator, typically used to combine two collections (e.g. set union, sequence
concatenation).
--
-- is a binary operator, typically used to remove the elements in one collection from another.
**
** is a binary operator, typically used to find the common elements in two collections.
>
> as a binary operator means greater than, and as a unary operator means successor (equivalent to "+1"
when applied to an integer operand).
<
< as a binary operator means less than, and as a unary operator means predecessor (equivalent to "-1"
when applied to an integer operand).
..
.. is a binary operator taking two operands of integer, character or an enumerated type. It is typically
defined as yielding a sequence of all those values of the type from the first up to the second, in ascending
order.
?
? is used in Perfect to mean "we haven't decided what belongs here yet". You can use this in many places
in Perfect to make your source file syntactically correct even though you haven't finished it yet, allowing
you to run the source file through the compiler to check what you have written so far.
As well as the above symbols, Perfect uses the symbols * / % ## %% and ^ as binary operators, and +
and - as unary or binary operators.
A Perfect language processor will always construct the longest symbol it can from the source text
before starting a new one. For example, x--y is recognised as the operator -- between identifiers x and y
even if no such operator is defined between the corresponding types. If you meant to negate y and
subtract the result from x, you had better include a space between the two - symbols, or (preferably) use
x-(-y) instead.
Brackets



Round brackets are used to parenthesize expressions, as in 3 * (4 + 5), and to surround the
parameters in a method call, such as members!add("David Crocker", "Escher Technologies
Limited"). They are also used to parenthesize types and parts of postconditions.
Square brackets are used to invoke the indexing operator (like indexing into an array in most
programming languages) or to enclose conditions (also called guards) in conditional constructs.
You can always tell which of these meanings a pair of square brackets has, because square
brackets that are part of a conditional construct are not directly preceded by an expression. So
both sets of square brackets in ([x > y]: x, []: y) are there to enclose conditions (the second
condition is empty).
Curly brackets are used only to enclose the parameter list in a constructor call. So if you see the
expression account{"Joe Bloggs", 0.0} then you can be sure that this is a constructor call and
account must be a type name.
The various kinds of literals (numbers, strings etc.) are discussed when we talk about the classes to
which they relate.
In Basic Tutorial Two, we will go on to the built-in data classes bool, char, and int. We'll show you
how to construct your own enumeration classes, and we'll cover the most commonly used expressions.
15
Class bool
Class bool has the values denoted by literals true and false.
The following binary operators are provided between Boolean objects:
&
logical "and"
|
logical "or"
==>
implication (e.g. a ==> b means the same as ~a | b )
<==
reverse implication (e.g. a <== b means the same as a | ~b
)
<==>
equivalence (e.g. a <==> b means the same as a = b )
and the following unary operator:
~
logical negation ("not")
For the binary operators, the second operand need not be well defined if the value of the expression can
be determined from the first operand alone (e.g. the expression i = 0 | (10/i > 3) is wellformed).
Aside from the operators, class bool has one other interface member:
function toString: string
returns "true" or "false" as appropriate
Class bool is a final class, so other classes cannot inherit from it.
Class char
Class char represents characters in some character set. A Perfect implementation will typically provide
Unicode; it may provide other character sets such as ASCII as well. The required character set is
selected when invoking the compiler.
Character literals are written in back-quotes, e.g. `a` (not 'a' as in many programming languages).
Special characters can be expressed using escape sequences (introduced by the backslash character), of
which the following are most useful:
`\n`
Newline
`\a`
Alert (bell)
`\t`
Horizontal tab
`\``
Back-quote
`\\`
Backslash
The character whose code is the integer literal
123
Methods of class char include unary ">" (successor) and unary "<" (predecessor), both yielding char.
`\(123)`
16
For example, (>`0`) = `1` and (<`C`) = `B`.
A character can be converted to its integer equivalent in the underlying character set using the unary
"+" operator. A character object can be created from its integer equivalent by construction, e.g.
char{123}.
Other interface members include the following:
function isLetter: bool
true if the character is a letter
function isDigit: bool
true if the character is a digit
function isPrintable: bool
true if the character is not a control character or space
function digit: nat
returns the numerical value corresponding to the
character, which must be a digit. For example,
`0`.digit = 0
function toString: string
returns a string of length 1 containing the character
Class char is a final class, so your own classes cannot inherit from it.
Class int
Class int represents all integers (positive, negative and zero). In principle, there are no bounds on the
value that can be represented by an int.
Perfect allows non-negative integer literals to be expressed in decimal format (e.g. 123), in binary (e.g.
0b1010) or in hexadecimal format (e.g. 0x4ac5). In the case of binary and hex formats, the letters
may be any mixture of upper and lower case.
A construct such as -12 is not an integer literal, it is a minus-sign (representing the negation operator)
followed by an integer literal.
The methods of class int include the following binary operators:
+ addition
- subtraction
* multiplication
/ integer division
% remainder
^ exponentiation
In each case, the second operand is also of type int and so is the result.
For the integer division and remainder operators, the divisor must be positive. This sort of restriction is
called a precondition. We will discuss preconditions further in a later tutorial.
17
You can look up the preconditions for all the predefined operators of Perfect in the Library Reference
page of the Language Reference Manual (look under the entry for the class of which the operator is a
member, e.g. int).
The result of integer division is rounded towards minus infinity, and the result of the remainder
operator is always in the range 0 to one less than the second operand. This means that the expression
(a/b)*b + a%b = a is always true (remembering that b must always be positive).
For the exponentiation operator, the second operand must not be negative, and at least one of the
operands must be non-zero.
Class int also defines the following unary operators:
- negation
< predecessor, equivalent to subtracting one
> successor, equivalent to adding one
Again, each of these yields another int.
The toString method of class int yields a string of decimal digits representing the integer. An int can be
constructed from a string of decimal digits with an optional leading minus-sign, for example
int{"123"} = 123.
Class int is a final class, so your own classes cannot inherit from it.
Enumeration classes
An enumeration is a class having a finite number of named, constant instances.
For example, we might wish to declare a class called Color whose instances are named red,
yellow, orange, green, blue, black, white.
We declare such a class in Perfect like this:
class Color ^= enum red, yellow, orange, green, blue, black, white
end
Because the instances are declared as member constants of the class, we need to give the class name
when referring to them, using the syntax name@class as in the following example:
var colorOfCar: Color; ...
colorOfCar! = red@Color;
Whenever you declare an enumeration class, the predecessor and successor unary operators < and >,
the sequence construction operator .. and the function toString are automatically defined for you.
The predecessor and successor operators yield the previous and next values in the list of instance names
respectively. For example, <yellow@Color yields red@Color, while <red@Color is illegal
because red is the lowest value of type Color and therefore has no predecessor.
18
The usual comparison operators (including the rank operator ~~) are also defined automatically for
you. Values in the enum list compare higher than earlier values in the list and lower than later values;
for example, red@Color < black@Color yields true.
Enumeration classes also support the type operators highest and lowest. Using the above example, the
expression lowest Color yields red@Color, while highest Color has the value
white@Color.
Enumerations are implicitly final classes, so it is not possible for other classes to inherit from them.
Expressions overview
Perfect has a wide range of expression types. In Perfect, expressions do not have side-effects, so
expressions are never ambiguous.
Some types of expression have preconditions.
Here, just think of preconditions as conditions that must be true for the expression to be legal.
For example, if total and number are integer variables, the expression total/number has the
precondition number ~= 0 (i.e. "number not-equal-to zero").
In the rest of Basic Tutorial Two, we will look at operator expressions (including comparisons).
We will discuss how you can use brackets



to force evaluation order
to introduce names for temporary values
and build conditional expressions.
More complicated stuff is left for Basic Tutorial 3. That is where we will cover


expressions with bound variables, which are powerful constructs that usually involve collections
type conversions and type enquiries.
Operator Expressions
We have already introduced some of the predefined operators for the built-in classes of Perfect. A full
list of predefined operators and their preconditions may be found in the Library Reference chapter of
the Language Reference Manual. The relative precedence of operators is more or less what most people
expect, but if in doubt you can use brackets.
Unlike some programming languages, when you use a unary operator expression as an operand of
another expression, you need not use brackets around the unary operator expression. So you will often
see expressions like <#accounts. Can you work out what this means, assuming that accounts is a
seq of something?
19
Comparison operators
The comparison operators (including the equality operator) and their negated forms do not behave in
quite the same way as other operators.
First, equality is defined automatically for all classes (although sometimes it is not available at run-time
- see ghost methods). Also, you can compare any two expressions for equality; the only requirement is
that the types of the two expressions overlap, i.e. they could conceivably be of the same actual type at
run-time; otherwise they could never be equal.
The other comparison operators are all related to the binary ordering operator ~~, which may be
pronounced compare. This operator returns a value of the enumeration class rank whose members are
below, same and above.
It is guaranteed that two equal values compare same@rank, also that a ~~ b yields the reverse of b
~~ a (i.e. above@rank swaps with below@rank). The compare operator is also transitive. To see
what this means, imagine that all the possible values of a type are partitioned into a number of sets, and
these sets are placed in some linear order. Then to decide how two values rank with each other, see
which sets they belong to. If the first one belongs in a set that comes before than the second, the answer
is below@rank; if they are in the same set, the answer is same@rank; otherwise it is
above@rank.
The binary operator > is defined such that a > b yields true when a ~~ b yields above@rank;
similarly a < b is equivalent to a ~~ b = below@rank. You can also use a >~ b which yields
true when a ~~ b yields above@rank or same@rank, or a <~ b which is equivalent to a ~~
b ~= above@rank.
If the ~~ operator defines a total ordering (i.e. each partition contains just one value), then two unequal
values can never compare same@rank. Under these conditions, the comparison operators >= and <=
are also defined to mean the same as >~ and <~ and turn out to mean just what you would expect.
Unlike most programming languages, you can string comparisons together just as in mathematics. For
example, a < b < c means the same as a < b & b < c.
The built-in classes int, char and bool have a predefined operator ~~ which defines a total ordering.
For class int, the ordering is exactly what you would expect. For class char, a ~~ b is defined as
(+a) ~~ (+b). For class bool, true is considered greater than false. Operator ~~ is also
predefined for all enumeration classes.
Brackets and Conditional Expressions
Brackets
Brackets can be used in the usual way to force an evaluation to occur in a particular order. They can
also be used to define names for temporary values, using the keyword let. For example, to compute the
fourth power of x, we could use the expression:
(let square ^= x * x; square * square)
We can also include assertions within the brackets:
(assert x ~= 0; x * x)
20
This tells the verifier (and anyone reading the specification) that x should not be zero at this point.
You can have multiple let and assert parts in one expression, e.g.:
(let square ^= x * x; assert square >= 0; let fourth ^= square * square; fourth *
square )
Conditional expressions
A conditional expression is a special form of bracketed expression, containing two or more guarded
expressions.
A guarded expression takes the form:
[condition]: expression
An example of a complete conditional expression is:
( [total > 10]: "lots", [total > 5]: "a few", []: "not enough" )
which may be read: 'If total greater than 10 then "lots", else if total greater than
5 then "a few", else "not enough"'.
When evaluating a conditional expression, the guards are evaluated in order until one evaluates to true;
then the corresponding expression is evaluated. The remaining guards and the other expressions are not
evaluated. As shown here, the last guard may be empty (i.e. no expression is given within the square
brackets), which is equivalent to the condition true and means otherwise (or else).
You may place let and assert parts between the opening round bracket and the first guard; for example:
( let total ^= mine + yours;
assert total >= mine;
[total > 10]:
"lots",
[total > 5]:
"a few",
[]:
"not enough"
)
This example also demonstrates one possible layout for conditional expressions that do not fit on a
single line.
Calling Functions and Constructors
Function calls
Functions that are not class members are invoked using the function name followed by the parameter
list in brackets, for example:
myFunction(x, 2)
Member functions may be invoked on the current object self in the same way, or on a specific object
using the dot notation.
21
If a function takes no parameters, an empty pair of brackets is not used. This means that the expression
myAccount.balance could either refer to a member variable balance within the object myAccount,
or the result of calling a member function balance on object myAccount. This is deliberate; it allows
you to replace a member variable by a function without having to change every reference to it (and
clients of myAccount would typically have no business knowing whether balance is a variable or a
function anyway).
Constructor calls
A class declares constructors to build new values of that class. A constructor is invoked by giving the
class name follows by a parameter list in braces (curly brackets), like this:
BankAccount{"Joe Bloggs", 100}
This would yield a value of class BankAccount whose initial value is somehow related to the string "Joe
Bloggs" and the number 100. We've seen other examples of constructor calls in the descriptions of
built-in classes, for example char{123}.
If there are no parameters, an empty set of braces is used so that we can identify the expression as a
constructor call. A constructor that takes no parameters is called a default constructor for the class.
Collection classes
*** You need to enable Javascript for this page to work fully! ***
Objects belonging to collection classes hold multiple values, rather like arrays in programming
languages.
There are three basic collection classes available in Perfect. These are set of X, bag of X and seq of X.
The parameter X is a placeholder for whatever class you wish to have a collection of, so you can have
for example seq of int, seq of char (which is also known by the name string), or seq of BankAccount
(provided you declare the class BankAccount).
An object of class set of X holds an unordered collection of objects of class X with duplicates
prohibited. Class bag of X represents a similar collection except that duplicates (and multiple instances
in general) are permitted. An object of class seq of X is like a bag of X except that it is ordered, so we
can enquire what value is at a particular position in the sequence (just like indexing into an array).
The constructors for each of these class takes a list of objects of type X, for example set of
int{1, 3, 5}. To build an empty collection, just use an empty parameter list, e.g. seq of
real{}.
All three of these collection classes implement the unary "#" operator, which returns the number of
elements in the collection as an int and a member function empty which returns true if the length is
zero. For example, # set of int{1, 3, 5} yields 3. To test whether a value occurs in a
collection, use the binary in operator (which returns bool); e.g. 3 in set of int{1, 3, 5}
yields true. The bag and seq classes also provide a binary "#" operator for counting the number of
times an element occurs (e.g. 3 # bag of int{1, 2, 3, 1, 3, 1} has the value 2.
To add an element to a collection (yielding a new collection of the same type of object), use the append
22
member function. In the case of seq the new element is added at the end of the sequence (e.g. seq of
int{1, 2}.append(3) = seq of int{1,2,3} ). To add an element at the beginning of a
sequence use the prepend member instead.
To combine two collections to form a new collection containing all the elements from the originals, use
the "++" operator. This operation is called uniting for set and bag operands, or concatenation for seq
operands.
You can remove individual elements from a set or bag using the remove member function, or whole
groups of elements using the "--" (set or bag difference) operator.
To find all the elements that occur in two sets or two bags, use the "**" (intersection) operator.
For example, set of int{1, 2, 3} ** set of int{5, 3, 4} = set of int{3}.
To test whether one set or bag is contained in another, use the "<<" or "<<=" operator. When these
operators are used between sequences, they test whether the first operand is a subsequence of the
second. The difference between the two operators is that "<<=" also returns true if the collections are
equal, whereas "<<" does not.
You can also use ">>" or ">>=" which behave the same as "<<" and "<<=" but take the operands in
the opposite order.
You've probably already guessed that all three of these collection classes are
fin
Expressions with Bound Variables
Operations on collections
A number of operations are provided on values of the built-in collection types set, bag and seq. In each
case we declare a bound variable, which has the type of a single element of the collection. In the
following, condition and expression are expressions that involve the bound variable identifier.
forall identifier::collection :- condition
yields true if all the elements of collection satisfy condition, or if collection is empty. For example,
forall c::"4321" :- c.isDigit
yields true. (The literal "4321" has type seq of char - also called string).
An expression of the form
exists identifier::collection :- condition
yields true if at least one of the elements of collection satisfies condition. So
exists c::"hello world" :- c.isDigit
yields false.
The expression
those identifier::collection :- condition
23
yields a result of the same type as collection comprising those elements of collection that satisfy
condition. For example,
those q::"hello world" :- q ~in "aeiou"
yields "hll wrld".
An expression of the form
that identifier::collection :- condition
yields the element of collection that satisfies condition (there must be exactly one). So
that x::seq of int{1, 10, 100} :- 5 < x < 50
is well-formed and has the value 10.
The expression
any identifier::collection :- condition
yields any element of collection that satisfies condition (there must be at least one). For example, any
w::1..10 :- w%2 = 0 could yield any of 2, 4, 6, 8 or 10. (Recall that 1..10 constructs a
sequence of the integers 1 to 10 in ascending order, and % is the remainder or modulo operator).
The last expression type in this group is a little more complicated but very powerful. The expression
for identifier::collection yield expression
yields a result similar to collection except the element type is the type of expression. The result
comprises the elements in collection mapped to the values defined by expression. For example,
for x::"ibm" yield <x
yields "hal" (in this case, expression has the same type as the elements of collection, so the result type
is the same as the type of collection).
An expression of the form
for those identifier::collection :- condition yield expression
is similar, but only those elements of collection that satisfy condition are taken. So if you can remember
what conditional expressions look like, you should be able to satisfy yourself that
for those thingy::"Go 4 it!"
:- thingy.isLetter | thingy.isDigit
yield ([thingy.isLetter]: "letter", []: "digit")
is equal to
seq of string{"letter", "letter", "digit", "letter", "letter"}.
If you use any of these expressions as an operand in a larger expression, you will need to enclose it in
brackets.
Variations on expressions involving bound variables
Some of the expressions introduced in the preceding section have alternative forms.
First, in the forall and exists expressions, we can declare the bound variable as having a type instead of
belonging to a collection. The bound variable then ranges over all possible values of that type. The type
24
may be finite (e.g. an enumeration type), or infinite (e.g. int).
Now, it may seem odd that in a forall or exists expression, we allow the bound variable to range over
an infinite type. Surely such an expression could take an infinite amount of time to compute? This is
true, which is why Perfect Developer will refuse to generate code if the type is infinite. However, it can
be useful to use these sorts of expressions in specifications, and the validator is quite used to handling
them.
You can also declare more than one bound variable in a forall or exists expression (see the Language
Reference Manual for details).
Second, in the that expression, we can abbreviate that identifier::collection :- true to
that collection. Obviously, collection had better contain exactly one element! We can do the
same for the any expression.
More on Sequences
You can construct a seq of char using a quoted string; for example, "hello" means the same as seq
of char{`h`,`e`,`l`,`l,`o`}.
Elements in a seq can be accessed by position. Following convention in most programming languages,
the indexing selector is represented by square brackets. Valid indices range from 0 to one less than the
length of the sequence; for example, "abc"[1] yields `b`, while "abc"[3] is invalid.
The index of the last element of a sequence x is readily expressed as <#x which reads "predecessor-of
length-of x".
Quite often, it is useful to process a sequence by splitting it into a single element and the remainder. To
split after the first element, use the member head to get the first element and tail to get the rest. If you
wish to split just before the last element, use members front and last instead. Naturally, x.head is
equivalent to x[0], and x.last is the same as x[<#x].
Aside from using constructors and string literals, there is another way to construct a sequence of
elements where the element type is char, int or an enumeration class. All such classes define the ..
binary operator, which constructs a sequence of ordered values from the first to the second operand
inclusive. For example, 1..4 is equivalent to seq of int{1,2,3,4}. If the second operand is less
than the first, an empty sequence is returned.
Other useful methods include:
take(n)
returns the first n elements of the sequence (n must not exceed the length of the
sequence)
drop(n)
returns all except the first n elements of the sequence (n must not exceed the
length of the sequence)
slice(d,
t)
returns the first t elements starting at position d (d+t must not exceed the length
of the sequence)
begins(s
returns true if the sequence starts with the sequence s
)
ends(s)
returns true if the sequence ends with the sequence s
25
isndec
returns true if the sequence is non-decrementing (i.e. each element is not less
than its predecessor)
returns true if the sequence is non-incrementing (i.e. each element is not
greater than its predecessor)
See the Class Library Reference in the Language Reference Manual for other methods of class seq of
X.
isninc
Declaring Subtypes
Sometimes an existing class may offer the functionality and behaviour you need, but it is useful to give
the class a new name to identify the type of information being stored.
You can declare the new name like this:
class ExamScore ^= int;
(Remember? the symbol ^= reads is defined as - see Symbols).
Often the range of values supported by the existing class is more than you need. For example, it might
be that an exam score cannot be negative and cannot exceed 100. This is a constraint.
You can incorporate a constraint in your definition like this:
class ExamScore ^= those x: int :- 0 <= x <= 100;
which can be read as
Class "ExamScore" is defined as those x of type int such that x lies between
0 and 100 inclusive.
Type constraints are not limited to simple range constraints. For example, if exam scores are always
even numbers, we can declare:
class ExamScore ^= those e: int :- 0 <= e <= 100 & (e % 2) = 0;
Using the definition of the enumeration class Color given earlier, we could declare a class for
representing the colors of traffic light bulbs like this:
class LightBulbColor ^= those x: Color :- x in set of Color {red@Color,
yellow@Color, green@Color};
which reads
Class "LightBulbColor" is defined as those x of type Color such that
x is in the set of Color constructed from values red, yellow and
green
The Perfect language includes the following built-in type definitions:
class nat ^= those x: int :- x >= 0;
class string ^= seq of char;
You should use nat instead of int to declare any variable (or parameter, or return type) that you know
can never be negative, because the more information you give to Perfect Developer, the more it can
help you, either by finding bugs in your system or by proving it correct. The pseudonym string is
26
provided merely as a convenience (it reads more naturally than seq of char and is quicker to type).
United Types
Sometimes we need to declare an entity whose precise type may depend on how the program executes,
or which may even vary during program execution.
For example, a function to return the balance of a customer account might normally return an integer,
but if it fails for any reason (e.g. account unknown, or server inaccessible), it might instead return a
string giving the reason for failure. We can represent the type of such an entity by a union, like this:
int || string
If we expect to use this type for declaring several entities, it may be wise to give it a name, as we
learned to do in the last lesson:
class BalanceOrReason ^= int || string;
Although united types (or variant records in some languages) are commonly needed in procedural
languages, object-oriented software engineers rarely use them, because it is usually better to declare a
class hierarchy instead. However, in Perfect there is a special case which is frequently needed: uniting
a type with void. Recall that type void has a single value called null. So by uniting a type with void,
we have the option of supplying either a value of that type, or no value at all (i.e. null).
For example, suppose a class of students sits an examination and we wish to record the results. If some
of the students are unable to sit the examination due to illness, it seems unfair to give them a score of
zero, so we wish to indicate instead that no result is available. If exam scores are integers in the range 0
to 100, we might declare the following:
class RealExamScore ^= those x: int :- 0 <= x <= 100;
Then we can represent a single examination result (allowing for the possibility that the examination
was not taken) as a value of type RealExamScore || void, and we can store the anonymized
collection of student exam scores as a bag of (RealExamScore || void).
If we also declare:
class ExamScore ^= RealExamScore || void;
then we use type ExamScore when declaring a single result, and type bag of ExamScore when
declaring the collection.
Note that the type union operator || has a very low precedence; so if you use a united type after the of
keyword, you need to enclose it in brackets. In other words, bag of RealExamScore || void
(without brackets) means the same as (bag of RealExamScore) || void.
Type Conversions and Type Enquiries
Although Perfect is strongly typed, it nevertheless supports the declaration of entities whose exact type
is not known at compile time. This is done by uniting two or more types using the || operator, or by
27
applying the keyword from to the name of a non-final class (this will be covered in the tutorial on
inheritance).
The types of such values can be tested at run-time using a type enquiry expression. For example, if
variable answer has type string || int || void then we can use the following type enquiry
expressions:
answer within int
will yield true when the current value of answer is of type int, otherwise false;
answer ~within string
yields true when the current value of answer is not of type string;
answer within int || string
yields true when the current value of answer is an int or a string (since the only other possibility is void
which has the single value null, this yields the same value as the expression answer ~= null); and
answer like guess
yields true when the run-time values of answer and guess are of the same type.
Turning now to type conversions, we have the type narrowing conversion:
answer is int
which is only valid when the current value of answer is of type int, and yields the value of answer as a
plain int. The converse is a type widening conversion such as:
42 as int || void
which converts the value 42 to type int || void.
If a within, is or as expression is used as an operand in a larger expression or postcondition, you will
need to enclose it in brackets.
Automatic type conversions
The only automatic type conversion performed in Perfect is the widening of a value from its original
type to a type that encompasses the original.
Widening is performed automatically on expressions in the following contexts:




After the ^= symbol, if a type has been declared for the entity being defined;
On the right hand side of an assignment postcondition, to match the type of the left hand side;
When passing a parameter to a method;
After a guard in a conditional expression.
For example, we could declare:
const theAnswer: string || int ^= 42;
Because we have declared theAnswer to be of type string || int, the value that follows the is-defined-as
symbol should conform to this type; but instead we have supplied an expression of type int. The
compiler will automatically widen the expression 42, equivalent to changing the declaration to:
const theAnswer: string || int ^= 42 as string || int;
The compiler will never automatically insert the inverse type conversion (corresponding to an is
conversion); for example, we cannot supply theAnswer as defined above in a context that requires an
28
int.
The rule for conditional expressions is that there must be at least one branch whose expression type T
includes the types of all the other branches. Then the other branches will be automatically widened to
type T. For example,
([finished]: 42, []: null)
is not allowed because the types int and void do not overlap; however,
([finished]: 42, []: null as int || void)
is legal because the type of the expression 42 can be automatically widened to int || void.
Although widening is applied to the operands of normal operators, it is not applied to operands of
comparison operators. This is because comparisons are in any case permitted between operands of
different types, provided only that the types have some common ancestor besides the root class
anything.
When determining type compatibility, constraints are ignored. Perfect compilers will generate validity
conditions to check that constraints are satisfied whenever a value is required to conform to a
constrained type, such as during assignment or parameter passing.
Classes and Methods overview
It's time to describe simple class declarations in Perfect. We'll describe class structure and class
constructors, how to declare variables and invariants. We'll describe the kinds of method that can be
declared. We won't be describing class destructors, because Perfect doesn't have those!
The four kinds of method are:




functions
operators
selectors
schemas
In this tutorial, we'll only look at functions. In the next one, we'll look at schemas.
To show you the main ideas, we'll explain the development of a simple Book class.
Simple class structure
Classes in Perfect are divided into a number of sections. The most common sections are the abstract
and interface sections - indeed, many classes have only these sections. So here is an example of a
simple class, complete with a constructor:
class Book ^=
abstract
var title: string,
authors: set of string;
interface
build{!title: string, !authors: set of string};
end;
29
This declares a new class called Book having attributes (i.e. data) title and authors. A book has a single
title but may have several authors. We chose to represent the collection of authors as a set rather than a
sequence or bag, on the assumption that the authors are distinct and it doesn't matter what order they
are listed in.
The class also has a single constructor, which is the declaration that starts with the keyword build. This
constructor takes a parameter of type string and another parameter of type set of string. By giving
each parameter the same name as one of the attributes and preceding it with an exclamation mark, we
indicate that the attribute is to be initialized directly from the corresponding parameter.
We can create a Book by using a constructor call expression like this:
... Book{"Life, the Universe and Everything", set of string{"Douglas Adams"}} ...
For example, we could add the following declaration, outside the class declaration:
const myFavoriteBook ^= Book{"Life, the Universe and Everything", set of string{"Douglas
Adams"}};
As it stands, this class isn't very useful. We can create books, but we have to provide a list of authors
each time, even though most books have only one author. After creating a book, we can't do anything
with it except test it for equality with another book - we can't even get at its title and authors because
they are private variables.
By the way, two objects are equal in Perfect if each attribute (i.e. abstract variable) of one is equal to
the corresponding attribute of the other.
Let's start by adding another constructor that lets us pass just one author parameter instead of a set:
class Book ^=
abstract
var title: string,
authors: set of string;
interface
build{!title: string, !authors: set of string};
build{!title: string, author: string}
post authors! = set of string{author};
end;
The second constructor means we can declare:
const myFavoriteBook ^= Book{"Life, the Universe and Everything", "Douglas Adams"};
as well as the longer version. Note that the author parameter in the new constructor declaration is not
preceded by an exclamation mark, because there is no variable named author to be initialised from the
parameter. The post keyword introduces a postcondition. We will cover postconditions in more detail
later on, but for now all you need to know is that post v! = e states that the attribute v (and
nothing else) changes such that its final value is equal to e.
Let's now deal with accessing the title and author of a Book.
First,we'll provide a way of extracting the title of a book. The usual way of doing this sort of thing in
object-oriented languages is to provide an accessor function (perhaps called getTitle), which we can do
in Perfect like this:
30
class Book ^=
abstract
var title: string,
authors: set of string;
interface
function getTitle: string
^= title;
build{!title: string, !authors: set of string};
build{!title: string, author: string}
post authors! = set of string{author};
end;
but in Perfect, a simpler way is to simply redeclare title as an interface function like this:
class Book ^=
abstract
var title: string,
authors: set of string;
interface
function title;
build{!title: string, !authors: set of string};
build{!title: string, author: string}
post b! = set of string{author};
end;
This means that given an entity b of type Book, the expression b.title yields the value of the title
variable in b.
Redeclaring a variable as an interface function provides read-only access to it, just like an accessor
function. It is also possible to redeclare a variable as an interface selector so as to provide read-write
access to it, but this is rarely a good idea.
More functions
Now let's provide some sort of access to the authors attribute of our Book objects. Rather than make
authors publicly readable, let's instead add a method to check whether a particular person is included in
authors. We can do this by adding a method hasAuthor as follows:
class Book ^=
abstract
var title: string,
authors: set of string;
interface
function title;
function hasAuthor(author: string): bool
31
^= author in authors;
build{!title: string, !authors: set of string};
build{!title: string, author: string}
post authors! = set of string{author};
end;
Now we can use the expression b.hasAuthor("Douglas Adams") to check whether "Douglas
Adams" is one of the authors of the Book object b.
Looking at the function declaration in more detail, we can see that it comprises the keyword function
followed by the name we have chosen, then comes the parameter list (if there are any parameters), then
the return type is declared (after a colon, just as for variables and parameters). Note that a function is
not allowed to change either the abstract data of the object it is called on, nor its parameters - all it can
do is return a value. (Actually, functions can return several values at once, but we'll skip this facility for
now).
Let's add a second way of accessing the authors variable, by declaring a function to return any one of
the authors. Of course, when there are several such authors, the result is not precisely defined. The
function is therefore nondeterministic, which we indicate by declaring it opaque. Here is one way of
declaring an anyAuthor function:
class Book ^=
abstract
var title: string,
authors: set of string;
interface
function title;
function hasAuthor(author: string): bool
^= author in authors;
opaque function anyAuthor: string
^= any authors;
build{!title: string, !authors: set of string};
build{!title: string, author: string}
post authors! = set of string{author};
end;
We have defined anyAuthor using an any expression to choose any element from the collection
authors.
However, there is another way we can specify what a function returns, which is particularly useful for
opaque functions:
opaque function anyAuthor: string
satisfy result in authors;
Instead of using the is-defined-as symbol ^= to define the return value, we use the keyword satisfy to
introduce a condition (or several comma-separated conditions) that the result must obey.
32
Before we move on, take a good look at the Book class we have arrived at. Can you see any problems
with it in general, and with the anyAuthor function (expressed in either way) in particular?
Time to try it out!
Before we go any further, if you have a copy of Perfect Developer already installed then we suggest
you try building and verifying the Book class as it now stands. Follow these instructions:
1. Start Perfect Developer.
2. From the File menu select New project. Browse to whatever directory you would like to store
the project in, enter a suitable project name (e.g. Books) and click Save.
3. From the Project menu select Create file. Enter Book as the filename, leave the other boxes
alone and click OK. Perfect Developer will create the file Book.pd, generate a skeleton for class
Book in it, and open the file in whatever editor you selected (under Windows this will be
Notepad if you have not yet used the Set Editor facility in the Options menu).
4. Paste the following text into the abstract section:
var title: string,
authors: set of string;
5. Paste the following text into the interface section, deleting the default constructor declaration
that is already there:
function title;
function hasAuthor(author: string): bool
^= author in authors;
opaque function anyAuthor: string
^= any authors;
build{!title: string, !authors: set of string};
build{!title: string, author: string}
post authors! = set of string{author};
6. Within the editor still, from the File menu select Save, then exit the editor (or simply switch to
the Perfect Developer window).
The Files window within Perfect Developer will now show a single file, namely Book.pd. At this stage
there are several things you can do; we suggest the following:

From the Project menu select Settings, then change whatever settings you may consider
appropriate (for example, in the Code Generation tab you may wish to change the output
language from C++ to Java).

From the Build menu select Build to generate code and watch the Results window. You can
also generate code for a single file by right-clicking on the filename in the Files window and
33
selecting Build. If any error messages are generated, double clicking on them will take you into
the editor (if the editor allows and the facility is configured correctly, it will take you to the
exact line and column of the error). Error messages should be self-explanatory, but if you can't
understand the message and you think the file looks ok, try retyping the offending line (in case a
strange character has been pasted across and isn't showing in the editor).

Once the file is building without errors, from the Build menu select Verify. You can also verify
a single file by right-clicking on the filename in the Files window and selecting Verify.
You should see one verification warning message in the Results window. Can you understand the
reason for it?
Class Invariants
If you tried out Perfect Developer on the Book class and you used the first definition we gave for
anyAuthor, you should have seen a message like the following when you tried to verify the project:
C:\...\Book.pd (19,8): Warning! Unable to prove: Operand of 'any' or 'that' is non-empty in context of
class Book [C:\...\Book.pd (7,7)], cannot prove: 0 < #self.authors (see C:\...\Book_unproven.htm#1).
If your editor supports a "goto specified row/column" function and you have configured Perfect
Developer accordingly, you can double-click on the error message in the Results window. The Perfect
source file to which the message relates will be opened in the editor and the cursor will be positioned at
the place in the source to which the message refers. Try it!
Do you see the problem? We have specified that the function anyAuthor should return any author in the
set ... but what if a Book object has no authors at all? Suppose we created a Book with the following
expression:
Book{"Anonymous poems", set of string{}}
There is nothing in our Book class declaration to prohibit this!
Let's assume that we decide to solve the anyAuthor problem by insisting that every book has at least
one author.
One way of doing this is to use a subtype declaration (remember those?) to declare a new type
representing a non-empty sequence of strings, then declare the authors variable to be of that type.
Another way is to add a class invariant, like this:
class Book ^=
abstract
var title: string,
authors: set of string;
invariant #authors ~= 0;
interface
function title;
34
function hasAuthor(author: string): bool
^= author in authors;
opaque function anyAuthor: string
^= any authors;
build{!title: string, !authors: set of string};
build{!title: string, author: string}
post authors! = set of string{author};
end;
We declare a class invariant using the keyword invariant followed by one or more (comma-separated)
Boolean expressions. The meaning is that each expression must be true for every instance of the class.
Recall that the unary "#" operator is predefined for the built-in collection classes to return the number
of elements in its operand. So the invariant expression reads "count of authors not-equal-to zero".
With this invariant added, the original verification error disappears because there will always be at least
one author in the list. But does that mean that the class is now verifiable? Try adding the invariant and
running Verify again to see!
Preconditions
If you tried verifying Book with the class invariant added, Perfect Developer will have generated a
warning something like this:
C:\...\Book.pd (23,3): Warning! Unable to prove: Class invariant satisfied (defined at C:\...\Book.pd
(12,22)), cannot prove: ~(#self'.authors = 0) (see C:\...\Book_unproven.htm#1).
By the way, if you right-click on the error message and select "Go to proof/unproven information", the
system will open a file containing more details of the failed proof attempt - assuming that you didn't
change the project settings to suppress this file. The information in this file may be helpful in
pinpointing the exact nature of the problem - sometimes it even includes a suggested fix!
The reason for this warning is that the class invariant says that an empty authors set isn't allowed.
Every constructor for the class must satisfy this invariant no matter what parameters it is called with.
However, the original constructor we declared initializes authors directly from the corresponding
parameter, but places no restriction on what values we may pass to it.
We can impose such a restriction by declaring a precondition for the constructor like this:
class Book ^=
abstract
var title: string,
authors: set of string;
invariant #authors ~= 0;
interface
function title;
35
function hasAuthor(author: string): bool
^= author in authors;
opaque function anyAuthor: string
^= any authors;
build{!title: string, !authors: set of string}
pre #authors ~= 0;
build{!title: string, author: string}
post authors! = set of string{author};
end;
The keyword pre is followed by one or more (comma-separated) Boolean expressions, representing
conditions that must be satisfied whenever the constructor is called. If the constructor has a
postcondition, the precondition must be inserted before the postcondition. Note that the semicolon in
the above example is not part of the precondition syntax; its job is to separate the entire constructor
declaration from any other declarations that follow it - so the constructor postcondition would follow
the precondition directly, without an intervening semicolon.
Try adding the precondition as illustrated above to your class declaration for Book and check that
verification now completes successfully. Also try adding the following, outside the class declaration:
const anotherBook ^= Book{"Anonymous Poems", set of string{}};
and see what happens when you attempt to verify the file.
It isn't only constructors that may have preconditions - functions may have them too. Function
preconditions may refer to the attributes (i.e. variables) of the class as well as to the parameters. A
function precondition must be declared before the ^= symbol or satisfy keyword. So, if for some reason
we decided to permit the hasAuthor query to be made only on those books with a title beginning with
the word "The" and a space, we could use the following:
function hasAuthor(author: string): bool
pre title.begins("The ")
^= author in authors;
When writing preconditions for functions, it is good style to avoid referring to attributes and member
functions which the caller cannot access. The hasAuthor function is an interface function and the only
member its precondition refers to is title - which we redeclared as an interface function - so this style
guideline is satisfied.
As an exercise, try adding class invariants and corresponding constructor preconditions to express the
following:



The title cannot be the empty string;
There are at most 5 authors to a book;
None of the authors may be the empty string.
Use Perfect Developer to verify your solution.
36
The toString method
There is just one function that is automatically defined for every class you declare. That function is
called toString and it takes no parameters. Its job is to return a textual representation of any object of
that class. However, the default version isn't very useful because it prints the same string regardless of
the values of the class attributes. If you intend to use toString at all, you will certainly want to
customize it for your class. We can customize toString for our Book class by placing the following
declaration in the interface section:
redefine function toString: string
^= "'" ++ title ++ "' by " ++ interleave(authors.permndec, " and ");
Recall that the operator ++ between sequences means concatenation (and strings are just sequences of
characters). The permndec member function of class set of X yields the members of the set as a
sequence in nondecreasing order. The global function interleave takes two arguments: a seq of seq of X
(e.g. a seq of seq of char, which is the same as a seq of string) and a seq of X (e.g. a seq of char, or
string). It concatenates the elements of the first parameter, inserting the second parameter between
each pair of elements. So in this case, it concatenates all the authors together, inserting the string "
and " between them.
Given a Book such as myFavoriteBook we can now use the expression myFavoriteBook.toString to get
a value that we can print or display to represent the book.
Sometimes you will want to provide a toString method that takes some sort of formatting parameter, so
that you can generate different textual representations of the same word. For example, you might want
to make our program support alternative output languages. You can provide alternative versions of
toString (or any other function) provided that the different versions take different numbers or different
types of parameters. Here's a version of toString that lets the caller pass words meaning "by" and "and"
in a chosen language:
function toString(byWord, andWord: string): string
^= "'" ++ title ++ "' " ++ byWord ++ " " ++ interleave(authors.permndec, " " ++ andWord ++ " ");
That's getting rather hard to read and understand, so here's an equivalent version that uses letdeclarations to break the complex expression into simpler parts:
function toString(byWord, andWord: string): string
^= ( let quotedTitle ^= "'" ++ title ++ "'";
let byWithSpaces ^= " " ++ byWord ++ " ";
let andWithSpaces ^= " " ++ andWord ++ " ";
quotedTitle ++ byWithSpaces ++ interleave(authors.permndec, andWithSpaces)
);
Now let's define the parameterless toString function in terms of this new one:
redefine function toString: string
^= toString("by", "and");
That's enough theorising; to try this out, let's look at how to write a simple test harness.
37
Creating main
You can create a main program within Perfect Developer like this. From the Project menu select
Create file. Select the Perfect main entry point option and then click OK (the filename Main.pd will
be automatically filled in for you).
Next, in the editor, change the import directive to read:
import "Book.pd";
Change the postcondition of the schema main to read:
post ( let thisBook ^= Book
{ "Perfect Developer Tutorial",
set of string{"David Crocker", "Judith Carlton"}
};
context!print(thisBook.toString ++ "\n")
),
ret! = 0;
Build and verify the project (which now has two files).
How you create an executable program now depends on whether you are using C++ or Java and on
what development environment you are using.
If you are using Java, we suggest you read our Knowledge Base article Creating a Java application
using Perfect Developer and JDK 1.4. The critical factors are:

Version 1.4 of Java is needed (this means that older versions of JBuilder are unsuitable).

You need to choose a package name (e.g. books) and set the package name in the Perfect
Developer project. For Perfect Developer version 2.0 you do this by adding the option gk=books in the Additional options field on the Miscellaneous tab of the project settings. For
later versions, you should instead enter the package name on the Code Generation tab.

On the Code Generation tab, set the Target language to Java. The Target Compiler setting
does not matter because Java is highly standardised.

Most Java development systems are very fussy about what directories the source files are placed
in. You should create subdirectories src and classes within your project directory. We also
suggest you create a subdirectory called output to hold the generated archive file. Assuming
your chosen package name is books then create a books subdirectory within src and another
within classes. Java source files for package books need to be placed in src/books, so on the
Code Generation tab of the project settings, change Output directory to Specified Directory
and browse to src/books.

Make sure that the Java CLASSPATH includes not only your Classes directory but also the
PerfectRuntime.jar file. The easiest way to do this is to copy PerfectRuntime.jar into your
output subdirectory and then include output/PerfectRuntime.jar in your CLASSPATH. If you
wish to use a debug version of the runtime library, use PerfectRuntimeD instead of
PerfectRuntime.

You need to create a Java source file containing a main class. We suggest that you adapt the file
Entry.java that we provide in the HelloWorld example project.

The HelloWorld project that we provide in the Examples subdirectory of the Perfect Developer
38
installation includes a script file postbuildjava.bat (Windows) or postbuildjava (Linux) that
automates some of the above.
If you are using C++ under Windows and your compiler is Microsoft Visual C++ 6.0, see Creating a
Perfect console application using Visual C++.
If you are using C++ with the GNU compiler gcc, do the following:

Ensure you are using gcc version 3.01 or later.

On the Code Generation tab of the Perfect Developer project settings, change Target
Compiler to gcc.

When compiling the generated files with gcc, use optimisation level 2 or lower unless
compiling with gcc version 3.31.

Include the Runtime/Include/Cpp directory of Perfect Developer in the gcc include-files path.

Link with the library Runtime/Lib/Cpp/PerfectRuntime.lib (or PerfectRuntimeD.lib for a debug
build).

If you want to automate the build process, you can configure a Post-build step command file
on the Build tab of the project settings to automatically invoke a program such as make.
When you have successfully built your C++ or Java project, running it from a command prompt should
produce the output:
'Perfect Developer Tutorial' by David Crocker and Judith Carlton
Now you can experiment by making changes to the Main.pd file. What happens if you break a
precondition (e.g. by creating a book with no authors)? How does the result depend on the Runtime
checks setting on the Code generation tab?
Schemas
In Perfect, a method that can change something is called a schema. A schema might be declared as
changing the current instance of the class of which it is a member, or one or more of its parameters, or
both.
Let's illustrate schemas by extending our Book class to include some information that is liable to
change after a Book has been created. We'll assume that a book might be available in any combination
of paperback, hardback and Braille editions. Here is a suitable enumeration class to represent these
editions:
class PublishedEdition ^= enum paperback, hardback, Braille end;
If you are trying this example out, place this declaration in the Book.pd file but outside the declaration
of class Book (or you could place it in its own file and then import that file into Book.pd).
Now let's include a variable in class Book to represent the editions in which the book is available. Each
book might be available in any combination of editions, or none at all (i.e. the book is out-of-print), so
we need to store a set of editions. Add the following variable declaration to the abstract data:
var editions: set of PublishedEdition;
(alternatively, just include the new declaration in the existing var declaration list). Now you will need
39
to extend the constructor postconditions to initialise the new variable - let's initialise to the empty set.
We will now add two new methods: a method to be called when a book is published in a particular
edition, and a method to be called when a particular edition goes out-of-print. Here are the two method
declarations:
schema !publish(v: PublishedEdition)
post editions! = editions.append(v);
schema !outOfPrint(v: PublishedEdition)
pre v in editions
post editions! = editions.remove(v);
This is what it means:




The exclamation-mark before each schema name indicates that the schemas may change the
current object. If the schema name is not preceded by an exclamation mark, this means that the
schema does not change the current object (those of you familiar with C++ may liken this to a
const method).
The absence of exclamation marks in the parameter lists indicates that the schemas do not
change their parameters (by now, you may be getting the idea that in Perfect, an exclamation
mark always means that the value of an associated entity changes).
Schemas may have preconditions, just like functions and constructors.
The schema postcondition describes the final state of objects modified by the schema, just as a
constructor postcondition describes the state of the constructed object. However, a schema
postcondition may depend on the initial value of the current object (whereas a constructor
postcondition can't depend on the initial value, because the object doesn't have a value until the
constructor completes).
We also need to initialise the new attribute editions in each of the constructors. This leaves our
complete Book file looking like this:
class PublishedEdition ^= enum paperback, hardback, Braille end;
class Book ^=
abstract
var title: string,
authors: set of string,
editions: set of PublishedEdition;
invariant #authors ~= 0;
interface
function title;
function hasAuthor(author: string): bool
^= author in authors;
opaque function anyAuthor: string
^= any authors;
function toString(byWord, andWord: string): string
^= ( let quotedTitle ^= "'" ++ title ++ "'";
40
let byWithSpaces ^= " " ++ byWord ++ " ";
let andWithSpaces ^= " " ++ andWord ++ " ";
quotedTitle ++ byWithSpaces ++ interleave(authors.permndec, andWithSpaces)
);
redefine function toString: string
^= toString("by", "and");
schema !publish(v: PublishedEdition)
post editions! = editions.append(v);
schema !outOfPrint(v: PublishedEdition)
pre v in editions
post editions! = editions.remove(v);
build{!title: string, !authors: set of string}
pre #authors ~= 0
post editions! = set of PublishedEdition {};
build{!title: string, author: string}
post authors! = set of string{author},
editions! = set of PublishedEdition {};
end;
Knowledge round-up quiz
Exercise
Modify the 'toString' method to include the set of available editions in the returned string, e.g.:


'Through the Looking Glass' by Lewis Carroll, available in
hardback and Braille
'Fly Fishing' by J R Hartley, out of print
Hint: the toString method is automatically defined for enumeration classes. Use the permndec
function to turn the set of editions into a sequence, i.e. editions.permndec yields the available
editions as a sequence.
Calling a schema
Class member schemas that do not modify the current object are called using the normal dot-notation;
however, if the schema modifies the current object (i.e. it was declared with a "!" before the name),
then the dot is replaced by an exclamation mark (this means that, at the point of call, you can tell that
the object is changing). So, given a variable myBook we can use the construct:
... myBook!publish(hardback@PublishedEdition) ...
to change the state of myBook such that it is recorded as available in paperback.
A schema call is a form of postcondition, so it may only appear where a postcondition is permitted.
Apart from following the keyword post in a constructor or schema declaration, one other place you can
41
use a postcondition is in an after expression. So if you have a value such as myFavoriteBook that you
can't (or don't want to) change but you nevertheless wish was available in hardback, the expression:
... myFavoriteBook after it!publish(hardback@PublishedEdition) ...
yields a copy of myFavoriteBook with the editions attribute suitably amended. The keyword after can
be preceded by any expression, so this:
... Book{"Alice in Wonderland", "Lewis Carroll"} after it!publish(hardback@PublishedEdition) ...
is equally valid. Note that the precedence of after is very low, so you frequently need to enclose after
expressions in brackets.
Schemas that modify their parameters
Schemas may be designed to modify their parameters instead of (or as well as) the current object. For
example, suppose we wish to change the outOfPrint schema so that as well as updating the set of
available editions, it also returns the updated set to the caller. Here is a suitable schema declaration:
schema !outOfPrint(v: PublishedEdition, editionsLeft!: out set of PublishedEdition)
pre v in editions
post editions! = editios.remove(v), editionsLeft! = editions';
The new parameter editionsLeft is declared with an exclamation mark after its name to indicate that the
schema can modify it. Furthermore, in this example the keyword out indicates that it is only used for
passing a value out of the schema, so its initial value is unimportant (indeed, the caller does not need to
initialize it).
We have followed the post keyword by two postconditions separated by a comma. The meaning is that
both postconditions are to be satisfied, but in no particular order. However, the prime after editions in
the second postcondition means we want the final value of editions at that point; but the final value of
editions is defined by the first postcondition, so in this case they will be satisfied in order.
When calling a schema that modifies its parameters, the corresponding actual parameters must be
followed by an exclamation mark (thereby indicating at the point of call that the parameter is changed).
For example:
... myBook!outOfPrint(hardback@PublishedEdition, left!) ...
where myFavoriteBook is a variable (or modifiable parameter) of type Book and left is a variable (or
modifiable parameter) of type set of PublishedEdition.
It isn't often necessary to use a schema that changes more than one object in an after expression, but
where necessary it can be done. Since the postcondition that follows after is only allowed to change it,
the expression you use in front of after needs to be a class with multiple modifiable components. If the
schema modifies two objects, the library class pair of (X, Y) is convenient for this purpose:
... ( let temp ^= pair of (Book, set of PublishedEdition)
{myFavoriteBook, set of PublishedEdition{}};
temp after it.x!publish(hardback@PublishedEdition, it.y!)
)
42
...
or, more succinct (but possibly less readable):
... pair of (Book, set of PublishedEdition)
{myFavoriteBook, set of PublishedEdition{}}
after it.x!publish(hardback@PublishedEdition, it.y!)
...
Either way, the construct is an expression of type pair of (Book, set of PublishedEdition) whose x
component represents the updated Book object and whose y component is the set of remaining editions.
Postconditions
You've seen how we use postconditions to define the objects created by constructors, to define the
changes made by a schema, and in after expressions to express the value that some expression would
have if we made some changes to it. It's now time to cover postconditions in detail.
In Perfect, every postcondition precisely defines two things:


A frame, which is a set of variables (or parts of variables) that may be changed; and
A condition to be satisfied.
The most basic form of postcondition allows you to specify these two elements separately, like this:
change frame satisfy condition
for example:
change x, array[i] satisfy x' = array[i] & array'[i] = x
expresses that the values x and array[i] are swapped and nothing else changes. In the condition, we
use a prime to denote the final value of a variable (unprimed variables refer to the initial value). For
each variable in the change list, that variable (or some variable containing it) must occur primed at
least once in the satisfy expression. Incidentally, array[i]' means exactly the same as array'[i] and so
does self '.array[i] if array is an attribute of the current class.
It's very common to want to change a single variable by making its value equal to some expression; so
we have a shorthand for this form. Specifically, any postcondition of the form:
change var satisfy var' = expr
can be abbreviated:
var! = expr
which is the sort of postcondition we've mainly used so far. Note that if you would need to bracket expr
in the expression var' = (expr) - such as when expr is a forall, exists, for...yield or after expression, or
expr contains a Boolean operator - then you also need to bracket expr in the postcondition var! =
(expr).
A variation of the above is the postcondition
change var satisfy var' = var operator expr
where operator is any binary operator. This may be abbreviated to:
var! operator expr
43
So the postcondition
change array[index] satisfy array'[index] = array[index] + count
may be abbreviated to either
array[index]! = array[index] + count
or to
array[index]! + count
More Postconditions
To recap, we've just covered three sorts of postconditions:



change frame satisfy condition
var! = expr
var! op expr
Now to look at two more sorts of postcondition:


schema calls
pass
The call:
myBook!outOfPrint(hardback@PublishedEdition, left!)
means satisfy the postcondition of the schema outOfPrint, substituting myBook for self,
hardback@PublishedEdition for the parameter v, and left for the parameter editionsLeft.
For purists: the frame of a schema call postcondition is basically the variables that are followed by
exclamation marks (including the implicit self if self!s is abbreviated to !s), but may be narrowed to
just parts of those variables (since the frame of the schema postcondition may be confined to just some
parts of the variables that the schema is allowed to modify). The condition of a schema call
postcondition comprises the condition of the postcondition of the schema (with appropriate parameter
substitutions). For example, the above schema call is equivalent to
change myBook.editions, left
satisfy myBook'.editions = myBook. editions.remove(hardback@PublishedEdition)
& left' = myBook'. editions
(except that the expanded version would be illegal unless the editions attribute of class Book were
declared publicly writable). If the above sounds a little complicated, don't worry - expanding schema
calls into frames and conditions is a job for Perfect Developer : you needn't do it!
The other simple form of postcondition pass has an empty frame and satisfies the condition true. In
other words, pass represents "no change".
We've now covered five types of simple postcondition, namely:




change frame satisfy condition
var! = expr
var! op expr
schema call
44

pass
'forall' Postconditions
Sometimes it's useful to apply a postcondition to all elements of a collection. Let's suppose we want to
add 1 to all elements of a sequence of integers called array. We can do this using a forall
postcondition, like this:
forall i::0..<#array :- array[i]! + 1
This is very much like a forall expression, except that the ":-" symbol is followed by a postcondition
instead of an expression. The meaning is that the bound variables (the single variable i in this case) take
all values in the bound (all the indices of array in this case) and the postcondition following ":-" is
satisfied for all those values. So the above is equivalent to combining the postconditions:
array[0]! + 1, array[1]! + 1, array[2]! + 1, ... , array[<#array]! + 1
Notionally, all the "branches" are satisfied in parallel. It is required that the postcondition following ":-"
has a distinct frame for each value of the bound variable (or each combination of the values of the
bound variables, if there is more than one). This means that you can't use a postcondition such as:
forall i::0..<#array :- total! + array[i]
because total is part of the frame irrespective of the value of i. In practice this means that forall
postconditions are almost always used to update sequences.
Combining Postconditions
There are four ways of combining postconditions:




Combining with "&"
Combining with then
Bracketing without guards
Bracketing with guards
To combine postconditions with "&", first put brackets around any change...satisfy or forall
postconditions that you wish to combine; then write "&" between the postconditions. For example, the
following swaps the values of variables x and y:
x! = y & y! = x
Postconditions combined with "&" are satisfied in parallel, but where you prime a variable this refers to
the final value of a variable in the entire combined postcondition, so you can make one of the
component postconditions depend on a final value that is defined by another. So the postcondition:
y! = e & z! = y'
sets both y and z to the value e, as does the postcondition:
z! = y' & y! = e
since the order of postconditions combined using "&" is immaterial to the meaning.
The frames of postconditions combined with "&" must be disjoint, so:
45
x! = y & x! = y
is illegal, and:
array[i]! = y & array[j]! = x
is valid subject to i ~= j.
Combining Postconditions Sequentially
To combine postconditions sequentially, just write the keyword then between them; for example:
y! = e then z! = y
has the same meaning as
y! = e & z! = y'
In postcondition elements combined with then, a primed value refers to the final value in that
particular element, not in the entire postcondition. So in the following:
z! = e & x! = z' then z! + 1
the final value of x is equal to e, not e + 1.
It is good practice to avoid using then if the desired state change can be expressed in another way. A
specification should express what a method achieves, not how it achieves it. The main reason for using
then is to combine sequentially a schema call postcondition with another postcondition that affects the
same variable(s). In this situation, avoiding use of then would involve replacing the schema call with
the postcondition of the schema.
Combining postconditions with "&" has higher precedence than combining with then, so:
v! = e & w! = f then w!t2 & v!s2
means the same as:
(v! = e & w! = f) then (w!t2 & v!s2)
When you combine postconditions using then, the resulting postcondition cannot be satisfied in
parallel with any other postcondition. This means that you cannot use a then-combined postcondition in
a list of more than one postcondition, nor can you use "&" to combine it with any other postcondition.
For example, the following are not permitted:
(v! = e then & v!s2) & (w! = f then w!t2) // illegal use of "&" above "then"
v! = e then & v!s2, w! = f then w!t2
// illegal use of "then" in a postcondition list
Bracketed Postconditions
Just as in expressions, brackets can be used with postconditions to override the usual precedence rules.
For example, in:
(forall i::1..<#s :- s[i]! + 1) & s[0]! = 0
brackets are used around the 'forall' postcondition in order to allow it to be combined with another
postcondition using "&".
46
However, brackets can do far more than that. First of all, a list of postconditions may be used in place
of a single postcondition by bracketing. For example, the postcondition:
(forall i::1..<#s :- s[i]! + 1, s[0]! = 0)
is equivalent to the one given earlier on this page, because postconditions in a list are satisfied in
parallel (just as if they are combined using "&").
Second, between the opening bracket and the start of the postcondition list, it is permitted to place letdeclarations and assertions (just as in bracketed expressions). It is also possible to declare variables
here. This is especially useful when calling a schema with a out parameter, since a local variable
declaration can be used to capture the returned value; for example:
(var temp: int; !doSomething(temp!) then x! = f(temp))
where schema doSomething modifies the current object and also returns a value in its out parameter.
Lastly, brackets are used to build conditional postconditions in much the same way as conditional
expressions are constructed, as we shall see next.
Conditional Postconditions
Conditional postconditions are constructed in an analogous way to conditional expressions; that is, a
conditional postcondition is constructed by enclosing two or more guarded postconditions in brackets.
To illustrate this, let's return to our Book example. Suppose we wish to associate a list price with each
version in which a book is published. Leaving aside for the moment how we store this data, let's
assume we want a schema to adjust the price of a particular published version by a given percentage.
We could make it a precondition of the schema that the specified version is included in the published
editions; but instead, let's define the schema to ignore the call if the specified version is not available.
This is what such a schema might look like:
schema !adjustPrice(ver: PublishedEdition, percent: int in (-99..99))
post ( [ver in editions]:
?, // postcondition to change price of version 'ver' goes here
[]:
pass
);
As in a conditional expression, the empty guard "[]:" means "else". In fact, the combination "[]: pass"
is required so often that Perfect has a special shorthand for it which is to omit the colon and pass
keyword, like this:
schema !adjustPrice(ver: PublishedEdition, percent: int in (-99..99))
post ( [ver in editions]:
?, // postcondition to change price of version 'ver' goes here
[]
);
The first guard may be preceded by let-declarations, assertions and variable declarations in the usual
way, e.g.:
schema !adjustPrice(ver: PublishedEdition, percent: int in (-99..99))
post ( let gotVersion ^= ver in editions;
47
[gotVersion]:
?, // postcondition to change price of version 'ver' goes here
[]
);
Mappings
Currently, each Book object holds a set of PublishedEdition indicating what editions are available
(paperback, hardback etc.). Our task is to associate a price with each version. Let's assume we will
define a class Money to represents monetary amounts.
To relate prices to editions, one possibility is to define a new class VersionAndPrice to hold both a
PublishedEdition and a Money object, then our editions attribute can hold a set of values of this class.
However, since each version has exactly one price, we would need an invariant or type constraint to
restrict editions to contain no more than one VersionAndPrice value for any PublishedEdition, like this:
var editions: set of EditionsAndPrice;
invariant forall x, y::editions :- x.ver = y.ver ==> x = y;
(where the ver attribute of EditionsAndPrice is assumed to be the PublishedEdition).
If we wished to avoid declaring a new class solely to store a version and a price, we could, at some loss
of readability, use the predefined template class pair instead, like this:
var editions: set of pair of (PublishedEdition, Money);
invariant forall p, q::editions :- p.x = q.x ==> p = q;
(the first component of a pair is called x so in this case it is the PublishedEdition part).
A simpler way of representing this data is to use the "map of (X -> Y)" class. This represents a set of
values of type X, with a value of type Y associated with each value in the set. So the following
declaration:
var editions: map of (PublishedEdition -> Money);
captures precisely what we need. Note that each PublishedEdition has exactly one associated Money
value (i.e. one price), but there is nothing to stop several editions having the same price.
If we define editions like this, we can use it in the following ways. First, if we want to determine all the
available published editions in the mapping, we can do so using the dom method (short for "domain"),
like this:
function allEditions: set of PublishedEdition
^= editions.dom;
Likewise, we can obtain all the prices using the ran method (short for "range"), e.g.:
function allPrices: set of Money
^= editions.ran;
To test if a particular PublishedEdition called ver is in the mapping, we could use the expression ver
in editions.dom but the expression ver in editions means exactly the same thing, and it
is shorter.
To lookup the price of a PublishedEdition called ver that is known to be in the mapping, we can use the
expression editions[ver], which is rather like indexing into a sequence. In fact, a sequence can be
48
considered as a particular type of mapping in which the domain comprises the set of integers from zero
to one less than the length of the sequence.
We can also use indexing to modify an element of a mapping, as in the postcondition
editions[ver]! = newPrice.
Armed with this, we can now complete the specification of the adjustPrice schema:
schema !adjustPrice(ver: PublishedEdition, percent: int in (-99..99))
post ( [ver in editions]:
version[ver]! = (version[ver] * (100 + percent))/100,
[]
);
assuming that class Money provides operators for multiplying and dividing by integers. We'll look at
how to do this next.
Operator Declarations
Let's consider how we might declare the Money class, and in particular how to define multiplication
and division by integers for it. We'll assume prices are expressed in whole dollars and cents (feel free to
substitute your own national currency here). There's no point in storing the dollars and cents separately;
we may as well just store the price in cents. So here is how our class starts out:
class Money ^=
abstract
var amount: nat;
build{!amount: nat};
interface
build{dollars: nat, cents: nat}
pre cents < 100
^= Money{100 * dollars + cents};
end;
We will deliberately not make amount publicly readable (hence no redeclaration as an interface
function); instead we will provide all the required functionality.
We have declared a constructor that builds a Money object directly from an amount in cents; but since
this is declared in the abstract section, it is not accessible outside the class. We have also declared an
interface constructor that takes separate values for the dollars and cents. Instead of defining this in the
usual way with a postcondition, we have used the "^=" symbol to define the result in terms of our
abstract constructor.
Let's now redefine the toString method to make monetary amounts printable:
class Money ^=
abstract
var amount: nat;
build{!amount: nat};
49
interface
build{dollars: nat, cents: nat}
pre cents < 100
^= Money{100 * dollars + cents};
redefine function toString: string
^= "$"
++ (amount/100).toString
++ "."
cents
leading zero
// currency indicator
// number of dollars
// separate dollars and
++ ( let cents ^= (amount % 100).toString;
[#cents = 1]:
cents.prepend(`0`),
// 1 digit so add a
alone
[#cents = 2]:
cents
// 2 digits so leave
);
end;
Now let's add a method for adding two amounts of Money together. We could declare a function add for
this purpose:
function add(other: Money): Money
^= Money{amount + other.amount};
However, it is much more natural to use the "+" operator to denote addition. We can declare a "+"
operator for class Money using the following declaration:
class Money ^=
abstract
var amount: nat;
build{!amount: nat};
interface
build{dollars: nat, cents: nat}
pre cents < 100
^= Money{100 * dollars + cents};
redefine function toString: string
^= "$"
// currency indicator
++ (amount/100).toString
// number of dollars
++ "."
// separate dollars and cents
++ ( let cents ^= (amount % 100).toString;
[#cents = 1]:
cents.prepend(`0`),
// 1 digit so add a leading zero
[#cents = 2]:
cents
// 2 digits so leave alone
);
operator +(other: Money): Money
^= Money{amount + other.amount};
end;
50
Because we have declared a parameter, this is a declaration of a binary "+" operator (we would declare
a unary "+" operator by omitting the parameter). When "+" is used between two expressions of type
Money, the operator will be invoked with the left-hand operand taking the role of self and the righthand operand taking the role of other.
Now let's add another operator to the interface section, this time to multiply the current Money object
by a nonnegative integer:
operator *(other: nat): Money
^= Money{amount * other};
This operator allows us to write expressions like price * 2, where price is of type Money. But what
if we want to allow 2 * price as well? Class int does not include an operator "+" taking a
parameter of type Money and no mechanism is provided for adding one. However, we can add an
inverted binary operator declaration to class Money like this:
operator (other: nat)*: Money
^= Money{amount * other};
By declaring the parameter before the operator symbol "*" instead of after it, we signify that this
operator declaration matches a use of "*" with a left-hand operand of type nat and a right-hand operand
of type Money.
All that is left is to add an operator to divide a sum of money by an integer, rounding up:
operator /(other: nat): Money
pre other ~= 0
^= Money{(amount + other - 1) / other};
Note the precondition to ensure that a sum of money cannot be divided by zero.
Declaring Comparison Operators
A special sort of operator declaration is a declaration of the comparison operator. This operator is
named "~~" and takes an operand whose type is implicitly the type of the class in which it is declared
(so the type is not given explicitly). Likewise, the result is always of type rank so the result type is
never declared. Here is the declaration of a comparison operator for class Money:
total operator ~~ (other)
^= amount ~~ other.amount;
Here we are making use of the fact that "~~" is already defined for class int (as it is for the other builtin classes). The reason for the total keyword is that we wish to declare that this operator declaration
defines a total ordering on Money objects. In other words, if two Money objects are not equal, one of
them must be greater than the other.
When might we not want to define a total ordering? When we have no particular reason to compare all
the attributes of the objects concerned! For example, support we wish to define a comparison operator
on class Book such that we can sort a sequence of books into title order. In the event of two Book
objects having the same title but different authors, published editions or prices, we don't care which
comes first. So we will only compare titles, giving us the following operator declaration in class Book:
operator ~~ (other)
^= title ~~ other.title;
51
Since two books with the same title but different authors will be unequal but compare 'same', this
operator does not define a total ordering; hence we do not use the total keyword.
What if we do want to define a total ordering for class Book? Let's say that when the titles compare
same, we will order books by the author list. We can achieve this using the following definition:
operator ~~ (other)
^= ( let titleCompare ^= title ~~ other.title;
[titleCompare = same@rank]:
authors ~~ other.authors,
[]:
titleCompare
);
However, this is still not a total ordering, because we could have two Book objects with the same title
and authors but different published editions or prices. Extending the declaration to provide a total
ordering is left as an exercise for you.
Once the "~~" operator has been declared in a class, as well as using the "~~" operator between objects
of that class, you can also use the binary ">" (greater-than) and "<" (less-than) operators and their
negated editions.
For example, the expression myBook < yourBookis equivalent to (myBook ~~ yourBook) =
below@rank.
If the operator is declared total, you can use ">=" and "<=" as well.
For example, myMoney <= yourMoney is equivalent to (myMoney ~~ yourMoney) ~= above@rank.
Verifying Specifications
So far we've focused on how to structure a class and how to write method specifications. We've
introduced you to the principle of using preconditions to specify what needs to hold when a method is
called, and we've show you how to use the verification facility of Perfect Developer to make sure that
the specification doesn't involve for something untoward such as dividing by zero or indexing off the
end of a sequence.
This means that if you have written a program specification entirely in Perfect and verified it without
errors, you can be fairly sure that if you go on to generate code and run the program, it won't crash. We
can't absolutely promise that it won't crash since we don't have any control over such things as
compilers, linkers and other tools involved in building the executable program; also, you may need to
avoid a few situations listed on the website here for which verification is currently incomplete.
But a crash-proof program isn't much good if it doesn't actually do what it is supposed to. For example,
what if we made a mistake in specifying the "+" operator in class Money? If we're writing this class for
a banking or accountancy application and we make a mistake, then either our client or its customers
(depending on the nature of the error) are likely to be very unhappy!
So how do we ensure that we got the specification right? Traditional techniques for checking
specifications include peer review and testing. Of course, testing doesn't check the specifications as
such; it checks whether code that purports to implement the specifications behaves - for the test data 52
in accordance with those specifications.
With Perfect Developer you can verify specifications against expected behavior. If you are able to
express all the known user requirements as expected behavior and successfully verify the specifications
against it, you will have gone a long way towards showing that the specification meets the
requirements.
Verifying specifications against expected behavior is better than testing in the following respects:



instead of testing with a finite set of test data, you can verify against an infinite (or very large)
range of possible data;
you can verify a specification that hasn't been implemented yet, so it can be done much earlier
in the development process;
you don't have to construct a test harness - you just express the expected behavior along with
the specifications;
but does have one disadvantage:

verifying a specification does not detect errors in the other steps (design, implement, compile,
link etc.). The design and implementation steps can be verified by Perfect Developer, but you
will still need testing to verify that the compile and link steps have not introduced errors (at
least, until compilers and linkers are developed using Perfect Developer!).
Software development practice has shown that the earlier an error is introduced and the later it is
detected, the more expensive it is to rectify. This means that a specification error that is not detected
until testing (which is only possible when all the other steps have been completed) is a very expensive
error indeed! It's much better to verify the specification early on.
To verify specifications against expected behavior using Perfect Developer:
1. Declare the expected behavior using post-assertions and property declarations;
2. Run 'Verify'.
The post-assertions and property declarations are placed in the Perfect source alongside the
specifications, so you don't need to take any special measures to ensure that declaration of expected
behavior remains part of the project.
When should you use post-assertions, and when should you use property declarations? As a general
guide:


Behavior that is associated primarily with calls to one particular method should be declared as a
post-assertion attached to that method;
Other sorts of expected behavior should be declared as a property of the class concerned (or a
global property if it doesn't fit in any one class).
Post-Assertions
A post-assertion is an assertion given at the end of a method (i.e. after the result or postcondition) that
expresses one or more conditions that should be true when the method completes. Each condition
should have something to say about the return value (for a function or operator) or the final values of
modified objects (for a schema).
For example: in class Money we declared a division-by-integer operator that is supposed to round up
the result. If our specification really does round up, we expect that as long we start with a nonzero
53
amount, we end up with a nonzero result. We can express this expectation in a post-assertion like this:
operator /(other: nat): Money
pre other ~= 0
^= Money{(amount + other - 1) / other}
assert amount ~= 0 ==> result.amount ~= 0;
By way of another example: multiplying a Money object by one should give a result equal to the
original:
operator *(other: nat): Money
^= Money{amount * other}
assert other = 1 ==> result = self;
As a final example: calling the adjustPrice schema should not change what editions are available,
neither should it affect the price of any version except the one specified. We can verify this by adding
post-assertions:
schema !adjustPrice(ver: PublishedEdition, percent: int in (-99..99))
post ( let gotVersion ^= ver in editions;
[gotVersion]:
version[ver]! = (version[ver] * (100 + percent))/100,
[]
)
assert editions'.dom = editions.dom,
forall v::editions.dom :- v ~= ver ==> editions'[v] = editions[v];
Expressing Behaviours as Properties
Where an expected behaviour is intimately concerned with more than one method call, then instead of
expressing it as a post-assertion attached to one of the methods, it is better to express it in a property
declaration.
Let's look at another expected behaviour of operators in the Money class. We declared operators for
adding another Money object and for multiplying and dividing by integers. As a check on the
correctness of our specifications, let's declare our expectation that adding a Money object to itself is
equivalent to multiplying it by two. We could declare this as a post-assertion on the declaration of "+"
like this:
operator +(other: Money): Money
^= Money{amount + other.amount}
assert other = self ==> result = self * 2;
Or we could instead attach a post-assertion to the declaration of "*" like this:
operator *(other: nat): Money
^= Money{amount * other}
assert other = 2 ==> result = self + self;
However, this behavior involves both operators to a similar degree, so rather than use a post-assertion,
it is more appropriate to declare a property within the interface section of class Money like this:
property
assert self * 2 = self + self;
54
Not only is this more symmetrical in its treatment of the "+" and "*" operators, it is also simpler to
express.
Let's take another example. If we multiply a Money object by an integer, then divide the result by the
same integer, we expect to get back to our original value. We can express this as follows:
property (n: nat)
pre n ~= 0
assert (self * n)/n = self;
By declaring a parameter n of type nat, we are stating that the behavior should be true for all values of
n (as well as for all legal values of self). However, we must exclude zero, since the precondition of our
"/" operator requires that its second operand is nonzero; hence we give the property a precondition.
Properties don't have to be declared within classes - you can declare global properties too (of course, in
a global property there is no self object to refer to). Here is a global property that states that our two
different "*" operators behave like each other with the operands reversed:
property (p: Money, n: nat)
assert p * n = n * p;
This could equally well be stated as a class member property thus:
property (n: nat)
assert self * n = n * self;
55
Download