Translating a Subset of OCaml into System F. Jonatha Protzenk under the dire ion of François Po ier

14 

Loading....

Loading....

Loading....

Loading....

Loading....

Full text

(1)

Translating a Subset of OCaml into System F

Jonatha

Protzenk

under the dire

ion of François Po ier

(2)

I.

Some background

1. Motivations

Contents

I Some background . . . 2

1 Motivations . . . 2

2 A high-level description of our translator . . . . 3

II Constraint solving and decorated ASTs . . . 4

1 Constraint-based type inference . . . 4

2 Generating an explicitly-typed term . . . 5

a. Encoding the term in the syntax of constraints 5 b. Digging holes in terms . . . 6

c. What are the slots? . . . 6

d. The target language: CamlX . . . 7

e. About this method . . . 7

3 Verifying our work . . . 7

III System F and coercions… . . . 8

1 An overview of the translation . . . 8

a. Our version of System F . . . 8

b. The steps to System F . . . 8

2 Generating coercions . . . 8

a. The core issue . . . 8

b. What is a coercion? . . . 9 c. Coercions in patterns . . . 9 d. A set of rules . . . 10 e. More on coercions . . . 10 3 Other simplifications . . . 10 a. Uniqueness of identifiers . . . 10 b. Desugaring . . . 11

?

Removinglet-patterns . . . 11

?

Removingfunction . . . 11

c. Bonus features . . . 11

4 An example . . . 12

a. Original OCaml program . . . 12

b. The decorated AST (CamlX) . . . 12

c. The core AST . . . 12

IV A reflexion on this work . . . 13

1 Future improvements . . . 13

a. Actually writing the type-checker . . . 13

b. Enriching the language . . . 13

2 Related work . . . 13

a. Modules . . . 13

b. A real compiler . . . 13

c. More modern features . . . 13

3 Conclusion . . . 13

V Bibliography . . . 14

Part I

Some background

Type-checking functional languages à la ML is a long-time topic that has been well-studied throughout the past 30 years ([GMM+78, DM82]). More recently, there has been a surge of interest regarding the translation of rich, complex, higher languages into simpler, core lan-guages.

These core languages are well-typed, and the combina-tion of simplicity and rich type informacombina-tion makes them well-suited for program analysis, program transforma-tion and compilatransforma-tion.

Indeed, Haskell now features “System FC” [SCJD07], a core language that is both minimalistic and powerful. We applied the same design principles to a translation from OCaml [LDG+10] to a variant of SystemF

η. System Fη is System F with coercions [Mit88]; we augmented it with some coercions of our own. We call this language “FE+”, as in “SystemFη plus” extra coercions.

Our contribution is twofold: firstly, a strategy that builds upon previous work by François Pottier et. al. [PR05] and leverages constraint solving to build an explic-itly typed representation of the original program; sec-ondly, a translation process that makes all the subtyp-ing relations explicit and transforms the original, fully-featured program into a SystemFηterm, where all coer-cions have been made explicit, all redundant constructs eliminated, and well-typedness preserved. This final rep-resentation can be type-checked before going any fur-ther, to ensure the consistency of the process.

This report is structured as follows. In the next para-graphs, we briefly describe how our translation process works, introduce the main concepts and our original mo-tivation. The following part is devoted to our first con-tribution: adapting constraint generation to build a dec-orated term that corresponds to the original program with full type annotations. Part 3 is all about our trans-lation process from the decorated AST to a simpler, core, fully-explicit System F term. Finally, we reflect on our work and possible extensions.

1

Motivations

OCaml is an old, feature-rich language. It features many extensions to the original ML language, such as

• a full module system, including first-class modules, recursive modules, functors,

• structural objects, with polymorphic methods, classes, class types,

• polymorphic variants, with private types, • and a few more “dark corners”.

Even if we consider a small subset of the language, some features are somehow unusual: for instance, OCaml tries to generalize identifiersinsidelet-patterns, unlike Haskell.

(3)

2. A high-level description of our translator

I.

Some background

. . . .ml . . OCaml parse tree . . unifier . . Constraint . . CamlX “with holes” . solver . . CamlX . . FE+ . type checker .

Rest of the compilation process… . Pre-allocate solver struc-tures . We reuse the OCaml fron-tend . Generate a set of constraints . Remove union-find structures, switch to De Bruijn for types

.

Remove syntac-tic sugar, gen-erate coercions, use atoms for terms .

Fill holes with union-find in-formation

Figure 1: The overall structure of our translator

The current trend is to prove as much as possible of the compilation process [L+04]. Indeed, it is difficult to fully trust OCaml, because of its many features and possibly complex interactions between all the extra features.

Garrigue has been working in this direction ([Gar]) and has successfully extracted a program from a Coq proof that runs a small subset of OCaml, but it seems hard to prove a full, real-world type inferencer. Specifying and proving a union-find algorithm has been done be-fore, but specifying and proving a whole type-inferencer is another order of magnitude.

To strike a middle ground,we propose to translate any OCaml program into an equivalent program written in a System F-like core language.

The idea is to decompose complex, “hard-to-trust” fea-tures into simpler, core building blocks. As an example, the value restrictionà laGarrigue [Gar04] relies on sub-typing. If a type variable only appears in covariant po-sition, than it can be safely replaced with any of its su-pertypes. Garrigue introduces a supertype called zero,

