## 15-816 Linear Logic |

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 typesType 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 asULam (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.

- 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.

Frank Pfenning fp@cs