## 15 312: Principles of Programming Languages

15–312: Principles of Programming Languages Final Examination May 6, 2014 • There are 18 pages in this examination, comprising 4 questions worth a to...
15–312: Principles of Programming Languages Final Examination May 6, 2014

• There are 18 pages in this examination, comprising 4 questions worth a total of 100 points. • You may refer to your personal notes and to Practical Foundations of Programming Languages, but not to any other person or source. • No electronic devices of any kind are allowed unless they have been cleared in advance by the instructor. • You have 180 minutes to complete this examination. • Please answer all questions in the space provided with the question. • There are three scratch sheets at the end for your use.

Full Name: Andrew ID:

Question: Points:

Timeout

Polymorphism

Algol

Total

20

30

15

35

100

Score:

15–312 Final

1 of 18

May 6, 2014

Question 1 : Short Answer (a) (6 points) Consider a hybrid-typing extension to the parallel language in Homework 5, where the following four rules define the type dyn in its entirety: Γ ` e : dyn Γ ` e @ fun : dyn * dyn Γ ` e : dyn Γ ` e @ seq : seq(dyn)

Γ ` e : dyn * dyn Γ ` fun! e : dyn Γ ` e : seq(dyn) Γ ` seq! e : dyn

Γ ` e : dyn Γ ` fun? e : bool Γ ` e : dyn Γ ` seq? e : bool

If we instead extended the language from Homework 5 with sums and recursive types, give a recursive type that could be used to implement this dyn type: Solution: µt.(t * t) + seq(t) Given that dyn is defined as you described, implement seq? e such that, if v1 and v2 are appropriately typed values, seq?(seq! v1 ) 7→∗ true and seq?(fun! v2 ) 7→∗ false. seq? e , case (unfold e) of { inl _ => true | inr _ => false} (b) (4 points) By the proofs-as-programs principle, a proof of a given proposition corresponds to a term of a given type. For the following to logical statements, state the types that they correspond to by this principle. One example is given. Both A and B are true. A×B If A and B, then either C or D. Solution: (A × B) → (C + D) Either A is true or B is false. Solution: A + (B → void)

15–312 Final

2 of 18

May 6, 2014

(c) (10 points) Here are the (only!) statics and value judgment rules for the introduction forms of a type τ1 } τ2 : Γ ` e1 : τ1 Γ ` e2 : τ2 Γ ` foo(e1 ; e2 ) : τ1 } τ2

Γ ` e : τ1 Γ ` bar(e) : τ1 } τ2

e1 val foo(e1 ; e2 ) val

e val bar(e) val

Give appropriate and deterministic small-step structural dynamics rules for foo(e1 ; e2 ) and bar(e): Solution: e1 7→ e01 foo(e1 ; e2 ) 7→ foo(e01 ; e2 )

e 7→ e0 bar(e) 7→ bar(e0 )

Define (an) appropriate elimination form(s) for τ1 } τ2 and give both statics and deterministic dynamics. (There’s more than one reasonable way to do this.) You shouldn’t mention any types besides τ1 } τ2 in the statics you give. Solution: Γ ` e : τ1 } τ2 Γ ` e0 : τ2 Γ ` try e ow e0 : τ2

Γ ` e : τ1 } τ2 Γ ` prj(e) : τ1

e1 → 7 e01 try e1 ow e2 → 7 try e01 ow e2

e 7→ e0 prj(e) 7→ prj(e0 ) e1 val prj(foo(e1 ; e2 )) 7→ e1

e val prj(bar(e)) 7→ e

e val try foo(e; e0 ) ow e2 7→ e0

15–312 Final

3 of 18

e val try bar(e) ow e2 7→ e2

May 6, 2014

Question 2 : Timeout In this exercise, we will enrich PCF – strict nats and functions only, no sums or products – with a timeout operator. To do so, we extend PCF’s expressions with the following additional form: Sort Abstract Form Description Expressions e ::= timeout(e1 ; e2 ; e3 )

Timeout

In this expression, e1 is a provided upper limit to the number of steps e2 can take to evaluate. If e2 evaluates to a value in fewer than this number of steps, then the overall value of this expression is the value of e2 , otherwise it is the value of e3 . (a) (2 points) Write the typing rule for timeout(e1 ; e2 ; e3 ). Solution:

Γ ` e1 : nat Γ ` e2 : τ Γ ` e3 : τ Γ ` timeout(e1 ; e2 ; e3 ) : τ

tp to