which then can be subtyped into ’a wherever needed. This is precisely the kind of operations we would like to translate in an explicit manner, with very basic con-structs, so that the type-checker can later assert the cor-rectness of what we did.

Ultimately, what we want to do is, instead of proving the well-typedness of a program written in full OCaml, is type-checka posteriorian equivalent,coreprogram. And because we trust this core language, we can be sure that the program we are about to compile is, if not semanti-cally correct, at least type-sound and won’t crash.

The motto is, to quote Milner [Mil78], “Well-typed pro-grams can’t go wrong”, and our aim is to enforce that.

2

A high-level description of our translator

Naturally, the trade-off is that as expressions become simpler in the “FE+” language, types become possibly more complex. However, this is not considered to be a problem, and although we end up with complex types, the type-checker forFη remains simple and elegant.

(4)

II.

Constraint solving and decorated ASTs

1. Constraint-based type inference Our process is made of the following steps (in

chrono-logical order) :

• we lex and parse the original OCaml source code, in order to obtain a parse tree;

• we generate constraints, and build an AST decorated with empty “slots”: this is the CamlX (as in “eXplicit Caml”) term “with holes”;

• we solve the constraints; as a side-effect, this fills the empty “slots” in the CamlX term with relevant type information;

• we translate the “impure”, decorated CamlX term into a pure CamlX term where “slots” have disap-peared and types are now represented using De Bruijn indices;

• we desugar this complex CamlX AST into a FE+ one, and generate coercions to justify some steps on-the-fly;

• we type-check the resulting FE+ term and ensure well-typedness before going further.

Diagram 1 on the preceding page gives a graphical overview of the whole process. We will detail these steps in more detail.

We decided to reuse the OCaml parser and lexer for our tool. Because parsing OCaml is painful1, and because we needed to compare the output of our tool with OCaml’s, we decided to just use OCaml’s parsing/ subdirectory. All the other pieces of code in our tool are original work.

1We’re thinking about the numerous parsing subtleties: let x, y

=instead oflet (x, y) =, and many more.

Part II

Constraint solving and

decorated ASTs

The first requirement for our translation process to work properly is to build a full AST of the original pro-gram, with explicit type annotations. This is the CamlX AST. Type annotations are indeed required: given that type inference in System F is undecidable, the only oppor-tunity we have to perform type inference is when we’re still in ML. Once we have that explicit type annotation, we can send our program into System F, Curry-style, and then perform type-checking only.

We did not reuse the original OCaml further. In-deed, one of the side goals of this work was to show that constraint-based type-inferencing could be applied to OCaml, and possibly build a minimalistic replacement for the type-checker of OCaml. We are currently far from that goal, but fundamentally, the aim was to try some-thing new, which is why we did not hack the OCaml typer2.

Another reason for not reusing OCaml’s type-inferencer is that the process of generating a fully an-notated AST is closely linked to the way the type-inferencer works. We had no guarantee that OCaml’s type-inferencer was well-suited for this task, which is why it seemed better to us to start from scratch for the type-inferencer.

In our tool, the part that performs type inferencing corresponds roughly to the left half of figure 1. We per-form type inferencing using constraints [PR05]: the straint generator walks the AST to generate a set of con-straints, and another component, called thesolver, runs a second pass to solve the constraints. The final result is an environment with all the top-level bindings and their types, or an error if the program cannot be type-checked. The output of the constraint solver is nothing like a fully annotated AST; hence, some work was needed to adapt the constraint solving process, and make it gen-erate a fully annotated AST. This is the first part of our work: adapting constraint-solving to our needs.

1 Constraint-based type inference

The main advantage of constraint-based type inference is the clean, elegant separation between constraint gen-eration and constraint solving. Whereas traditional uni-fication algorithms generate constraints and solve them on-the-fly, constraint-based type inference first generates constraints and then solves them in a separate pass. The key component that makes it possible is thelet-binding on type variables.

This design allows one to change the semantics of the original program without ever touching the solver. For instance, thematchis not generalizing in OCaml. Turn-ing it into a generalizTurn-ing matchwas simply a matter of

2Moreover, the OCaml type-inferencer is known to be hard to tackle,

so as our goal was to build a prototype, it seemed reasonable to usnot to try to reuse it.

(5)

2. Generating an explicitly-typed term

II.

Constraint solving and decorated ASTs

changing a few lines in the constraint generator to insert alet-constraint in the right place. This clean separation is a feature we have tried to keep in our tool.

The reference paper for constraint-based type infer-ence is [PR05]. The whole process is described in great detail, and this is what our implementation used as a ref-erence. Without giving too much detail, let us just show the syntax of constraints (figure 3 on the next page) and the rules for constraint generation (figure 4 on the fol-lowing page).

The issue with constraint-based type inference is that this process answers yes or no. It does not add any anno-tation to the original AST, it does not build another AST, it discards information as it goes, and when it reaches the end of the constraint solving process,the best it can do is print the inferred types for all the top-level bindings.

This is in contradiction with our goal, which is to build an explicitly typed representation of the original AST. The following section details how we dealt with this issue.

2 Generating an explicitly-typed term

