COP 3223H meeting -*- Outline -*- * Hoare-style specifications of programs and statements Tony Hoare, a Turing-award winning Computer Science wrote a paper: "An Axiomatic Basis for Computer Programming" in CACM, 12(10), Oct. 1969 http://doi.acm.org/10.1145/363235.363259 ------------------------------------------ WHY SPECIFY PROGRAMS USING MATH? Usually programs have So ------------------------------------------ ... an infinite number of inputs and an infinite number of outputs ... tests only can give a sample implementers must generalize Q: What's wrong with asking implementers to generalize from a pattern? possibility of miscommunication Some examples from https://epicmath.org/2013/02/13/7-very-misleading-sequences/ e.g., 1, 2, 4, 8, 16, ____ the next number is 31 if we are thinking of how many pieces that number of distinct points divides up the area of a circle into the next number is 57... 1, 1, 1, 1, ____ the next number is 2 if we are reading from Pascal's triangle moral: your notion of induction and an implementer's may be different ** assertions ------------------------------------------ ASSERTIONS An assertion, such as assert n > 2 is equivalent to if not (n > 2): raise AssertionError() ------------------------------------------ The raise statement causes an exception to happen, which usually leads to the program aborting. Q: What happens if the expression is true (e.g., if n > 2)? nothing, it's equivalent to evaluating that expression (like pass) *** assertions and program states ------------------------------------------ STATES OF A PROGRAM def: a *state* of a program consists of an environment (context) and a heap Notation: a state sigma can be written as a pair of functions (env, heap) env: Variable -> Value heap: Location -> Value Value = Number + Boolean + Location + ... Location = {(o,f) | o is an object, f is a field name} ------------------------------------------ ...that maps variables to values (this includes globals, like stdin, stdout) ...that maps objects (fields) to values Note that values may include objects (i.e., references to them) ------------------------------------------ ASSERTIONS CHARACTERIZE SETS OF STATES Boolean assertions (expressions) describe states in which they are True assert n > 2 Which states are passed by this assertion? In general assertions can be used to describe sets of states assert isinstance(n, int) and n > 2 assert op == "and" or op == "or" assert p != None ------------------------------------------ ... { (env,heap) | (env,heap) in States, env(n) > 2} in which env(n) > 2 means that n is defined in env (assigned) and that the value is a number > 2. ** pre- and postcondition specifications ------------------------------------------ PRECONDITIONS AND POSTCONDITIONS def: a *precondition* is an assertion that is supposed to be true def: a *postcondition* is an assertion that should be true Partial correctness: postcondition need only be true if the action finishes normally Total correctness: postcondition must be true whenever the action is started in a state satisfying the precondition Example: assert n > 1 # precondition n = n+1 assert n > 2 # postcondition assert isinstance(num, int) s = square(num) assert s == num**2 assert isinstance(gallons, int) ti = teaspoonsIn(gallons) assert math.isclose(ti, 128 * 6 * gallons) ------------------------------------------ ... before some action (statement) is to be run ... after some action (sttement) is run We will think of total correctness, as that requires more stringent preconditions ------------------------------------------ PRE- AND POSTCONDITION SPECIFICATIONS ARE MORE EXACTING THAN TESTS compare assert isinstance(gallons, int) ti = teaspoonsIn(gallons) assert math.isclose(ti, 128 * 6 * gallons) to the tests: from math import isclose def test_teaspoonsIn(): assert isclose(teaspoonsIn(1/128), 6.0) assert isclose(teaspoonsIn(1.0), 768.0) assert isclose(teaspoonsIn(0.25), 192.0) assert isclose(teaspoonsIn(0.5), 384.0) assert isclose(teaspoonsIn(10.0), 7680.0) ------------------------------------------ Q: What are the differences between the tests and a pre- and postcondition specification? The tests are executable, the specification is more abstract The specification is more general, you have to infer that from tests *** Design by Contract Idea popularized by Bertrand Meyer paper "Applying `Design by Contract'", IEEE Computer, 25(10), Oct, 1992. ------------------------------------------ CONTRACTS Imagine buying a car: dealer: benefit: $5000 (sale price) obligation: provide the car buyer: obligation: provide the $5000 benefit: the car Each side has some benefits and obligations ------------------------------------------ The dealer is like an implementation, the benefit is the precondition the obligation is the postcondition. For the buyer, the timing and roles are dual: the obligation (precondition) must be satisfied first then get the postcondition ------------------------------------------ FUNCTION/PROCEDURE CONTRACTS IN SOFTWARE implementation: benefit: assume the precondition obligation: guarantee the postcondition caller: obligation: guarantee the precondition benefit: assume the postcondition Example: square root function: benefit: assume argument is >= 0 obligation: guarantee result >= 0 and result**2 is close to argument def sqrt(arg): assert arg >= 0 # assume precondition # compute res # establish postcondition assert res >= 0 and isclose(result**2, arg) return res caller: obligation: guarantee argument is >= 0 benefit: assume result >= 0 and result**2 is close to argument assert arg >= 0 # establish precondition res = sqrt(arg) # assume postcondition assert res >= 0 and isclose(result**2, arg) ------------------------------------------ ------------------------------------------ CONVENTIONS FOR PRE- AND POSTCONDITIONS IN DOCUMENTATION STRINGS Preconditions: use "Requires P" to say that P is a precondition Postconditions: in functions: use "ensures Q(...result...)" to say that Q is a postcondition in procedures: use "effect Q(...old(arg)...)" to say that Q is a postcondition that describes changes, where old(arg) describes Adapting the syntax of Eiffel to Python: def sqrt(arg): """Requires arg >= 0 and ensures result >= 0 and isclose(result**2, arg)""" # result = ...some computation... return result ------------------------------------------ ... that describes the result of a call ... the starting value of arg The conventions are based on Liskov and Guttag's book "Abstraction and Specification in Program Development" (MIT Press, 1986) (There is a Python proposal (PEP) for this, which uses "pre:", "post:", "inv:", and "__return__" (for res).) *** examples of design by contract ------------------------------------------ ABSTRACTING FROM TEST CASES def test_aroundsun(): assert isclose(aroundsun(1), 2 * pi * AU) assert isclose(aroundsun(18), 36 * pi * AU) assert isclose(aroundsun(19), 38 * pi * AU) assert isclose(aroundsun(20), 40 * pi * AU) assert isclose(aroundsun(30), 60 * pi * AU) assert isclose(aroundsun(40), 80 * pi * AU) assert isclose(aroundsun(50), 100 * pi * AU) # oldest human lived 122 years and 164 days assert isclose(aroundsun(122.449315), 244.89863 * pi * AU) # some turtles and a clam have lived 500 years assert isclose(aroundsun(500), 1000 * pi * AU) # a bristlecone pine is thought to be about 5000 years old assert isclose(aroundsun(5000), 10000 * pi * AU) What would be an appropriate specification for aroundsun(earthyears)? Requires Ensures ------------------------------------------ ... earthyears isinstance(earthyears, numbers.Number) and earthyears >= 0 ... math.isclose(result, earthyears * 2 * math.pi * AU) ------------------------------------------ ANOTHER EXAMPLE def test_average3(): assert isclose(average3(2, 4, 6), 4) assert isclose(average3(50.1, 50.3, 50.2), 50.2) assert isclose(average3(27.0, 2.0, 1.0), 10.0) assert isclose(average3(50.342, 0.0, -50.342), 0.0) # some numbers from the Dow Jones Industrials follow assert isclose(average3(19897.67, 19950.78, 19888.61), 19912.353333333333) assert isclose(average3(15998.08, 17949.37, 19945.04), 17964.163333333333) What's a good specification for average3(n1, n2, n3)? Requires Ensures ------------------------------------------ ... isnumber(n1) and isnumber(n2) and isnumber(n3) where isnumber(n) == isinstance(n, numbers.Number) ... result is the average of n1, n2, and n3 or isclose(result, (n1+n2+n3)/3) ------------------------------------------ ANOTHER EXAMPLE def test_teaspoonsIn(): assert isclose(teaspoonsIn(1/128), 6.0) assert isclose(teaspoonsIn(1.0), 768.0) assert isclose(teaspoonsIn(0.25), 192.0) assert isclose(teaspoonsIn(0.5), 384.0) assert isclose(teaspoonsIn(10.0), 7680.0) What would be a good specification for teaspoonsIn(gallons)? Requires Ensures ------------------------------------------ ... gallons >= 0 ... isclose(result, gallons * 128 * 6) ------------------------------------------ ANOTHER EXAMPLE Sample interaction: 1st price? 99.99 2nd price? 100.00 3rd price? 300.00 Sales tax: $30.00 Total cost: $529.99 What would be a good specification for sales_total()? Requires Effect ------------------------------------------ ... after each of three prompts (on stdout) stdin contains a number, so these can be parsed as floats p1, p2, and p3. ... after receiving numbers for each of the three prompts (on stdout) the characters "Sales tax: $" and a number that is 6% of the sum p1+p2+p3 (rounded to the nearest cent), followed by a newline, followed by the characters "Total cost: $", followed by a number that is p1+p2+p3*1.06 (rounded to the nearest cent). ------------------------------------------ ANOTHER EXAMPLE Example: number? 5 number? 13 number? 27 number? 42 Yes, these are in strictly ascending order Example: number? 5 number? 13 number? 13 number? 42 No, these are not in strictly ascending order What would be a good specification for the procedure isascending()? Requires Effect ------------------------------------------ ... after each of 4 prompts of the form "number? " (on stdout) stdin contains an integer which can be parsed as ints n1, n2, n3, and n4 ... after receiving the numbers n1, n2, n3, and n4 from stdin in response to the prompts printed on stdout, stdout contains "Yes, these are in strictly ascending order" if n1 < n2, n2 < n3, and n3 < n4, and otherwise stdout contains "No, these are not in strictly ascending order" one conclusion is that such pre-post specifications lend themselves more to small functions/procedures than to entire programs (especially ones with graphics) ** Axioms for statements ------------------------------------------ DEFINEDNESS OF EXPRESSIONS For an expression E defined(E) means that the evaluation of E will not result in an error/exception or non-termination E.g., defined(n/d) == (d != 0) ------------------------------------------ defined depends on the details of an expression's semantics *** primitive statements ------------------------------------------ AXIOM SCHEMAS FOR STATEMENTS IN PYTHON A way to describe the semantics of statements, (total correctness interpretation) Pass: assert P pass assert P Assignment: assert ------------------------------------------ ... defined(E) and P(E) v = E assert P(v) ------------------------------------------ FUNCTION AND PROCEDURE CALLS Function call: Suppose f(arg1,...,argn) has specification requires P(arg1,...,argn) ensures Q(result,arg1,...,argn) assert Procedure call: Suppose p(arg1,...,argn has specifications requires P(arg1,...,argn) ensures Q(arg1,...,argn,old(arg1),...,old(argn)) assert ------------------------------------------ ... defined(E1) and ... and defined(En) and P(E1,...,En) res = f(E1,...,En) assert Q(res,E1,...,En) ... defined(E1) and ... and defined(En) and P(E1,...,En) oa1 = E1 # assume oa1,...,oan are fresh ... oan = En p(oa1,...,oan) assert Q(E1,...,En,oa1,...,oan) ------------------------------------------ ASSERT STATEMENTS An assert statement is similar to pass when the condition is true: assert E assert E assert E ------------------------------------------ Q: Is assert E1 and E2 the same as executing assert E1 followed by assert E2? yes! *** compound statements ------------------------------------------ SUITES (COMPOUND STATEMENTS) Suppose assert P1 S1 assert Q and assert Q S2 assert Q2 then: ------------------------------------------ ... assert P1 S1 S2 assert Q2 Q: What role does Q play in the sequence? It's what's true in the middle. ------------------------------------------ CONDITIONAL STATEMENTS Suppose assert defined(E) and E S1 assert Q and assert defined(E) and not E S2 assert Q then: ------------------------------------------ ...assert defined(E) if E: S1 else S2 assert Q Q: Why does this work? because when execution reaches S1, E is True, when execution reaches S2, not E is True. Q: What S1 and S2 don't produce the same postcondition? they have to, but it's okay if one is stronger than Q *** rule of consequence ------------------------------------------ RULE OF CONSEQUENCE Suppose assert P S assert Q then: assert P' S assert Q' holds, provided Why? ------------------------------------------ ... P' ==> P and Q ==> Q' ... want assert P' ==> assert P S S assert Q' <== assert Q if P ==> P' then when s in P', then s in P and we assume when S is executed in P, then Q holds. But if Q ==> Q', then for every s in Q, s is also in Q' so Q' holds. ** Examples ------------------------------------------ THE MAXIMUM FUNCTION Write a function maximum(n1, n2) that satisifes the specification: Requires isinstance(n1, numbers.Number) and isinstance(n2, numbers.Number) Ensures (result == n1 or result == n2) and result >= n1 and result >= n2 def maximum(n1, n2) assert isinstance(n1, numbers.Number) assert isinstance(n2, numbers.Number) ------------------------------------------ Q: What kind of statement do we need to write to make the postcondition true? a conditional one Q: Why? because we have to achieve a conjunction for a postcondition Heuristic for introducing an if-statement: see if there is some condition that will help you achieve part of the postcondition, and its negation will achieve the rest (or several such cases for a if-elif... statement) ... if n1 >= n2: assert n1 >= n2 else: assert not (n1 >= n2) then fill this in with assignments to result assert n1 >= n2 result = n1 assert result == n1 and result >= n2 assert not (n1 >= n2) # so... assert n1 < n2 result = n2 assert result == n2 and result > n1 # so... assert result == n2 and result >= n1 Note that result == n1 and result >= n2 implies (result == n1 or result == n2) and result >= n1 and result >= n2 also result == n2 and result >= n1 implies (result == n1 or result == n2) and result >= n1 and result >= n2