(b) (4 points) The dynamic semantics of timeout(e1 ; e2 ; e3 ) has four rules. The first one is the following: e1 7→ e01 ev to1 timeout(e1 ; e2 ; e3 ) 7→ timeout(e01 ; e2 ; e3 ) Define the remaining three. The resulting dynamics should be deterministic; for any given expression, there should be at most one transition rule. Recall that this expression evaluates to the value of e2 if this value can be produced in fewer than e1 steps, and to the value of e3 otherwise. Solution: timeout(z; e2 ; e3 ) 7→ e3

ev to2

e1 val e2 7→ e02 timeout(s e1 ; e2 ; e3 ) 7→ timeout(e1 ; e02 ; e3 ) e1 val e2 val timeout(s e1 ; e2 ; e3 ) 7→ e2

15–312 Final

4 of 18

ev to3

ev to4

May 6, 2014

(c) (4 points) When implementing user interfaces, the ability to delay the evaluation of an expression can be useful. Using timeout, define the expression delay(e1 , e2 ) that introduces e1 steps of delay before stepping to e2 . You may assume that e2 has type τ . Solution: delay(e1 , e2 ) , timeout(e1 ; fix[τ ](x.x); e2 ) where τ is the type of e2 . In the next two parts, your implementations needn’t be work efficient; it is okay for them to be wildly inefficient if they meet the specification. (d) (4 points) Using timeout, define the expression earliest(e1 ; e2 ) that returns the value of whichever among e1 and e2 takes the least number of steps to produce a value (it returns the value of e1 if they take the same number of steps). Note that one or both arguments may be divergent but your expression should only diverge if both arguments diverge. If convenient, you may use a call-by-value let and/or define auxiliary functions. Solution: earliest(e1 ; e2 ) ,

15–312 Final

let try = fix[nat → τ ](try. λn : nat. timeout(n; e1 ; timeout(n; e2 ; try (s n)))) in try z

5 of 18

May 6, 2014

(e) (6 points) Using timeout, define the expression numsteps(e) that returns the number of steps it takes to evaluate e (or diverges if e diverges). If convenient, you may use a call-by-value let and/or define auxiliary functions. Solution: numsteps(e) , let val try = fix try : nat -> nat is fn (n: nat) let /* Try to evaluate e to a value, it will either * return sz (if "let _ = e in s z" becomes a value in * fewer than n+2 steps, so e becomes a value in * fewer than n+1 steps, * that is, fewer than or equal to n steps) * or z (if e fails to become a value in n or fewer steps) */ val res = timeout (s (s n), let _ = e in s z, z) in ifz res { z => try (s n) /* try again */ | s _ => n /* found it! */ } end in try z end

15–312 Final

6 of 18

May 6, 2014

(f) (10 points) State and rigorously prove the progress theorem for this language, limiting yourself to the cases that involve timeout. If you need standard lemmas (e.g. canonical forms, inversion, etc.) you must separately state them yourself and cite them explicitly, but you do not need to prove them yourself. Solution: Theorem 2.1 (Progress). If Γ ` e : τ , then either e 7→ e0 or e val. Proof. The proof proceeds by induction on the derivation T of Γ ` e : τ . It seeks to construct either a derivation S of e 7→ e0 or a derivation V of e val. Case of to: T

T3 T2 T1 Γ ` e1 : nat Γ ` e2 : τ Γ ` e3 : τ = Γ ` timeout(e1 ; e2 ; e3 ) : τ

tp to

where e = timeout(e1 ; e2 ; e3 ). By IH on T1 , there are either S1 :: e1 7→ e01 or V1 :: e1 val. There are therefore two subcases to consider. Subcase S1 :: e1 7→ e01 : Then, S :: timeout(e1 ; e2 ; e3 ) 7→ timeout(e01 ; e2 ; e3 )

by ev to1 on S10 .

Subcase V1 :: e1 val: Then, by the canonical forms lemma, either e1 = z of e1 = s e01 for V10 :: e01 val. This leads to two further subcases: Subcase e1 = z: Then, S :: timeout(z; e2 ; e3 ) 7→ e3 by ev to2 . 0 Subcase e1 = s e1 : By IH on T2 , we have either S2 :: e2 7→ e02 or V2 :: e2 val. This leads to one last set of subcases. Subcase S2 :: e2 7→ e02 : Then, S :: timeout(s e01 ; e2 ; e3 ) 7→ timeout(e01 ; e02 ; e3 ) by ev to3 on V1 and S2 . Subcase V2 :: e2 val: Then, S :: timeout(s e01 ; e2 ; e3 ) 7→ e2 by ev to4 on V1 and V2 .