The key question is: how do we obtain an annotated AST?One might think of examining the generated con-straints to rebuild the original AST. But because the syn-tax of constraints does not mirror that of the expressions, we have no way to recover the original term from a con-straint. For instance, afun x ->expression in the OCaml AST, amatch, aletall result in alet-constraint (see fig-ure 4 on the next page).

a. Encoding the term in the syntax of constraints

The syntax of constraints has a strong notion ofscope: a type variable is defined through alet-binding, and is only available below it. Similarly, type schemes are de-fined through alet-binding, and are available below this binding. The natural extension of this design is to include the expressions in this scope as well, so that we can

an-notate them with the right types.

That is to say, we want constraints and annotated ex-pressions to share the same typing scope.

In the original design from [PR05], the solver is an au-tomaton, with a set of rules that describe the process of solving a constraint. The state of the solver is a triple, which consists in astack, aunification environmentand a constraint. The final state is as follows:

¯X.[. . .];U;true

The first component is the environment with all the top-level bindings, U is the unification knowledge ob-tained so far, andtrueis the empty constraint, meaning there is nothing left to solve.

In this new scenario, the solver recursively solves con-straints and returns terms. For instance, when solving an application-constraint, the solver obtains two terms annotated with the right types, and returns an AST node that applies the first one to the second one. When solving an instance-constraint, it returns an identifier annotated with the right types.

That way, instead of just obtaining the type of the top-level bindings at the end of the process, we end up with:

¯X.[. . .];U;CT

WhereCT is the final constraint-as-a-term that corre-sponds to our annotated AST.

This idea felt natural for a number of reasons:

• although tedious, the formalization had clear se-mantics and scoping rules, and consisted in an ex-tension of the previous design from [PR05];

• the whole process, unlike figure 1 on page 3, was not split in two but linear: the decorated AST was obtained as the output of the solver;

e ::= expression: letp1:σ1=e1 and… andpn :σn=en ine let binding function |p1:τ→e1 |… |pn:τ→en function binding x[τ1,…, τn] instanciatex e e1…en application matche:σwith |p1:σ1→e1 |… |pn:σn →en pattern-matching

(e1,…, en) n-ary tuple

… x ::= identifier: s a string p ::= pattern: x identifier _ wildcard p1|p2 or pattern (p1, . . . , pn) tuple τ ::= type: F(τ1, . . . , τn) type constructor α a type variable (an integer)

σ ::= type scheme:

. τ universal quantification

(6)

II.

Constraint solving and decorated ASTs

2. Generating an explicitly-typed term σ ::= type scheme: ¯X[C].T C, D ::= constraint: true truth false falsity PT1. . .Tn predicate application C∧C conjunction ¯X.C existential quantification defx:σinC type scheme introduction

xT type scheme instantiation

C, D ::= Syntactic sugar for constraints:

. . . As before

σT Tis an instance ofσ letx:σinC def +xhas an instance

σ σhas an instance

defΓ inC as before

letΓ inC as before

Γ as before

Figure 3: Syntax of type schemes and constraints

Jx:TK = xT

z.t:TK = X1X2.(letz:X1inJt:X2K∧X1→X2T)

Jt1t2:TK = X2.(Jt1:X2TK∧Jt2:X2K)

Jlet z=t1in t2:TK = letz:X[Jt1:XK].XinJt2:TK Figure 4: Constraint generation

• this allows the solver tohide its internal data struc-tures and never reveal union-find implementation details to the outside.

Figure 5 on the next page develops this idea and describes a tentative syntax for these “extended con-straints”. The underlined constraints are those that gen-erate a term.

However, this solution was discarded. The main reason was that while attractive in theory, this design had no simple implementation. The number of term-constraints kept growing, because as we recognized more features, we had to extend the syntax of constraints. This led us to abandon this design, and we chose a more tractable process instead.

b. Digging holes in terms

This new idea is the one we implemented in our tool. While we have been unable to come up with a formal description of it, we believe it to be well thought-out and, in our experience, easy to implement.

The basic idea is for the constraint generator to pre-allocate solver structures3: for instance, when generat-ing the constraints for afun-binding, we pre-allocate a slotσfor the upcoming scheme of the argument, we cre-ate alet-constraint with a pointer to this slot, and we create afun-expression with a pointer to theexact same slot.

The difference with our previous approach is that now the decorated AST and the constraint tree are separate structures. They are generated in parallel but are distinct entities; they only share pointers to common structures (see figure 6 on the facing page).

Right after the constraint has been generated, the “slots” σ are empty, because the constraint generator

3In the implementation, the constraint generator is a functor that is

parameterized over the solver types and some solver helpers to allocate new “slots”.

only allocated them. However, as figure 6 on the next page shows, the type schemeσissharedby the constraint and the CamlX AST “with holes”.

What happens next is solving: the driver feeds the solver with the constraint. When the solver encounters a

let-constraint, it solves it, generalizes variables as it can, and then fills the slot with all the information gathered at thislet.

We are voluntarily vague regarding the contents of this slot – this is the topic of the next section. Let us just say, for the moment, that because the slot is mutable, once the constraint solving is done, and although the solver never heard about the term with “holes”, the holes are now filled with all the information required.

c. What are the slots?

The slots are mutable structures that are shared be-tween the constraint and the CamlX term “with holes”. The solver fills them in one side, in the constraint. They end up on the other side, in the CamlX AST. But what do they contain exactly?

