30
Y 0 Y-not Y-knot Y- naught? Greg Morrisett with Aleks N., Ryan W., Paul G., Rasmus P., Lars B.

Y 0 Y-not Y-knot Y-naught? Greg Morrisett with Aleks N., Ryan W., Paul G., Rasmus P., Lars B

  • View
    215

  • Download
    2

Embed Size (px)

Citation preview

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)

wheremapsto(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]. whereupdate(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 Swapforall (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-conditiononly!

Do

do : forall (A:Set)(P1:Pre)(Q1:Post A)(E1:Post exn) (P2:Pre)(Q2:Post A)(E2:Post exn), (IO A P1 Q1 E1) -> (forall h,(P2 h) -> (P1 h)) -> (forall y old m, (p2 old) -> (Q1 y old m) -> (Q2 y old m)) -> (forall e old m, (p2 old) -> (E1 y old m) -> (E2 y old m)) -> IO A P2 Q2 E2.Implicit Arguments do [A P1 Q1 E1].

Essentially, the rule of consequence.

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