Homework 8.0

Pi calculus

Please submit homeworks via the new submission page.

In this homework, you’ll implement an interpreter for the typed pi calculus along with several encodings in the typed pi calculus.

I’ve provided starter code; it will unzip a variety of files into a directory hw08. Please edit those files and resubmit them in a zipfile. Any edits you make to the *Main.hs files will be overwritten with my test harness.

Please update README.md with collaborator info.

I will grade your homework by unzipping the zipfile and running ‘cd hw08; make clean; make’. If that doesn’t work and the cause is something that is not my fault (like needing to install library you used), you will get a zero. This time you should make sure your files are in a hw08 directory, just like my zipfile provides for you. (That’s the standard form of interchange.)

Overview of tasks

There are five places in the homework marked with the words TASK!. Those are the parts you have to do. They are:

  1. Implement an evaluator for the typed pi calculus (Pi.hs)
  2. Implement a type checker for the typed pi calculus (Pi.hs)
  3. Compile boolean expressions to the typed pi calculus (BoolExp.hs)
  4. Compile boolean environments to the typed pi calculus (BoolExp.hs)
  5. Compile the linear lambda calculus to the typed pi calculus (Lam.hs)

You should perform the tasks in that order. Some tests have been provided for you—make will build some testers (PiMain, BoolMain, and LamMain). You’ll want to write some of your own tests, though—in particular, part (2) doesn’t get tested until part (5).

Finally, I’ve left open room for extra credit.

Pi calculus

In Pi.hs, you have an AST for the typed pi calculus. Here’s a grammar:

c is a channel name
x is a variable

Types
t ::= Chan t
    | t1 * ... * tn

Expressions
e ::= x | c | (e1, ..., en)

Values
v ::= c | (v1, ..., vn)

Patterns
pat ::= x | _ | (pat1, ..., patn)

Pi calculus processes
p, q ::= 0 | p|q | new x:t. p | send e1(e2) | rec e(pat). p | rec! e(pat). p

Note that channel names are just abstract symbols, drawn from some infinite set. Expressions are nothing but variables, channels, and tuples (where n can be zero). Values are a subset of expressions: channels and tuples of zero or more values. We have patterns, written pat, for analyzing values that arrive over channels; _ is the wildcard “don’t care” pattern.

Channels are typed; we use new to create channels; send outputs over a channel, while rec and rec! receive input once or infinitely many times, respectively.

Operational semantics

When a value is sent along a channel from a sender to a receiver, the receiver decomposes the incoming value with a pattern. Matching a value against a pattern generates a substitution, Θ, which substitutes values for zero or more values. For example, - is the substitution that does nothing, while Θ = [x |-> c1][y |-> ()] substitutes the channel c1 for x and the empty tuple () for y. We can give the pattern matching formal semantics like so, writing v ~ p |> Θ to mean that matching v against p generates the substitution Θ.

Substitutions
Θ ::= - | Θ[x |-> v]

Pattern matching

----------
v ~ _ |> -

-------------------
v ~ x |> -[x |-> v]

v1 ~ pat1 |> Θ1     ...     vn ~ patn |> Θn
-------------------------------------------- where each Θ has a disjoint domain
(v1, ..., vn) ~ (pat1, ..., patn) |> Θ1...Θn

We apply substitutions to expressions by writing Θ(e) and to processes by writing Θ(p).

I’ll give here a single-step operational semantics for pi calculus processes using substitution; in the homework, you’ll define a big-step evaluator using environments.

Channels (set)
C ::= {c1, ..., cn}

Running processes (list)
R ::= [p1, ..., pm]

Machine state
M ::= <C; R>

Operational semantics

---------------------------------------------------------------------------
<C; [p1, ..., pi-1, 0, pi+1, ..., pn]> --> <C; [p1, ..., pi, pi+1 ..., pn]>

------------------------------------------------------------------------------------
<C; [p1, ..., pi-1, (p|q), pi+1, ..., pn]> --> <C; [p1, ..., pi, p, q pi+1 ..., pn]>

q = new x:t. p    q in R    c not in C
-------------------------------------------
<C; R> --> <C U {c}; (R \ [q]) ++ [p[c/x]]>