There are two kinds of slots that decorate the AST: • instantiation slots, that describe all the variables

used to instantiate a type scheme;

• type scheme slots: this can be the type scheme of a whole pattern, as inlet (x, y) = f e1 e2, or only the type scheme associated tox.

These two kinds of slots are attached to different nodes in the AST and in the constraint, as is shown in the table below.

Kind of slot Attached to… (in the CamlX term)

Attached to… (in the constraints) type scheme let, fun,

func-tion,match

let instance instance instance

(7)

3. Verifying our work

II.

Constraint solving and decorated ASTs

Constraint Decorated AST

letX[C1] :z| {z }7X σ inC2 letz:σ=E1inE2 X1X2.(let z| {z }7X1 σ inC1∧X1→X2T) fun(z:σ)→E1 … …

(σallows one to recover the union-find structures forz) Figure 6: Sample output of our modified constraint gen-erator

To understand why we do this, one must think of what is needed to build an explicitly typed FE+ term. We will need type annotations inλ-bindings. We will also need to coerce patterns atlet-bindings and function-bindings. We might need to coerce generalizing matches as well (more on this later).

In order to properly generate all the coercions and type annotations, the translator must access as much type in-formation as it can, which is why we decorate the AST. The next part is all about this translation process. d. The target language: CamlX

This method allows us to build an explicitly typed “CamlX” AST. Its full description can be found in figure 2 on page 5. It is very close to the original OCaml language, except that it is decorated with type annotations. Coer-cions are implicit in this language, and theΛs are explic-itly counted. That is to say, at the level oflet-bindings and matches, we keep track of the variables that have been generalized.

The implementation details vary between the rep-resentation “with holes” and the reprep-resentation with “clean” De Bruijn types: the data types are different, but the same information is conveyed.

e. About this method

This method has proved to be easy to implement and work with, although it lacks some formal clarity. When used carefully, it provides a very flexible way to forward some information to further parts of the translation pro-cess.

The trick is simply to think of what will be needed for the next steps, pre-allocate it, and then store it some-where in the CamlX term, so that the next passes can find it and use it.

3

Verifying our work

Rewriting a type-inferencer and adapting it to our needs is a rather big task, and implementing it properly requires some care. To make sure our constraint gener-ator and solver were reliable, we developed a series of tests to check both the inferred types and the generated constraints.

The authors of [PR05] developed a prototype imple-mentation of their work, calledmini. This prototype has a constraint parsing facility, which we took advantage of. One of the first series of test we ran consisted in pretty-printing the constraint tree, and having the pro-totype implementation solve it. This allowed us to spot a few bugs inmini, but also to ensure our results were correct.

Another series of tests took advantage of the original OCaml. After the solver does his job, we obtain an en-vironment with all the top-levellet-bindings and their types in it. We wrote a quick parser for the output of

ocamlc -i4, and we have a series of tests that compares the output of our solver with that of OCaml5.

A complexity test is also featured. Boris Yakobowski wrote a prototype implementation of MLF [RY08], that was supposedly faster than the prototype implementa-tion for [PR05]. We compared our tool6 with the two others, and we managed to confirm the issue reported by B. Yakobowski and fix the corresponding complexity bug inmini.

4This prints the inferred signature of a compilation unit. 5make testsin the working directory will run the series of tests. 6make benchs, assuming you have the relevant packages, should

plot some nice data

C ::= constraint:

C∧C conjunction

¯X.C existential quantification

xT type scheme instantiation

T=T type equality

as before

CT ::= constraint with term:

¯X.CT existential quantification CT∧C mix with a regular constraint C∧CT mix with a regular constraint

x identifier

xT instance

λz.T. CT function

CT CT application

letz:X[CT].XinCT let-binding Figure 5: Alternative encoding of expressions in constraints

(8)

III.

System F and coercions…

2. Generating coercions

Part III

System F and coercions…

System F [Rey74, Gir72] was introduced at the begin-ning of the ’70s. Since then, many extensions have been derived: SystemF<:(“System F-sub”) with delimited poly-morphism, SystemFµfor recursive types, SystemFωwith functions from types to types, and SystemFη [Mit88].

The previous part was all about running type inference on the original OCaml program, and building a decorated CamlX AST with all the required type information. This part is about translating the CamlX AST into the FE+ lan-guage, close to SystemFη, which is described in figure 7 on the next page.

Our translation is targeting theFη language. Fω and Fµ might be relevant later on for us, but the subset of OCaml we have chosen has not required more power thanFη.

1 An overview of the translation

a. Our version of System F

We’re using our own flavour of SystemFη, called “FE+”, and there are a few important features.

• We’re using patterns, and these patterns are used only inmatches.

• We’re using coercions inside patterns – this will be discussed later on.

• The only type annotations are at λ-bindings: the type of the arguments is provided. The types of the arguments do not use: indeed, sincewe’re in ML, the arguments cannot have polymorphic types. • τ1 → τ2 is understood to be the application of the

“arrow” type constructor; the same goes for prod-uct types. Constant types (int, unit, …) are type constructors with arity 0.

• Types are represented using De Bruijn indices, which is why type variables are integers.

b. The steps to System F

