Next Generation Type Systems

42
Next Generation Type Systems Greg Morrisett Harvard University

description

Next Generation Type Systems. Greg Morrisett Harvard University. A Famous Phrase:. “Well typed programs won’t go wrong.” More formally: 1. Describe abstract machine: M ::= [ s ,c] 2. Give transition relation: M 1  M 2 [ s , (x:=42 ; c)]  [ s {x  42}, c] - PowerPoint PPT Presentation

Transcript of Next Generation Type Systems

Page 1: Next Generation Type Systems

Next Generation Type Systems

Greg MorrisettHarvard University

Page 2: Next Generation Type Systems

A Famous Phrase:

“Well typed programs won’t go wrong.”

More formally:1. Describe abstract machine: M ::= [,c]2. Give transition relation: M1 M2

[, (x:=42 ; c)] [{x42}, c][, if true then e1 else e2] [, e1]

[, if false then e1 else e2] [, e2]

3. Classify all terminal states as “bad” or “good” good: [, exit(0)], [, 42] bad: [, if 42 then e1 else e2], [, “Simon” / true]4. Prove well-typed code never reaches bad states.

Page 3: Next Generation Type Systems

What’s “good” and “bad”?

• I could say [, “Simon” / true ] [, 42].

• I could say [, exit(0)] is “bad”.

• It’s up to you!

• But of course, for even simple safety policies, statically proving a program (much less a language) won’t “go wrong” is pretty challenging.

Page 4: Next Generation Type Systems