q = send c(v)    p = rec c(pat). p'    q, p in R    v ~ pat |> Θ
----------------------------------------------------------------
<C; R> --> <C; (R \ [q, p]) ++ [Θp']>

q = send c(v)    p = rec! c(pat). p'    q, p in R    v ~ pat |> Θ
-----------------------------------------------------------------
<C; R> --> <C; (R \ [q]) ++ [Θp']>

Your first task is to implement this semantics. You should write a concurrent interpreter for the pi calculus, using Haskell’s threads to implement pi calculus processes and Haskell’s channels to implement pi calculus channels. You’ll want to look at Concurrent.hs for some helpful functions (parallel, in particular) and Channels.hs for examples of how to use MVars, Haskell’s version of channels. Be sure to check out Control.Concurrent.Chan to figure out how to use Haskell’s channels.

Do not change any of the AST definitions.

Type system

The pi calculus has many variants, typed and untyped. After having written your interpreter, you’ll write a type checker for the pi calculus. There are three judgments you’ll need: typing for expressions (Γ |- e : t, read e has type t under Γ), for patterns (|- pat : t |> Γ, read pat matches type t producing Γ), and for pi calculus processes (Γ |- p, read p is well formed under Γ). We’ll give you the first two, but you have to come up with the last one.

Contexts
Γ ::= . | Γ,x:t

Expression typing

Γ(x) = t
----------
Γ |- x : t

Γ |- e1 : t1    ...    Γ |- en : tn
-----------------------------------
Γ |- (e1, ..., en) : t1 * ... * tn

Pattern typing

-------------
|- _ : t |> -

-----------------
|- x : t |> -,x:t

|- pat1 : t1 |> Γ1    ...    |- patn : tn |> Γn
-------------------------------------------------
|- (pat1, ..., patn) : t1 * ... * tn |> Γ1,...,Γn

The typing rules for expressions don’t mention channels—because our pi calculus AST doesn’t allow us to ever write a “channel” down.

First, write down your typing rules for the pi calculus in theory.txt. Then implement typeExp, typePat, and check. Be sure to test these functions now!

Encoding booleans

Just as we wrote Church numerals in the lambda calculus, the pi calculus admits an encoding of boolean expressions. We’ll use the following language:

Boolean expressions
B ::= x | true | false | B1 && B2 | B1 || B2 | not B

I’ve defined an AST for you in BoolExp.hs. Note that (:&&:) and (:||:) are infix constructor names, like (:). Neato! To give meaning to variables, we’ll use environments mapping variable names to boolean values.

You should implement two functions: compileBExp and compileBExpEnv. The Haskell expression compileBExp tchan fchan b should compile the boolean expression b to a pi calculus process such that if b is true, it will send a value on tchan, but if it’s false it will send a value on fchan. The Haskell expression compileBExpEnv env p should take an environment (mapping variables to booleans) and generate appropriate “servers” for those variables. Look at startBool for a sense of how the plumbing works, but it’s largely up to you to decide what protocol your boolean servers will use.

Hint: the most challenging part is figuring out what to do for expression where a variable is used more than once, as x is in (x || z) && (x || y).

Use BoolMain to test your code.

Encoding linear lambda calculus

The linear lambda calculus is a restriction of the typed lambda calculus: each variable must be used exactly once. It’s out of scope for the course to study why the linear lambda calculus is interesting; your job will be encoding it into the pi calculus.

I’ve tried to make your job simpler: I’ve given you an AST for the lambda calculus along with a standard and a linear type checker. You just have to translate the linear lambda calculus into pi calculus. You’ll be able to use the fact that each variable gets used exactly once in your translation.

Your job is to write just one function, compileLam. It takes (a) a name generator (of type IO Name), so you can come up with fresh channels; (b) an output channel, (c) a typing context, and (d) a well typed linear lambda calculus term. It outputs (a) a lambda calculus type, and (b) a pi calculus process.

If compileLam fresh n Γ e yields (t, p) then we should have it that typeOf Γ e == Right t and if e evaluates to some value v, then the pi calculus process p sends (a translation of) v as its “result” on n. That is, the “result channel” n is where the lambda calculus term returns to.

What types of values are communicated on these “result channels”? In general, when thinking about compilers, it’s useful to think first about translating types and later about how translating expressions. Translating the linear lambda calculus to the pi calculus, we’re able to directly give a translation function, typeTrans, which “compiles” the lambda calculus types to the pi calculus types that will appear on the associated result channel.

If the lambda expression e computes a unit value, then the process that implements e will return the empty tuple, (), on its result channel. On the other hand, if the lambda expression e computes a function value with type t1->t2, then the process that implements e will return a pair of channels (i, o) on its result channel. The first channel i is the argument or input channel—to use the function, send your argument on the channel i to the process implementing the function. The second channel, o, is the output channel. When the function completes, it will return its result on the the output channel.

With these ideas in mind, it’s up to you to implement the function compileLam in Lam.hs. You should structure your function `compileLam similarly to the way you would structure a type checker for the lambda calculus, going by cases on the lambda calculus AST.

Use LamMain to test your code. If you’re generating ill-typed terms, is the problem in your compiler or in your type system?

As always, feel free to clarify these instructions or ask questions on Piazza or in person with me, Eric, or Patrick.

Extra credit

You should this “extra credit” mostly for fun. If you’re on the line between grades, it might edge you over. But most likely it’ll have no effect at all, so don’t feel obligated—do it if it’s fun, don’t if it’s not.

Note: whatever extra work you do, please clearly delineate it separately from Pi.hs, Bool.hs, and Lam.hs. If you overwrite the basic tasks with something fancy, I won’t know and the auto-grader will deduct points. So leave a note in README.md and start a new subdirectory.

Here are some suggestions. Some of these are hard—or may be technically impossible without a slight change of assumptions. Feel free to talk to me about them, but only after you’ve done the rest.

  • Compile pi calculus with tuples into pi calculus without tuples.

  • Add natural number expressions; print out your numbers in, e.g., unary.

  • Encode a simple imperative language, with variables, assignments, and sequencing. Add conditionals. Add loops (but watch out for the timeout in Concurrent.hs).

  • Translate arbitrary (typed or untyped) lambda calculus terms into the pi calculus.

  • Something else!

Credits

This homework is directly based on a homework from COS 510 at Princeton, courtesy of Professor Dave Walker. Thanks, Dave!