If one recalls the previous section, the output of the constraint generator is a term whose type information consists in filled “slots”, that is to say, union-find equiva-lence classes, with mutable structures and a lot of shar-ing. We cannot work with such a representation, which is why a first “cleanup” step is needed.

We’re using types represented as De Bruijn indices. That is, type variableirefers to thei-th enclosingΛabove the term. This is one of the available representations for types and felt natural in our case.

The first step transforms the union-find structures into these De Bruijn types. We still have a syntax of expres-sions (see figure 2 on page 5) that is more or less equiva-lent to that of the original OCaml AST. We’re not trying to desugar anything. Simply, we’re cleaning up the repre-sentation of types. This is the step that goes from CamlX “with holes” to CamlX in figure 1.

The second step is where all the work is actually per-formed. A number of OCaml constructs were redundant:

function,let … and,let pattern = …. We eliminate all those, and sometimes introduce temporary identifiers, or change the original expression a little bit. We also guar-antee that identifiers are globally unique in the result of this transformation; they’reatoms. The syntax of this “FE+” language is described in figure 7 on the next page. This step corresponds to the final arrow from CamlX to FE+ in figure 1.

Once we’ve obtained a SystemFη term, we can type-check this term to ensure the consistency before pro-ceeding with the rest of the compilation process.

2 Generating coercions

One feature of OCaml is that, as we said previously, patterns are generalizing. We discard the value restric-tion for the sake of clarity in the following examples. a. The core issue

Let us consider the following example.

let (l, f) = (fun x -> x)([], fun x -> x);; val l : ’a list = []

val f : ’a -> ’a = <fun>

If now think of F-terms, we’ll be matching the expres-sion

ΛΛ(Λ. λ(x:0). x) [(list[1], 0→0)] (Nil[1],(Λ. λ(x:0). x)[0]) against the pattern(l, f).

In order to assign type schemes to the identifiers of the pattern, we must compute the type of the expression above. We thus obtain:

∀∀(list[1], 0→0) that is to say,

αβ.(αlist, β→β)

The pattern-matching will fail because the pattern on the left-hand side has a type that starts with head symbol “tuple”, and the type on the right-hand side starts with an abstraction.

The following example is similar: # type ’a t = A of (’a -> ’a);; # let A f = A (fun x -> x);; val f : ’a -> ’a = <fun>

The expression on the right side of the equals sign has typeα.A(α→α): this pattern matching will also fail.

One might think about changing the expression into an equivalent one, so that its type is correct. For instance, one can translateΛ.A(λx.x)intoA(Λ.λx.x)to solve this issue.

This sounds like a good idea, but this is not applicable in situations such as the first example. The expression in the first example is anapplication, so there is no way we can push theΛinside the application, because this is not possiblesyntactically.

The conclusion is we must operate on types, and we needcoercions.

(9)

2. Generating coercions

III.

System F and coercions…

x ::= identifier:

a an atom

e ::= expression:

Λ.e type abstraction

e[τ] type application

eIc coercion application

λ(x:τ).e λ-abstraction

e1e2 application

letx=e1ine2 let-binding

x instanciation

matchewithp1→e1| . . . |pn→en match

(e1, . . . , en) n-ary tuple

… p ::= pattern: x identifier _ wildcard p1|p2 or pattern (p1, . . . , pn) tuple

pIc coerce when matching this pattern

c ::= coercion: id identity c1;c2 composition [τ] ∀elimination [c] ∀covariance ∀× ∀distributivity

×i[c] coercion projection on thei-th component

τ ::= type:

. τ universally quantified type F(τ1, . . . , τn) type constructor α a type variable (an integer)

Figure 7: Our syntax for FE+

b. What is a coercion?

The previous discussion shows there is a need for sub-typingin our system. We need a subtyping judgement: Γ `ττ0 that tells us thatτ0 is a subtype ofτ. More-over, we need to apply these subtyping judgements at some specific points in the expressions so that the type of an expressionecan be cast fromτtoτ0. This is where we use coercions.

A coercion is a witness for subtyping. We say that a subtyping judgement is witnessed by a coercion:

Γ `c:ττ0

For instance, the coercion (∀×) witnesses the distribu-tivity ofwith regard to tuples.

Γ `(∀×) :(τ1, . . . , τn)(τ1, . . . ,τn) The typing rule for a coercion thus becomes:

Γ `e:τ Γ `c:ττ0 Γ `(eIc) :τ0

Other subtyping rules are pretty standard and are de-scribed in figure 9 on page 11.

c. Coercions in patterns

One feature of OCaml is thator-patterns can be nested arbitrarily deep. The following piece of code is valid: type ’a t = ((’a -> ’a), (’a -> ’a) list);; let (_, (x, _) | (_, [x])) =

(* Some polymorphic expression with type (unit * ’a t) *) ;;

However, we cannot simply apply a coercion to the expression on the right-hand side of the equals sign: the

coercions needed on the left side and the right side of the or-pattern are not the same.

After applying∀×once, which states that we can push theinside the tuple, what will happen in the right side of the outermost pair is:

• The left side of the or-pattern will require (∀×), which states thatcan be distributed inside tuples once more.

• The right side will require(∀×); (×1[(list)])which states that

can be distributed inside the tuple of typet we can apply a coercion on the second branch

of the tuple,

can be pushed inside data typelist. In the end, we want to assign. 0→0tox.

