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