Y0 Y-not Y-knot Y-naught? Greg Morrisett with Aleks N., Ryan W., Paul G., Rasmus P., Lars B. ESC, JML, Spec#, … • Need a different spec language: – “pure” boolean expressions: length(x)==42 – “modeling” types (e.g., pure lists, sets, …) • If the implementation is pure, can’t use it in the specs! – x.f versus \old(x.f) • What if Simplify can’t prove something? – Ignore (e.g., arith, modifies clause): unsound – Rewrite code? – Weaken spec? • Not really modular: can’t write “app” or “map” DML, ATS, Omega, … • Introduce different spec language. – Again, a separate “pure” language • To capture all properties of lists, you’d have to index them with well, lists. • Can’t talk about properties of effectful computations (or limited capacity). • ATS can build proofs, but it’s awkward. Coq, PRL, Isabelle, … • Ynot starts with Coq as the basic language: – [co]inductive definitions, h.o. functions, polymorphism, h.o. predicates, proofs, … – Strong support for modularity • e.g., can package up and abstract over terms, types, predicates, proofs, in a uniform fashion. – can prove that, e.g., append is associative and rev(rev(x)) = x, etc. after defining it. • But huge drawback: – No effects (non-term, IO, state, recursive types, etc.) – Not a strong phase separation Quick Coq • Set : (think * in Haskell) – nat, bool, functions from sets to sets, … – inductive set definitions (e.g., list) – co-inductive set definitions (e.g., stream) • Prop : – Think of nat->Prop as a subset of nat. – Equality, /\, \/, etc. – [co-]inductive definitions (e.g., judgments) Refinement • Can form mixed products & sums • {n:nat | n >= 42} is a pair of a nat and a proof that this nat is >= 42. • Array subscript: forall (A:Set)(n:nat) (v:vector n A) (j:nat), (j < n) -> A • Can extract Ocaml or Haskell code. – “erase” Prop objects (really, replace with unit). Purity • We want to pull the Curry-Howard isomorphism to represent a proof of Prop P as a term with type P. – Reduce proof-checking to type-checking. – Should be no term with type False. • If we added recursive functions, recursive types, exceptions, or refs, we could code up a term of type False. – So everyone forgoes these “features” in their type theory. Coq Demo • A few examples… Ynot and HTT • We add a new type constructor, IO A – As in Haskell, encapsulate effectful computations. – We’re not pretending that IO False is a proof of false -- rather, it’s a computation which when run, if it terminates, then it produces a proof of False. • Of course, it can’t terminate. Ynot IO: Attempt #1 • IO : Set -> Set • return: forall (A:Set), A -> IO A • bind : forall (A B:Set), IO A -> (A -> IO B) -> IO B • ffix : forall (A:Set), (IO A -> IO A) -> IO A Reasoning about IO • steps : IO A -> IO A -> Prop. • steps_ret_bnd : forall (A B:Set)(v:A)(f:A->IO B), steps (bind (ret v) f) (f v). • steps_bnd_cong : forall (A B:Set)(c1:IO A)(f:A->IO B), (steps c1 c2) -> (steps (bind c1 f) (bind c2 f)). • steps_ffix : forall (A:Set)(f:IO A->IO A), steps (ffix f) (f (ffix f)) Problem: • We have added a way to prove False! • Sketch of problem (not quite right): • Define diverges(c:IO A):Prop – Define stepsn c1 c2 n as c1 steps to c2 in no more than n steps. – Define diverges c as there’s no n and v such that stepsn c (ret v) n. • Define T := { f : nat -> IO nat | for some n, diverges(f n) } Problem Continued • Next, define: f(p:T):T = {g;q } where g n = if n = 0 then 0 else (fst p)(n-1) and q argues that for some n, g diverges: (snd p) provides a proof that for some m, (fst p) diverges, so pick n=m+1. • Finally, take F := ffix(f) – snd(F) proves fst(F) diverges – but fst(F) does not! How to Fix? • One option: restrict IO to admissible types. – In essence, we need closure conditions to ensure that fixed-points preserve typing. – Comprehensions (subsets of types) are problematic in general. – Crary shows some sufficient syntactic criteria for determining admissibility. • Another option: don’t expose steps or any other axiom on IO terms. – Well, we can expose some (the monad laws.) No Axioms? • Can interpret IO A := unit. – ret v = tt, bind v f = tt, ffix f = tt • Without any axioms, can’t tell the difference! • Allows us to establish consistency of logic. – a trivial model. • Aleks is then able to prove preservation and progress for the real operational semantics. • But we have limited reasoning about computations within the system. Extending IO • We want to handle the awkward squad: – Refs, IO, exceptions, concurrency, … • So need to scale IO A. – Today: refs, exceptions – Tomorrow: IO – Quite a ways off: concurrency? Heaps & Refs in Ynot We model heaps in Coq as follows: • loc : Set • loc_eq:(x y:loc)->{x=y}+{x<>y} – can model locs as nats. • dynamic := {T:Set; x:T} • heap := loc -> option dynamic – NB: heaps aren’t “Set” w/out impredicative IO Monad • Pre := heap -> Prop • Post(A:Set) := A -> heap -> heap -> Prop • IO: forall (A:Set), Pre -> Post A -> Post exn -> Set. • Implicit Arguments IO [A]. Return & Throw ret : forall (A:Set)(x:A), IO (fun h => True) (fun y old h => y=x /\ h=old) (fun e old h => False) Implicit Arguments ret[A]. throw : forall (A:Set)(x:exn), IO (fun h => True) (fun y old h => False) (fun e old h => e=x /\ h=old) Reading a Location read : forall (A:Set)(x:loc), IO (fun h => exists v:A,mapsto h x v) (fun y old h => old = h /\ mapsto h x v) (fun e old h => False) where mapsto(A:Set)(h:heap)(x:loc)(v:A) := (h x) = Some(mkDynamic {A;v}} Writing a Location write : forall (A:Set)(x:loc)(v:A), IO (fun h => exists B, exists w:B, mapsto h x w) (fun y old h => y = tt /\ h = update old x A v) (fun e old h => False) Implicit Arguments write[A]. where update(h:heap)(x:loc)(A:Set)(v:A):heap := fun y => if (eq_loc x y) then Some(Dynamic{A,v}) else h y Bind bind : forall (A B:Set)(P1:Pre)(Q1:Post A)(E1:Post exn) (P2:A->Pre)(Q2:A->Post B)(E2:A->Post exn), (IO A P1 Q1 E1) -> (A -> IO B P2 Q2 E2) -> IO B (fun h => P1 h /\ (forall x m,(Q1 x h m) -> P2 m)) (fun y old m => exists x m, (Q1 x old m) /\ (Q2 y m h)) (fun e old m => (E1 e old m) \/ (exists x m, (Q1 x old m) /\ (E2 e m h))) Implicit Arguments bind [A B P1 Q1 E1 P2 Q2 E2]. Using Bind Definition readThen := fun (A B:Set)(x:loc) (p:A->pre)(q:A->post B) (e:A->post exn) (c:forall y:A, IO (p y) (q y) (e y))=> bind (read A x) c. Implicit Arguments readThen [A B p q e]. Example: Definition swap := fun (A B:Set)(x y:loc) => (readThen x (fun (xv:A) => readThen y (fun (yv:B) => writeThen x yv (writeThen y xv (ret tt))))). Type Inferred for Swap forall (A B : Set) (x y : loc 1), IO (fun i : heap => (fun i0 : heap => exists v : A, mapsto i0 x v) i /\ (forall (x0 : A) (m : heap), (fun (y0 : A) (i0 m0 : heap) => mapsto i0 x y0 /\ m0 = i0) x0 i m -> (fun (xv : A) (i0 : heap) => (fun i1 : heap => exists v : B, mapsto i1 y v) i0 /\ (forall (x1 : B) (m0 : heap), (fun (y0 : B) (i1 m1 : heap) => mapsto i1 y y0 /\ m1 = i1) x1 i0 m0 -> (fun (yv : B) (i1 : heap) => (fun i2 : heap => exists B0 : Set, exists z : vector B0 1, mapsto_vec i2 x z) i1 /\ (forall (x2 : unit) (m1 : heap), (fun (_ : unit) (i2 m2 : heap) => m2 = update i2 x yv) x2 i1 m1 -> (fun (_ : unit) (i2 : heap) => (fun i3 : heap => exists B0 : Set, exists z : vector B0 1, mapsto_vec i3 y z) i2 /\ (forall (x3 : unit) (m2 : heap), (fun (_ : unit) (i3 m3 : heap) => m3 = update i3 y xv) x3 i2 m2 -> (fun _ : unit => nopre) x3 m2)) x2 m1)) x1 m0)) x0 m)) pre-condition only! Do do : forall (A:Set)(P1:Pre)(Q1:Post A)(E1:Post (P2:Pre)(Q2:Post A)(E2:Post (IO A P1 Q1 E1) -> (forall h,(P2 h) -> (P1 h)) -> (forall y old m, (p2 old) -> (Q1 y old m) -> (Q2 y old (forall e old m, (p2 old) -> (E1 y old m) -> (E2 y old IO A P2 Q2 E2. Implicit Arguments do [A P1 Q1 E1]. Essentially, the rule of consequence. exn) exn), m)) -> m)) -> Ascribing a Spec to Swap Program Definition swap_precise : forall (A B:Set)(x y:loc 1), IO (fun i => exists vx:A, exists vy:B, mapsto i x vx /\ mapsto i y vy) (fun (_:unit) i m => exists vx:A, exists vy:B, m = update (update i x vy) y vx) (fun _ _ _ => False) := fun A B x y => do (swap A B x y) _. Followed by a long proof. (can be shortened with combination of key lemmas and tactics.) Another Example: Definition InvIO(A:Set)(I:Pre) := IO I (fun (_:A) _ m => I m) (fun (_:exn) _ m => I m). Program Fixpoint mapIO(A B:Set)(I:pre) (f:A -> B -> InvIO B I (acc:B)(x:list A) {struct x} : InvIO B I := match x with | nil => do (ret acc) _ | cons h t => do (bind (f h acc) (fun acc2 => mapIO A B p q e pf f acc2 t)) _ end. Advantages • For pure code: – Can use refinements a la DML/ATS – Or, can reason after the fact • E.g., can prove append associative without having to tie it into the definition. • Modeling language is serious – e.g., heaps are defined in the model. • Abstraction over values, types, specifications, and proofs (i.e., compositional!) • If you stick to simple types, no proofs. Key Open Issues • Proofs are still painful. – Need to adapt automation from ESC • Need analogues to object invariants, ownership, etc. for mutable ADTs. – Separation logic seems promising (next time). • IO and other effects – Need pre/post over worlds (heaps are just a part.) • Better models? – Predicate transformers seem promising – Rasmus & Lars working on denotational model