15–312 Final

7 of 18

May 6, 2014

15–312 Final

8 of 18

May 6, 2014

Question 3 : Polymorphism In this question, we will work in an an extension of System F with pairs τ1 × τ2 , existential types ∃t.τ , and natural numbers nat. Types τ ::= t | τ1 → τ2 | τ1 × τ2 | ∀t.τ | ∃t.τ | nat Expressions e ::= x | λ(x : τ ) e | e1 (e2 ) | he1 , e2 i | e · l | e · r | Λ(t) e | e[τ ] | pack ρ with e as ∃t.τ | open e1 as t with x : τ in e2 | z | s(e) | ifz e{z ⇒ ez | s(x) ⇒ es } We lose some power in our natural numbers because we only provided the ifz elimination form for natural numbers instead of the recursor, but that won’t matter for this question. (a) (4 points) Define the Church encoding of option types opt(ρ) with the following type structure: ∆, Γ ` NONE[ρ] : opt(ρ)

∆, Γ ` e : ρ ∆, Γ ` SOME(e) : opt(ρ)

∆, Γ ` e : opt(ρ) ∆, Γ ` e1 : τ ∆, Γ, x:ρ ` e2 : τ ∆, Γ ` ocase e {NONE → e1 | SOME(x) ⇒ e2 } : τ And the following equational properties: ocase NONE[τ ] {NONE → e1 | SOME(x) ⇒ e2 } ≡ e1 ocase SOME(e) {NONE → e1 | SOME(x) ⇒ e2 } ≡ [e/x]e2 In the translations below, you may assume e has type ρ, that eo has type opt(ρ) as you defined it, that e1 has type τ , and that if x is a variable of type ρ, e2 has type τ . opt(ρ) , ∀t. t → (ρ → t) → t NONE[ρ] , Λ(t) λ(x : t) λ(f : ρ → t) x SOME(e) , Λ(t) λ(x : τ ) λ(f : ρ → t) f (e) ocase eo {NONE → e1 | SOME(x) ⇒ e2 } , e[τ ] e1 (λ(x : ρ) e2 )

15–312 Final

9 of 18

May 6, 2014

This Church encoding is just one way of implementing the option type. If we want to write code that can work with multiple representations, we can use existential types. Recall the rules for introducing and eliminating existential types: ∆, Γ ` ρ type ∆, t type, Γ ` τ type ∆, Γ ` e : [ρ/t]τ ∆, Γ ` pack ρ with e as ∃t.τ : ∃t.τ ∆, Γ ` e1 : ∃t.τ ∆, t type, Γ, x : τ ` e2 : τ2 ∆ ` τ2 type ∆, Γ ` open e1 as t with x : τ in e2 : τ2 The type we will use for a packaged existential type of optional nat values is ∃t. (t × (nat → t)) × (∀p. t → p → (nat → p) → p) (b) (5 points) Unpack an unknown implementation let-bound as impl and assign the pieces none, some, and ocase correctly: let impl : ∃t. (t × (nat → t)) × (∀p. t → p → (nat → p) → p) = (omitted) open impl as t with x : (t × (nat → t)) × (∀p. t → p → (nat → p) → p) in let none = x · l · l let some = x · l · r let ocase = x · r ... (c) (3 points) Implement this type, using types and syntax from part (a): pack opt(nat) with hhNONE[nat], λ(x : nat) SOME(x)i, Λ(p) λ(x : opt(nat)) λ(y : p) λ(f : nat → p) ocase x {NONE ⇒ y | SOME(n) ⇒ f (n)} as ∃t. (t × (nat → t)) × (∀p. t → p → (nat → p) → p) (d) (3 points) Implement this type using only natural numbers: pack nat with hhz, λ(x : nat) s(x)i, Λ(p) λ(x : nat) λ(y : p) λ(f : nat → p) ifz x {z ⇒ y | s(n) ⇒ f (n)} as ∃t. (t × (nat → t)) × (∀p. t → p → (nat → p) → p)

15–312 Final

10 of 18

May 6, 2014

Question 4 : Algol In this question we will develop an abstract machine semantics for Algol commands s 7→ s0 , while keeping the usual structural dynamics e 7→ e0 for expressions. There are two states of this machine: s ::= ( k B m ) | ( k C v ) `Σ k : τ