One cannot simply add a or-coercion, that is, a coer-cion c1 | c2 that mirrors the or-pattern and distributes c1 and c2 on the left side and the right side of the or-pattern when matching. This is not powerful enough, because we decided to implement a generalizing match

(this is the topic of section 3 on the next page). As a consequence, there were some cases where the coer-cions needed for each branch of amatchconstruct were different. If the match only has one branch, then the or-coercion is enough. But if the match has different branches, one must choose a coercion for each branch of thematch.

In this scenario, the most reasonable option was to attach coercions to patterns. That way, e Ic becomes syntactic sugar forletxIc=einx. Applying a different coercion on each side of the or-pattern boils down to changing the pattern intop1Ic1|p2Ic2.

(10)

III.

System F and coercions…

3. Other simplifications J_, τK = _ Jz, τK = zIelim(τ) J(p1|p2), τK = (Jp1, τK|Jp2, τK) J(p1, . . . , pn), (τ1, . . . , τn)K = ( Jp1,τ1K, . . . ,Jpn,τnK ) Ipush() push(∀∀) = [push()]; ∀× push() = id elim(τ) = [elim(τ)]; []if0#τ elim(τ) = [elim(τ)]otherwise

elim(τ) = idwhenτ6=τ0 Figure 8: Coercion generation

d. A set of rules

What happens now is that, in the process of translating the CamlX term (see figure 1 on page 3), that has De Bruijn types but complex expressions, into System F, werewrite patterns to introduce coercions wherever needed.

Although the subtyping relation inFη is undecidable [Mit88], we have a deterministic procedure for adding the required coercions. This is due to the fact that we only use very specific coercions that can be determined by examining simultaneously the pattern and the type of the pattern.

As we anticipated in previous sections, the type of the whole pattern is a piece of information we’ve attached to the decorated AST, and that we’ve forwarded through the different passes. Thus, we just need to apply the rules in figure 8 to insert coercions in patterns when needed. The rules for generating coercions are structured as follows.

• The main function isJK: it matches a pattern and its type in parallel, and generates a corresponding coercion. As we said before, in the case of an or-pattern, coercions are attached to the patterns on both sides.

• push recursively applies (∀×) under . This al-lowsJKto coerce(τ1, . . . , τn)into(τ1, . . . ,τn), and recursively generate the sub-coercions for each branch of the tuple.

• elim, when applied to τ, removes all the s that quantify on type variables that do not appear inτ. This allowsJK, when it hits an identifier at the end of the recursive calls, to remove all the unused quan-tifiers.

As an example, theJKfunction, when matching(x, y) against

∀∀.(1→1, 0→int) generates the following pattern

(

xI[[]], yI[])I[∀ ×];∀×

which results inxbeing assigned the type scheme. 0→ 0andybeing assigned the type scheme. 0→int.

We will come back later on this feature with a larger example.

e. More on coercions

One of the goals we haven’t achieved yet consists in mapping the value restriction à la Garrigue into these coercions. Because this just boils down to introducing a fews and instantiating some of them to, this should be feasible rather easily. This kind of manipulation on types is a perfect candidate for a translation in terms of coercions.

3 Other simplifications

Out of the many constructs that are offered by OCaml, many of them are redundant. For instance, thefunction

keyword can be replaced by fun x -> match x with. We might also want stronger guarantees, such as the uniqueness of identifiers. These many simplifications are all performed in the translation from CamlX to the Core language.

a. Uniqueness of identifiers

Previously, identifiers were simply scoped strings. That is, one had to forward an environment with a mapping when walking the tree, in order to map information to identifiers. This is not necessarily an issue, but one construct of the original OCaml caused us some pain through our translations.

let _ = let i = 2 and j = 3 in let i = j + i and j = j - i in i, j ;; - : int * int = (5, 1)

Because of these simultaneous definitions, we had to keep not only one scheme in the let-constraints, but a list of schemes. Similarly, we had to use lists all the way to the final step of the translation. One could argue that disambiguating this was possible early on. However, the initial steps are not supposed to deal with program transformations, and we leave this to the final step.

(11)

3. Other simplifications

III.

System F and coercions…