Thus, we cheat:• For real languages (Java, C#, ML, …):

– We add some artificial transitions:[, 42 / 0] [, throw(DivByZero)]

– and then label some bad states as good:[, throw(DivByZero)]

• Other examples:– Null pointer dereference, array index out of bounds,

bad downcast, stack inspection error, file already closed, deadlock, …

• So the reality is that today, well-typed programs don’t continue to go wrong.– Better than a code injection attack.– But little comfort when your airplane crashes.

Page 5: Next Generation Type Systems

Exceptions• The escape hatch for typing: throw : .exn • In languages such as ML & Haskell,

they don’t appear in interfaces:– div : int int int – sub : .array int

• All too easy to forget to write a handler.

Page 6: Next Generation Type Systems

Throws ClausesIn Java & C# we have throws clauses:

– div : int int int throws DivByZero

– sub : .array int throws Subscript

Much better documentation.

But introduces a couple of problems…

Page 7: Next Generation Type Systems

Problems with Throws:• Iterators (more generally,

abstraction) break:– map: ,.() list list – map div vs. map sub– map:,,.(throws)

list list throws

– Must add attendant polymorphism.• False positives:

if (n != 0) avg := div(sum,n);else avg := 0;

Page 8: Next Generation Type Systems

What We Really Want:• Refinements:

– div : int (y:int) int requires y != 0– sub : .(x:array ) (i:int)

requires i >= 0 && i < size(x)– head : .(x:list ) requires x != NULL

• And even:– printf : (x:string) -> (vs:list obj) -> unit requires (ts,parses(x,ts) &&

have_types(vs,ts))

– prove: (p:prop) -> (b:bool)

ensures (b = true => p)– compile : (x:ast) (y:x86)

ensures (bisimilar(x,y))

Page 9: Next Generation Type Systems

Driving Research Question:Static type-checking has proven to

be an effective, lightweight formal method.

Over 10 years, can we scale it to:– Eliminate remaining language errors?– Eliminate more API and user-level

errors?– Go all the way to correctness? QuickTime™ and aTIFF (Uncompressed) decompressorare needed to see this picture.

QuickTime™ and aTIFF (Uncompressed) decompressorare needed to see this picture.

Page 10: Next Generation Type Systems

Extended Static CheckingESC/M3, ESC/Java, Spec#, Cyclone, Deputy, …• Take existing languages (Java, C#, C).• Aimed at eliminating language bugs:

– null pointers, array bounds, downcasts, …

• Augment types with pre/post-conditions.• Calculate refinements at each program point.

– use weakest-pre or strongest-post-conditions– in conjunction with some abstract interpretation

techniques to generate loop invariants

• Use SMT prover to check pre/post-conditions.

Page 11: Next Generation Type Systems

Simple Example:void strncpy(char *d, char *s, int n)

requires(|d| <= n && |s| <= n)

{

int i = 0;

while (i < n) {

d[i] = s[i];

i++;

}

}

|d| <= n, |s| <= n, i=0

|d| <= n, |s| <= n, 0 <= i < n

|d| <= n, |s| <= n, 0 <= i < n

|d| <= n, |s| <= n, 0 <= i < n

|d| <= n, |s| <= n, i >= n

Page 12: Next Generation Type Systems

Tremendous Progress• Scalable analysis:

– in spite of path & context sensitivity– one key: modularity– another key: efficient representation techniques

• Much improvement in provers:– SMT provers integrate decision procedures– Advances with SAT, BDDs, ILPs, …

• Improved invariant finders:– e.g., octagon domains– counter-example guided refinement

For 70 Kloc in the Cyclone compiler, discharge 95% of the null & array bounds checks.

Page 13: Next Generation Type Systems

All Not Rosy: foldl( f(,,), , , list_t<>);

int strcpy_offset(string_t d, int i, string_t s) requires (i+|s| <= |d|) ensures (result = i+|s|);

int add_size(int dummy, string_t s, int a) ensures (result = a+|s|);

string_t concat(list_t<string_t> xs){ int n = foldl(add_size, 0, 0, xs); string_t res = new_string(n); foldl(strcpy_offset, d, 0, xs); return d;}

Page 14: Next Generation Type Systems

Even if we inline HO code:int strcpy_offset(string_t d, int i, string_t s) requires (i+|s| <= |d|) ensures (result = i+|s|);

string_t concat(list_t<string_t> xs){ int n = 0; for (let t = xs; t != NULL; t = t->tl) n += size(t->hd); string_t res = new_string(n); int i = 0; for (let t = xs; t != NULL; t = t->tl) i = strcpy_offset(res, i, t->hd); return res;}

Can’t synthesize an inductive invariant, much less prove it automatically!

Page 15: Next Generation Type Systems

Forced to Rewrite Again:int strcpy_offset(string_t d, int i, string_t s) requires (i+|s| <= |d|) ensures (result = i+|s|);

string_t concat(list_t<string_t> xs){ int n = 0; for (let t = xs; t != NULL; t = t->tl) n += size(t->hd); string_t res = new_string(n); int i = 0; for (let t = xs; t != NULL; t = t->tl) if (i + size(t->hd) <= n) i = strcpy_offset(res, i, t->hd); else throw Error; return res;}

Page 16: Next Generation Type Systems

The Reality of ES Checkers:• Still too many false positives:

– Still have 1000 checks left in Cyclone compiler– programmers will dismiss false positives

• Many Culprits:– language of specifications is too weak– calculated invariants are too weak– theorem provers are too weak– memory, aliasing, framing (more on this later)

• Even with unsound assumptions!– no overflow, no aliasing, no concurrency, …

• Seems hopeless, no?

Page 17: Next Generation Type Systems

What Can We Do?1. Change the language to minimize VCs:

– polymorphism minimized downcasts in C#.– make not-NULL the default.– aliasing issues are minimized when most

values are immutable or localized.– working with maps/folds/iterators on arrays

(or other finite maps) avoids indexing problems.

– bignums are easier to reason about.– …

If we don’t do this, we’ll spend 10 more years in the weeds working on problems we can define away.

Page 18: Next Generation Type Systems

Other Big Change:Give programmers the ability to work

around short-comings of automation.– Magic is good as long as it doesn’t prevent you

from getting real work done…

In particular, give them a way to build explicit proofs within the language.– if automation can’t find proof, at least

programmer can try to construct one.

Not a new idea: type theory!– in particular, Coq, Isabelle/HOL, PRL, Epigram,

…– but many challenges in making this practical

Page 19: Next Generation Type Systems

Programming in Type Theory:sub:(v:vector ,i:nat,pf:i<size(v)) ;

cmp:(i:nat,j:nat) LT{i<j} + GTE{i>=j}

checked_sub(v:vector , i:nat):option = match cmp(i,size(v)) with

| LT{pf} => SOME(sub(v,i,pf))

| _ => NONE

end

Page 20: Next Generation Type Systems

Another Example:sub:(v:vector ,i:nat,pf:i<size(v)) ;

lt_trans: i j k,(i < j) (j <= k) (i < k)

sum_upto(v:vector int, e:nat, pf: e <= size(v)) =

let loop(i:nat,a:int):int =

match cmp(i,e) with

| LT pf2 =>

loop(i+1, a+sub(v,i,lt_trans pf2 pf))

| _ => a

in

loop(0,0)

end

Page 21: Next Generation Type Systems

In Coq:sum_upto(v:vector int, e:nat, pf: e<=size(v)) = let loop(i:nat,a:int):nat = match lt(i,e) with | LT pf2 =>

loop(i+1, a+sub(v,i,[•])) | _ => a in loop(0,0) end

Can leave “holes” for proofs.• proof obligations are generated.• engage in interactive proof using tactics to

discharge or simplify obligations.• output is a proof object (can independently

check.)

Page 22: Next Generation Type Systems

Uniformity and Modularity:In Coq, we can abstract over:

– terms (i.e., values, functions)– types (i.e., polymorphism)– predicates (just another kind of type)– proofs (just another kind of term)

For example:vmap : (P Q:Prop) ((x:,P x) (y:,Q y)) (xs:vector ,vall P xs) (ys:vector ,vall Q

ys)

where vall P v = (i:nat), (pf:i < |v|) P (sub v i pf)

Page 23: Next Generation Type Systems

After-the-Fact Reasoning:Often, we don’t know what properties we

need to specify for a given function.e.g., vector sort:– (x:vector int) (y:vector int, pf:|y|=|x|)– (x:vector int) (y:vector int, pf:sorted(y))– (x:vector int) (y:vector int, pf:permutation(x,y))

Coq lets us prove properties of definitions after the fact (e.g., by induction on the length of the vector) and then export those proofs independently:– sort : vector int vector int– pf1 : x, permutation(x,sort(x))– pf2 : x, sorted(sort(x))

Page 24: Next Generation Type Systems

How Does All This Scale?X.Leroy [PoPL ‘06]: correct, optimizing

compiler from C to PowerPC:• Build interpreter for C code.• Build interpreter for PowerPC code.• compile: S (T, Cinterp(S) PPCinterp(T))

– compiler comparable to good ugrad class• CSE, constant prop, register allocation, …

– decomposed into series of intermediate stages

• Coq extracts Ocaml code by erasing proofs– not just modeling code and proving model correct.– software/proof co-design

Bottom line: it’s feasible to build mechanically verified software using this kind of approach.

Page 25: Next Generation Type Systems

Great Progress, but…• 4,000 line compiler:

– 7,000 lines of lemmas and theorems• includes interpreters/models of C and PPC code• much is re-usable in other contexts

– 17,000 lines of proof scripts• though with right tactics, could at least cut in half.• and keep in mind, this is a very deep property.

• Many research opportunities here:– Advances in SMT provers not yet adopted.– Can we maintain proofs when code changes?

• Proof scripts (a la Coq) are unreadable though smaller & less sensitive to change than explicit proofs.

• Explicit proofs (a la Twelf) are bigger, but perhaps force better abstraction, readability, & maintainability.

Page 26: Next Generation Type Systems

Another Big Problem:Systems like Coq are limited to pure, total

functions:– no hash tables, union-find, splay trees, …

• Leroy forced to use functional data structures• Not a bad thing per se, but we should be able to get

good algorithmic complexity where needed (e.g., unification.)

– no I/O, no exceptions, no diverging computations, no concurrency, …

• So building a server in Coq is out of the question.

Note: you can model these things in Coq.– but then you have the model/code

disconnect.

Page 27: Next Generation Type Systems

Why Purely Functional?1. Reasoning principles: foo(x: nat, v: vector int, p: x < |v| ) { … x := rand(); … sub(v,x,p) …

y := 2*x; }

Predicates (& other types) are persistent:• what is p’s type after x:=rand()?

Want substitution principles:• y = 2*x and 2*x = x+x, ergo

y = rand()+rand()?

Page 28: Next Generation Type Systems

Why Only Total Functions?• Again, simplifies reasoning:

– e.g., can use list induction on e to prove sorted(sort e).

• But perhaps more importantly:– In ML, Haskell, C#, …

fun bot() = bot() : .unit– If we can code bot in Coq:

bot() : 42 < 0– So the logic becomes inconsistent.

Page 29: Next Generation Type Systems

A Solution: MonadsSimilar to Haskell, distinguish purity with

types:• e : int

– e is equivalent to an integer value• e : int

– e is a delayed computation which when run in a world w either diverges, or yields an int and some new world w’.

– Because computations are delayed, they are pure.

– So we can safely manipulate them within types and proofs.

• e : (42 < 0)– meaningful, but means e must diverge when run!

Page 30: Next Generation Type Systems

Reasoning with : By refining with predicates, we can

capture the effects of an imperative computation within its type.

e : {P}x:int{Q}When run in a world satisfying P, either– diverges, or else– terminates with an integer x and world

satisfying Q.i.e., Hoare-logic meets Type Theory

Page 31: Next Generation Type Systems

Ynot:• Extends Coq with a refined monad:

– non-termination, (H.O.) state, exceptions– working on I/O, FFI, and concurrency

• Can extract imperative Haskell code directly.• Able to code & prove correct imperative ADTs

– e.g., hash tables

• “Principle Specifications”– Automatically infers weakest pre- and strongest

post-condition for straight-line, imperative code.– (Recursive functions require explicit invariants.)

Page 32: Next Generation Type Systems

For Example:insert(h:table)(k:key)(v:value) :=

Let i := mod(hash k,size h)

in

bucket <- (sub h i _);

upd h i ((k,v) :: bucket) _

end

Page 33: Next Generation Type Systems

What Coq “Infers”:insert(h:table,k:key,v:value) :

{bind_pre (read_pre (vsub h (mod(hash k,size h))

(read_post (vsub h (mod(hash k,size h))

(fun _ : list (key * value) =>

allocated (vsub h (mod(hash k,size h))))} _:unit { bind_post

(read_pre (vsub h (mod(hash k,size h))))

(read_post (vsub h (mod(hash k,size h))))

(fun bucket =>

write_post

(vsub h (mod(hash k,size h)))((k, v)::bucket)) }

Essentially, the code is reflected in the type.

Page 34: Next Generation Type Systems

In Practice:Cast to desired spec, but with explicit proof: do : {P1}x:a{Q1} (P2P1 Q1Q2) {P2}x:a{Q2}

For example:insert(h:table)(k:key)(v:val):

{reps(h,F)}_:unit{reps(h,ins(k,v,F)} := do (Let i := mod(hash k,size h) …) _

<16-line proof script>

Page 35: Next Generation Type Systems

Footprints in Hoare LogicConsider:inc(x:loc) : {x->v}_:unit{x->v+1}

Suppose I know {a->0 b->4}:Q: What do I know after calling inc(a)?A: {a->1}

I lose the fact that b->4 (and anything not specified in the post-condition of inc!).

Page 36: Next Generation Type Systems

Minimizing Footprints:Traditionally, we add a “modifies” clause:void inc(x:int ref) modifies(x)

But this suffers from many problems:– As before, HO code needs to abstract over modifies

clauses.– What we modify will depend (in general) on inputs.– For ADT interfaces, we don’t want to reveal specifics of

changed locations. • e.g., if I change my hash table buckets from lists to

splay trees, the interface has to change.

A key issue since Hoare Logic was introduced.

Page 37: Next Generation Type Systems

Much Recent Progress• Substructural types (e.g., linearity, uniqueness):

– At most one reference to a unique object.– So inc() can’t access a ref unless we pass it directly.– Therefore, any unique ref not passed to inc() has its

value preserved.– But of course, uniqueness is a huge constraint.

• Confined/Ownership types:– Label references with an “owner”

• e.g., buckets are owned by the Hashtable

– ADTs are allowed to modify locations they “own” without revealing this in interfaces.

– Works well for local state, but is problematic when ownership needs to be transferred.

Page 38: Next Generation Type Systems

Separation Logic[Reynolds, Pym, O’Hearn, …]

Specifications are rigged to be “parametric” in an extended heap:

If e : {P}x:a{Q} then:• forall heaps h = h1 h2 where P(h1)• when we run e in h, we get out an h’• where h’ = h1’ h2 and Q(h1’)Anything not mentioned by P is

preserved!

Page 39: Next Generation Type Systems

For Example:Define (P*Q)(h):=

h=h1h2 and P(h1) and Q(h2).

inc x : {x->v}_:unit{x->v+1}insert h k v : {reps(h,F)}_:unit{reps(h,ins(v,k,F))}

{a->0 * b->4 * reps(h,F)}inc(a);{a->1 * b->4 * reps(h,f)}insert h k v;{a->1 * b->4 * reps(h,ins(v,k,f))}

Page 40: Next Generation Type Systems

To Wrap Up• Type theory makes it possible to capture &

check deep properties of code.– from simple types to full correctness.

• Provides a uniform, modular framework for:– types and specifications.– code, models, and proofs.– abstraction at all levels.

• Recent advances scale it from pure languages to more realistic programming environments without losing modularity.– refined monads.– separation logic.

Page 41: Next Generation Type Systems

But Lots to Do:• More experience

– e.g., a server guaranteed to make progress

• Scaling the theory further:– concurrency– total correctness, liveness, …

• More automation:– better inference– adapt good decision procedures from SMT– termination analyses, shape analyses, etc.

• Re-think languages & environments:– in particular, for discharging explicit proofs

Page 42: Next Generation Type Systems

I Remain OptimisticThese obstacles will be overcome.

We won’t develop all software with proofs of correctness, but I do believe that within another 10 years:– type systems for mainstream languages

will rule out language-level errors, and many library errors, and

– a lot more safety & security-critical code will be developed with machine-checked proofs of key properties.