# 15-816 Linear Logic

## Assignment 7: Linear Functional Programming

In this homework we implement the core of a linear functional programming language. This implementation may later be used to check derivations produced by our automated theorem prover. You are encouraged to collaborate in groups up to 3 students and hand in one joint implementation.

• ctx.sig
• ctx.fun These provide the signature and functor defining polymorphic contexts. You should feel free to extend them with generic operations which are useful in your other modules.
• type.sig
• type.fun These provide the interface and implementation for types. Just as the definition of terms later, they employ an implementation technique for bound variables called de Bruijn indices. Simply put, an occurrence of a variable is implemented as a pointer to the place where it is bound. This pointer is generally a positive natural number, counting the binders from the inside out. Here are two examples from the language of types

```  Type                  Representation in ML
mu a. 1 + a           Mu (Plus (One, Var 1))
mu a.mu b.1 + a * b   Mu (Mu (Plus (One, Tensor (Var 2, Var 1))))
```

This representation requires that substitution keeps track of variables, which is quite tricky in general. Fortunately, we only need to substitute closed types for type variables (and later closed terms for term variables), which is much easier than the general case. Still, we need to count how many abstractions we have traversed in order to make sure we are substituting for the right variable. The substitution function for types is provided as an example.

• lterm.sig
• lterm.fun These provide the interface and partial implementation of linear lambda terms. These also use de Bruijn indices, with the added twist that one construct (the elimination rule for tensor) binds two variables, the first one of which is labelled 2 the second 1. In other words, they are always numbered in order of occurrence, going through the term from the inside out. Since there is only one form of variables, unrestricted and linear variables should be numbered consistently, which means that a context contains a mixture of unrestricted and linear type declarations. This is a departure from the representation used in the linear theorem prover. For example,
```  lam u. let w1 # w2 = u in w2 # w1 # u
```
would be represented as
```  ULam (LetTensor (Var 1, Tensor (Var 1, Tensor (Var 2, Var 3))))
```

An additional simplification in the representation is afforded by the omission of types in most places (see type checking below), and the addition of a new construct, let name u : A = M in N. Here are the typing and evaluation rules.

```G ; . |- M : A    (G,u:A) ; D |- N : C
--------------------------------------
G ; D |- let name u : A = M in N : C

[M/u] N  ==>  v
---------------------------------
let name u : A = M in N  ==>  v
```
• check.sig
• check.fun These implement the type checker; only the signature is provided. You are asked to write a bi-directional type checker. The basic idea is quite simple, following the definition of normal and atomic forms for natural deduction. While checking each normal term, we assume we are given the type to check against, while for each atomic term we synthesize the type. During the whole checking process we work with the assumption that the type of each variable in the context is uniquely determined. This is then combined with the context management presented in the handout on linear type checking.

It is remarkable that this simple strategy allows us to type check any normal form with minimal type annotations: we only need the types of the variables in the environment, but no labels on abstractions or injections, for example. For terms which are not in normal form, we introduce explicit definitions and label the name of the defined variable. This is the purpose of the let name construct. It is also possible to define a corresponding linear definition construct. The fixpoint construct in this context is viewed as an introduction construct, which means its type label may be omitted as well, while let name u:A = M in N is considered atomic if N is atomic and M in normal.

In the signature top level, we have specifications

```  exception Error of string
val check : Lambda.term * Type.tp -> unit
val infer : Lambda.term -> Type.tp
```

which check a normal term against a type or infer the type of an atomic term, starting from the empty context. Errors during type checking are signalled by raising the Error exception. The kinds of errors which may arise are:

• term not normal or atomic,
• term not closed,
• type mismatch,
• linearity restriction violated.
Your checker need not sort out the various error message, but it should report some error in all anomalous situations instead of raising some random uncaught exception or (worse) accepting the term.

• eval.sig
• eval.fun These implement evaluation and only a fragment is given. Evaluation may presume the term is well-typed in the empty context (that is, contain no free variables). You may proceed simply by reduction and substitution as specified in the handout on linear functional programming or you may consider a semantics with explicit environments which builds closures. In any case, the type of values should not be directly observable at the level of ML.

Your function print should only print legal values and then only whatever should be observable. For example, values of functional type or additive pairs should only be shown as an underscore or in the form lam _ and <_,_>, respectively.

[ home | schedule | assignments | languages | handouts | overview | links ]

Frank Pfenning
fp@cs