Γ `c:τiτi0 Γ `(×i[c]) : (τ1, . . . , τi, . . . , τn)(τ1, . . . , τi0, . . . , τn) (tuple projection) Γ `c:ττ0 Γ `([c]) :τ≤ ∀τ (covariance) Γ `c1:ττ0 Γ `c2:τ0 τ00 Γ `c1;c2:ττ00 (transitivity) Γ ` •[σ] :τ[σ/0]τ (elimination) Γ `(∀×) :(τ1, . . . , τn)(τ1, . . . ,τn) (distributivity)

Figure 9: Subtyping rules for our system of coercions

In order to recover the simpleλ-calculuslet-binding, we translate all identifiers toatoms. Atoms are imple-mented as records with a globally unique identifier and possibly more information, such as the name of the orig-inal identifier for errors and pretty-printing.

That way, we desugar the example above into: let _/62 =

let i/59 = 2 in let j/58 = 3 in

let i/61 = (+)/56 j/58 i/59 in let j/60 = (-)/55 j/58 i/59 in (i/61, j/60)

in ()

Without the unique numbers appended to the original identifiers, this example would be semantically wrong7! b. Desugaring

Here follow some quick descriptions of all the con-structs we had to remove to end up with a pure, System F-like language.

?

Removinglet-patterns

Patterns can be used conveniently in many places in OCaml. After removing simultaneous, multiple

let-bindings, we endeavoured to remove let-patterns. Here’s a sample program that uses a pattern.

let (x, y) = (fun x -> x)(1, 2) ;;

We translate it to a program that directly matches the corresponding expression. This is nothing but the stan-dard semantics oflet-pattern.

match

(λ (x/39: int * int) -> x/39) (1, 2) with

| (x/37, y/38) ▸ id; id -> ()

7One might wonder why thelet _is treated as an identifier and

not a pattern. This is because thelet _has a special status. The OCaml AST has a specialPstr_valuenode for it. We map it to alet-pattern in our “term with holes”. Finally, for the translation to FE+, we translate it to a fresh identifier to avoid introducing a uselessmatch.

?

Removingfunction

One feature that was trickier was removingfunction. This keyword allows one to fuse a regular fun and a

matchtogether. Let us consider the sample code below. let fst = function x, y -> x

val fst: ∀ β α. α * β → α

The second line is the output from our solver that prints, as an intermediate result, the type of bound iden-tifiers. If one decides to only keep the type schemes for identifiers (that is, keeping the type scheme ofxandy), then type information is missing to introduce a tempo-rary identifier.

Fortunately, because we also annotate the CamlX AST with type schemes for the whole patterns, we can intro-duce an artificial identifier, switch to a regular funand then match on the fake identifier. The resulting program is shown below – this is the output of our pretty-printer. let fst/29 =

ΛΛ. λ (__internal/30: 1 * 0) -> match __internal/30 with

| (x/31, y/32) -> x/31 in

()

One important thing to remember is that, because we are in ML, we don’t need to apply coercions to this iden-tifier. Functions do not have polymorphic arguments in ML, so there is really no reason to apply coercions to the

__internalidentifier. c. Bonus features

We also decided to include a new feature, the gener-alizing match. The following example type-checks with our tool but does not with OCaml.

let s, f = match (””, fun x -> x) with | _, f ->

f 2, f 2. | _ ->

42, 42.

Becausefneeds to be polymorphic, we must general-izeeinmatch e with…. However, OCaml does not per-form such a generalization. This was originally started as

(12)

III.

System F and coercions…

4. An example a proof that such trivial changes only require tweaking

the constraint generator (and not the solver), but we de-cided to keep it as the required changes to make it work in System F were minimal.

The important part is that becausematches now can operate on expressions with polymorphism inside, we need to apply coercions insidematches. As we said previ-ously, because we chose to attach coercions to patterns, this generalizing match translates naturally in FE+. match match Λ. (””, (λ (x/69: 0) -> x/69)) with | (_, f/70) ▸ ∀×; id -> ((f/70•[int]) 2, (f/70•[float]) 2.) | _ -> (42, 42.) with | (s/67, f/68) ▸ id; id -> ()

The sample output above is the pretty-printed F-term that results of this translation. Different coercions are used in the branches of thematch.

4 An example

We finish this part with a slightly bigger example, and all the intermediate representations.

a. Original OCaml program

This program demonstrates the following features:

let-patterns, generalization, instantiation, coercions. We do not show the generated constraints below, as they are quite huge and hard to understand.

let a, b =

let g, h = 2, fun x -> x in h 2, h

b. The decorated AST (CamlX)

The schemes that are displayed have been converted to De Bruijn. let-patterns are annotated with the scheme of the whole pattern, which is necessary to generate proper coercions.

let (a, b): ∀. [int * (0 → 0)] = Λ. let (g, h): ∀. [int * (0 → 0)] = Λ. (2, (fun (x: 0) -> x)) in (h [int] 2, h [0]) in ()

Arguments to functions are annotated with their type as well, because we are targeting a Curry-style System F. This representation is still feature-rich, and coercions are implicit. We introduce coercions and desugar in the next step.

c. The core AST

This AST is desugared. Once can notice that we co-erce the initial let-binding by eliminating the unused quantification in the first component of the tuple: .int becomes justint.

We have chosen to compose the coercions for each branch of a tupleoutsidethe tuple, for simplicity reasons. This is strictly equivalent to attaching the coercions in-side each branch of the tuple, as we wrote in the rules for generating coercions.

match Λ. match Λ. (2, (λ (x/52: 0) -> x/52)) with | (g/50, h/51) ▸ ∀×; ×0[•[bottom]] -> (h/51•[int] 2, h/51•[0]) with | (a/48, b/49) ▸ ∀×; ×0[•[bottom]] -> ()

The resulting matchis actually generalizing, and h is instantiated twice with different arguments.

(13)

3. Conclusion

IV.

A reflexion on this work

Part IV

A reflexion on this work

1 Future improvements

a. Actually writing the type-checker

As time went missing, we did not complete the final type-checker before finishing this report. It should be an easy task, though. Paving the way and finding the correct representation was actually more difficult, and we hope to finish this soon before the final presentation.

b. Enriching the language

Algebraic data types are missing, and we hope to add them soon, both in CamlX and FE+. The coercions shouldn’t be changed very much, though, except adding the coercions that witness the covariance of algebraic data types and the distributivity ofwith regard to them. Moreover, although the first half of our tool prop-erly supports equirecursive types, and propprop-erly prints in-ferred types that contains equirecursive types, the trans-lation process does not support them at all. This will re-veal some interesting challenges: indeed, equirecursive types will require to introduce µ combinators to pack and unpack recursive types, thus transforming our tar-get language intoFµη.

Finally, if we introduce data types, we might as well in-troduce records. We’re also thinking of introducing poly-morphic variants to explore how well they translate into System F.

2 Related work

a. Modules

One core feature of ML is modules. Related research [RRD10] has explored a way to translate modules into Sys-tem F. This matches our goals quite nicely, but recursive modules seem to be unsupported. This is one area that could be explored if we were to further enrich our lan-guage.

b. A real compiler

Simon Peyton Jones argues [SCJD07] that such an in-termediate representation is well-suited for compilation. Since LLVM [LA04] has been making a lot of progress lately, and already starts to provide hooks for external GCs, it would be interesting to explore the feasibility of a LLVM backend for our intermediate language.

c. More modern features

As the language is still quite “clean”, it might be inter-esting to explore some additions to the regular OCaml: better dealing with effects, possibly adding more mod-ern features such as GADTs [SP04], or type classes. Such work remains a far, distant sight.

3 Conclusion

So far, it seems possible to translate a reasonable sub-set of OCaml into System F. This is interesting for check-ing the well-typedness of the intermediate

representa-tion, and maybe performing some program analysis, al-though we have not explored this path yet. Some fea-tures that initially seemed overly complicated actually can be translated quite naturally into System F, which justifies their existence. The framework we have built will allow us to type-checka posteriorisome more com-plicated features, and we believe this work is already promising.

Some interesting exploration lies ahead of us: we could, for instance, try to translate more “exotic” fea-tures, such as polymorphic variants, and see how well they interact with our subset of SystemFη. They might translate nicely, which would mean they correspond to some “fundamental” concept. If they do not translate nicely, this might mean they are not the right abstrac-tion; perhaps changing their original semantics might help them express more “basic” ideas.

Finally, this work could be reused as a framework for performing type-checking à la ML for other languages. Since we have paid much attention to the general struc-ture of our program, it is perfectly feasible to write a parser and a constraint generator for another language, without touching the rest of the tool. We hope to show with this experiment that a fresh design for type-checking is actually doable, and that it helps us augment our trust in the compilation process.

(14)

V.

Bibliography

V. Bibliography

Part V

Bibliography

References

[DM82] L. Damas and R. Milner. Principal type-schemes for functional programs. In Proceed-ings of the 9th ACM SIGPLAN-SIGACT sympo-sium on Principles of programming languages, pages 207–212. ACM, 1982.

[Gar] J. Garrigue. A Certified Implementation of ML with Structural Polymorphism.

[Gar04] J. Garrigue. Relaxing the value restriction. Functional and Logic Programming, pages 7–57, 2004.

[Gir72] J.Y. Girard. Interprétation fonctionnelle et elimination des coupures de l’arithmétique d’ordre supérieur.Thèse d’etat, Université Paris VII, 1972.

[GMM+78] M. Gordon, R. Milner, L. Morris, M. Newey, and C. Wadsworth. A metalanguage for inter-active proof in LCF. In Proceedings of the 5th ACM SIGACT-SIGPLAN symposium on Principles of programming languages, pages 119–130. ACM, 1978.

[L+04] X. Leroy et al. The CompCert verified compiler. Development available at http://compcert. inria. fr, 2009, 2004.

[LA04] C. Lattner and V. Adve. LLVM: A compilation framework for lifelong program analysis & transformation. InProceedings of the interna-tional symposium on Code generation and timization: feedback-directed and runtime op-timization, page 75. IEEE Computer Society, 2004.

[LDG+10] Xavier Leroy, Damien Doligez, Jacques Gar-rigue, Didier Rémy, and Jérôme Vouillon. The objective caml system release 3.12. At http://caml. inria. fr, 2010.

[Mil78] R. Milner. A theory of type polymorphism in programming. Journal of computer and system sciences, 17(3):348–375, 1978.

[Mit88] John C. Mitchell. Polymorphic type inference and containment. 76(2–3):211–249, 1988. [PR05] F. Pottier and D. Rémy. The essence of ML type

inference, 2005.

[Rey74] J. Reynolds. Towards a theory of type struc-ture. In Programming Symposium, pages 408–425. Springer, 1974.

[RRD10] A. Rossberg, C.V. Russo, and D. Dreyer. F-ing modules. In Proceedings of the 5th ACM SIG-PLAN workshop on Types in language design and implementation, pages 89–102. ACM, 2010.

[RY08] D. Rémy and B. Yakobowski. From ML to ML F: graphic type constraints with efficient type inference. In Proceeding of the 13th ACM SIGPLAN international conference on Functional programming, pages 63–74. ACM, 2008. [SCJD07] M. Sulzmann, M.M.T. Chakravarty, S.P. Jones,

and K. Donnelly. System F with type equal-ity coercions. InProceedings of the 2007 ACM SIGPLAN international workshop on Types in lan-guages design and implementation, page 66. ACM, 2007.

[SP04] V. Simonet and F. Pottier. Constraint-based type inference for GADTs. 2004.

Figure

Updating...

References

Updating...

Related subjects :