`Σ k : τ

∅ `Σ m ∼ τ

( k B m ) okΣ

∅ `Σ v : τ

v valΣ

( k C v ) okΣ

Like in lecture (but unlike in the homework assignment on parallelism), we will be unconcerned with the ultimate return type of states and stacks, so k : τ means that k is a stack that may have values of type τ returned to it. `Σ k : τ 0 `Σ  : τ

`Σ f : τ ⇒ τ 0

`Σ (k; f ) : τ

If we just care about the Algol commands ret e and bnd x ← e; m, the dynamics and statics of commands are relatively simple. We only have to worry about one frame! e 7− → e0

v valΣ

Σ

0

( k B ret e ) 7− → ( k B ret e )

( k B ret v ) 7− →( kCv )

Σ

Σ

e 7− → e0 Σ

( k B bnd x ← e; m ) 7− → ( k B bnd x ← e0 ; m ) Σ

( k B bnd x ← cmd(m0 ); m ) 7− → ( k;

(omitted)

Σ

B m0 )

(a) (6 points) State what the frame that was omitted above should be by giving the remaining dynamic semantics rule (where a value is returned to the omitted frame) and the static semantics rule for that omitted frame.

( k; bndf(x.m) C v ) 7− → ( k B [v/x]m ) Σ

Γ, x : τ `Σ m ∼ τ 0 bndf(x.m) : τ ⇒ τ 0

15–312 Final

11 of 18

May 6, 2014

Given a bool and unit type in the expression language, we can add while loops to the language as a primitive construct: Γ `Σ m1 ∼ bool Γ `Σ m2 ∼ unit Γ `Σ while m1 m2 ∼ unit We add two additional forms of frame, whilecond(m1 ; m2 ) and whilebody(m1 ; m2 ), to present the dynamics: ( k B while m1 m2 ) 7− → ( k; whilecond(m1 ; m2 ) B m1 ) Σ

( k; whilecond(m1 ; m2 ) C false ) 7− → ( k C hi ) Σ

( k; whilecond(m1 ; m2 ) C true ) 7− → ( k; whilebody(m1 ; m2 ) B m2 ) Σ

( k; whilebody(m1 ; m2 ) C hi ) 7− → ( k; whilecond(m1 ; m2 ) B m1 ) Σ

(b) (4 points) Give appropriate statics for these two frames: Solution: ∅ `Σ m1 ∼ bool ∅ `Σ m2 ∼ unit `Σ whilebody(m1 ; m2 ) : unit ⇒ unit ∅ `Σ m1 ∼ bool ∅ `Σ m2 ∼ unit `Σ whilecond(m1 ; m2 ) : bool ⇒ unit

(c) (4 points) Consider changing the statics we initially gave for while to this: Γ `Σ m1 ∼ bool Γ `Σ m2 ∼ τ Γ `Σ while m1 m2 ∼ unit What changes, if any, would we need to make to (our) dynamics or (your) statics in order to preserve progress and preservation? Solution: Dynamics: the last rule must be changed to accept any value, not just hi, or progress will no longer hold. Statics: the whilebody frame typing rule must be changed to accept a τ type instead of a unit, or preservation will fail to hold.

15–312 Final

12 of 18

May 6, 2014

One reason we might want to make while loops primitive is so that we can introduce the additional commands break and continue. If, during evaluation, we reach a break or continue, we immediately exit or restart (respectively) the innermost while loop whose body contains that break or continue. We will implement these commands in terms of two new abstract machine states, s ::= . . . | ( k J break ) | ( k J continue ). `Σ k : τ

`Σ k : τ

( k J break ) okΣ

( k J continue ) okΣ

( k B break ) 7− → ( k J break ) Σ

( k B continue ) 7− → ( k J continue ) Σ

A loop will only catch a break or continue if it is evaluating the loop body, so whilecond frames are passed through: ( k; whilecond(m1 ; m2 ) J break ) 7− → ( k J break ) Σ

( k; whilecond(m1 ; m2 ) J continue ) 7− → ( k J continue ) Σ

(d) (8 points) Give the remaining dynamic semantics for break and continue. Make sure to account for the frame you defined in part (a). Solution: ( k; bndf(x.m) J break ) 7− → ( k J break ) Σ

( k; bndf(x.m) J continue ) 7− → ( k J continue ) Σ

( k; whilebody(m1 ; m2 ) J break ) 7− → ( k C hi ) Σ

