Y0 Ynot Y-knot Y

advertisement
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
Download