270 likes | 406 Views
Static Contract Checking for Haskell. Dana N. Xu University of Cambridge. Ph.D. Supervisor: Simon Peyton Jones Microsoft Research Cambridge. From Types to Contracts. BAD means “should not happen: crash”. h ead [] = BAD head (x:xs) = x head :: [ a ] -> a …(head 1)…
E N D
Static Contract Checking for Haskell Dana N. Xu University of Cambridge Ph.D. Supervisor: Simon Peyton Jones Microsoft Research Cambridge
From Types to Contracts BAD means “should not happen: crash” head [] = BAD head (x:xs) = x head :: [a] -> a …(head 1)… • head{xs | not (null xs)} -> {r | True} …(head [])… Type null :: [a] -> Bool null [] = True null (x:xs) = False Bug! Contract (arbitrary Haskell boolean expression) Bug!
What we want • Adapt Findler-Felleisen’s ideas for dynamic (high-order) contract checking. • Do static contract checking for a lazy language. Haskell function Contract Glasgow Haskell Compiler (GHC) Where the bug is Why it is a bug
Three Outcomes • (1) Definitely Safe (no crash, but may loop) • (2) Definite Bug (definitely crashes) • (3) Possible Bug
Sorting (==>) True x = x (==>) False x = True sorted [] = True sorted (x:[]) = True sorted (x:y:xs) = x <= y && sorted (y : xs) insert :: Int -> [Int] -> [Int] insert {i | True} -> {xs | sorted xs} -> {r | sorted r} merge :: [Int] -> [Int] -> [Int] merge {xs | sorted xs}->{ys | sorted ys}->{r | sorted r} bubbleHelper :: [Int] -> ([Int], Bool) bubbleHelper {xs | True} -> {r | not (sndr) ==> sorted (fstr)} insertsort, mergesort, bubblesort {xs | True} -> {r | sorted r}
AVL Tree (&&) True x = x (&&) False x = False balanced :: AVL -> Bool balanced L = True balanced (N tu) = balanced t && balanced u && abs (depth t - depth u) <= 1 data AVL = L | N Int AVL AVL insert, delete :: AVL -> Int -> AVL insert {x | balanced x} -> {y | True} -> {r | notLeafr && balanced r && 0 <= depth r - depth x && depth r - depth x <= 1 } delete {x | balanced x} -> {y | True} -> {r | balanced r && 0 <= depth x - depth r && depth x - depth r <= 1}
The Contract Idea for Higher-Order Function[Findler/Felleisen] f1 :: (Int -> Int) -> Int f1 ({x | x > 0} -> {y | y >= 0}) -> {r | r >= 0} f1 g = (g 0) - 1 f2 :: {r | True} f2 = f1 (\x -> x – 5) Blame f1: f1 calls g with wrong argument f1 does not satisfy its post-condition f3 :: {r | True} f3 = f1 (\x -> x – 1) Blame f2: f2 calls f1 with wrong argument f3 is Ok. Can’t tell at run-time
What is a Contract? arbitrary Haskell booleanexpression Ok = {x | True} Contract t ::= {x | p} Predicate Contract | x:t1->t2Dependent Function Contract | (t1, t2) TupleContract | Any Polymorphic Any Contract 3 { x | x > 0 } (3, []) Any 3 { x | True } (3, []) (Ok, {ys | null ys}) arbitrary constructor inc x:{x | x>0} -> {y | y == x + 1} Postcondition can mention argument Precondition Postcondition
What we want? Check f <contract_of_f> • If mainOk , then the whole program cannot crash. • If not, show which function to blame and why. Beauty of Contract Checking
[POPL’10] Define e t Main Theorem e tiffet is crash-free (related to Blume&McAllester:JFP’06) Construct et (e“ensures”t) ESC/Haskell [Haskell’06] Symbolically simplify (et) some e’ See if BADis syntactically in e’. If yes, DONE; else give BLAME
Wrappers and ( pronounced ensures pronounced requires) • e{x | p} = case p[e/x] of True -> e False -> BAD • ex:t1->t2 • = v. (e (vt1)) t2[(vt1)/x] • e (t1, t2) = case e of • (e1, e2) -> (e1 t1, e2 t2) • eAny = UNR related to [Findler-Felleisen:ICFP02]
Wrappers and ( pronounced ensures pronounced requires) • e{x | p} = case p[e/x] of True -> e False -> UNR • ex:t1-> t2 • = v. (e (vt1)) t2[vt1/x] • e (t1, t2) = case e of • (e1, e2) -> (e1 t1, e2 t2) • eAny = BAD related to [Findler-Felleisen:ICFP02]
Some Interesting Details Theory Practice • Contracts that loop • Contracts that crash • Lovely Lemmas • Adding tags, e.g. BAD “f” • Achieves precise blaming • Moretags to trace functions to blame • Achieves the same goal of [Meunier:POPL06] • Using a theorem prover • Counter-example guided unrolling
Summary • Static contract checking is a fertile and under-researched area • Distinctive features of our approach • Full Haskell in contracts; absolutely crucial • Declarative specification of “satisfies” • Nice theory (with some very tricky corners) • Static proofs • Modular Checking • Compiler as theorem prover
After Ph.D. • Postdoc project in 2009: probabilistic contract for component base design [ATVA’2010] • Current project at Xavier Leroy’s team (INRIA) - a verifying compiler: 1. Apply the idea to OCaml compiler by allowing both static and dynamic contract checking 2. Connect with Coq to verify more programs statically. 3. Use Coq to prove the correctness of the framework. 4. Apply new ideas back to Haskell (e.g. GHC).
Static and Dynamic Program with Specifications Static checking Dynamic checking Compile time error attributes blame to the right place Run time error attributes blame to the right place No blaming means Program cannot crashs Or, more plausibly: If you guarantee that ft, then the program cannot crash
What exactly does it mean to say that e “satisfies” contract t? et
When does e satisfy a contract? • Brief, declarative… inc x:{x | x > 0} -> {y | y == x + 1} Postcondition can mention argument Precondition Postcondition
When does e satisfy a contract? • The delicate one is the predicate contract. • Our decision: e {x | p} iff e is crash-free • Question: What expression is crash-free? e is crash-freeiff no blameless context can make e crash e is crash-free iff C. BAD s C. C[e] * BAD
Crash-free Examples Lemma: e is syntactically safe =>e is crash-free.
When does e satisfy a contract? See the paper for … • Why e must be crash-free to satisfy predicate contract? • Why divergent expression satisfies all contract? • What if contract diverges (i.e. p diverges)? • What if contract crashes (i.e. p crashes)?
How can we mechanically check that e “satisfies” contract t? e t ???
Example head:: [a] -> a head [] = BAD head (x:xs) = x head{ xs | not (null xs) } -> Ok head {xs | not (null xs)} -> Ok = \v. head (v {xs | not (null xs)}) Ok e Ok = e = \v. head (v {xs | not (null xs)}) = \v. head (case not (null v) of True -> v False -> UNR)
null :: [a] -> Bool null [] = True null (x:xs) = False not :: Bool -> Bool not True = False not False = True \v. head (case not (null v) of True -> v False -> UNR) Now inline ‘not’ and ‘null’ = \v. head (case v of [] -> UNR (p:ps) -> p) Now inline ‘head’ head:: [a] -> a head [] = BAD head (x:xs) = x = \v. case v of [] -> UNR (p:ps) -> p So head [] fails with UNR, not BAD, blaming the caller
Static and Dynamic [Findler, Felleisen, Blume, Hinze, Loh, Runciman, Chitil] [Flanaghan, Mitchell, Pottier] Program with Specifications Static checking Dynamic checking Compile time error attributes blame to the right place Run time error attributes blame to the right place