( k; whilebody(m1 ; m2 ) J continue ) 7− → ( k; whilecond(m1 ; m2 ) B m1 ) Σ

(e) (4 points) Do the rules for ( k J break ) okΣ and ( k J continue ) okΣ require the k : τ premise? Why or why not? Solution: The premises are absolutely necessary; otherwise preservation could easily fail in the last two rules in part (d).

15–312 Final

13 of 18

May 6, 2014

(This page merely summarizes material presented in lecture and recitation. The question is on the next page.) In Concurrent Algol, there was one really annoying judgment: α

m= ⇒ νΣ0 {m0 k P } Σ

One way to diagnose the complexity of this judgment was that, in in-lecture and in-PFPL formulation of concurrent Algol, a single step of the command m might do one of five things: • Synchronize on an evaluated event, a value of type event(τ ), with the action α using α the judgment e = ⇒ m, and produce a new command: Σ

α

e= ⇒m

e valΣ

Σ

α

sync(e) = ⇒ ν{m k stop} Σ

• Spawn a new process: 

spawn(cmd(m)) = ⇒ ν{ret hi k proc(m)} Σ

• Generate a new channel: 

newchn[τ ] = ⇒ νa ∼ τ {ret (chan[a]) k stop} Σ

• It may take an uneventful step, doing none of the three previous actions: e 7− → e0

e 7− → e0

Σ



ret e = ⇒ ν{ret Σ

Σ

e0



bnd x ← e; m = ⇒ ν{bnd x ← e0 ; m k stop}

k stop}

Σ

e 7− → e0

e valΣ

Σ



bnd x ← cmd(ret e); m = ⇒ ν{[e/x]m k stop} Σ



spawn(e) = ⇒ ν{spawn(e0 ) k stop} Σ

e 7− → e0 Σ



sync(e) = ⇒ ν{sync(e0 ) k stop} Σ

• It may evaluate a sequenced command bnd x ← cmd(m); m0 , meaning that it has to deal with any of the four above possibilities happening in the first command: α

m= ⇒ νΣ0 {m0 k P }

bnd x ←

Σ α 00 cmd(m); m = ⇒ Σ

νΣ0 {bnd x ← cmd(m0 ); m00 k P }

This judgment then interacted with the transition rules for the process calculus (the defiα nition of the judgment P 7− → P 0 ) at a single point: Σ

α

m= ⇒ νΣ0 {m0 k P } Σ α

proc(m) 7− → νΣ0 {proc(m0 ) k P } Σ

15–312 Final

14 of 18

May 6, 2014

α

By using control stacks, we can get rid of the judgment m = ⇒ νΣ0 {m0 k P } and take Σ

advantage of the fact that, while many different things might happen when we evaluate a command, at most one interesting thing (spawning a process, synchronizing on an event, creating a channel) ever happens at a time. Instead of atomic processes having the form proc(m), they will have the form proc(s) in our reformulation: that is, either the form proc( k B m ) or proc( k C v ). s 7− → s0 Σ 



proc(s) 7− → proc(s0 )

proc(  C v ) 7− → stop

Σ

Σ

α

α

P 7−−−−→ P 0

→ P10 P1 7−

`Σ α action

Σ,a∼τ

Σ α

α

→ P10 k P2 P1 k P2 7−

νa ∼ τ {P } 7− → νa ∼ τ {P }

Σ

Σ

Each Concurrent Algol feature, like sync(e), can then be defined with a mix of abstract machine rules that derive judgments of the form s 7− → s0 and process calculus rules that Σ

α

derive judgments of the form proc(s) 7− → P. Σ

e 7− → e0

e valΣ

Σ

( k B sync(e) ) 7− →( kB Σ

sync(e0 )

α

e= ⇒m Σ α

proc( k B sync(e) ) 7− → proc( k B m )

)

Σ

The abstract machine dynamics we already gave for ret e and bnd x ← e; m are sufficient; we don’t need any additional rules for these features. (f) (9 points) Give the dynamics of spawn(e) and newchn[τ ]. Solution: e 7− → e0 Σ

( k B spawn(e) ) 7− → ( k B spawn(e0 ) ) Σ

α

proc( k B spawn(cmd(m)) ) 7− → proc( k C hi ) k proc(  B m ) Σ

α

proc( k B newchn[τ ] ) 7− → νa ∼ τ {proc( k C chan[a] )} Σ

15–312 Final

15 of 18

May 6, 2014