NOTE: If you’d install `remu_ts`

and `remu_scope`

, clone them from RemuLang and use `opam install .`

to install them locally.

# FSYM: An Abstraction On Tagless-Final Style To Compositing And Decoupling Multiple Interpretations¶

`Lamu0`

is a very simple and basic programming language, but the implementation in plfp/lamu0 shows an approach to use Tagless Final to

handle the case that the order of interpretations is significant.

allow the composition of decoupled compiler phases

Given the grammar

```
type litype = IntT | FloatT | StringT
module type SYM = sig
type repr
val letl : string -> repr -> repr -> repr
val lam : string -> repr -> repr
val app : repr -> repr -> repr
val lit : litype -> string -> repr
val var : string -> repr
end
```

## Terminology¶

The following terms represent the same thing in this presentation.

interpretation (from a separate view of compiler)

`SYM`

(from the view of routine implementation in OCaml)algebra (from a view of mathematics)

compiler phase(from an overall view from compiler)

## Background¶

The steps through `type repr`

to the executable low level instructions are complex:
Usually, a phase `A`

should be performed prior to some phase `B`

, so `B`

depends on `A`

.

For instance, if the last phase is back end code generation, it for sure depends on all of other phases.

```
A -> B -> ... -> C
\ \ |
.. .. |
\ \|/
CodeGen
for each phase, it's either prior to phase CodeGen or phase CodeGen itself.
```

Another intuituve example, to perform type checking/inference, we shall understand the scope information firstly.

```
---------------------
| |
let x = 1 | identify the occurrences of
let y = 2 | a type variable
in y + x-------------|
in x---------------------
```

More instances could be found in the real world:

All type-directed code generations require the information from (partially) type inference.

The compilation for type classes, in the approach of dictionary passing, requires type information, and a scope for class instances.

The closure conversion needs scope information to get the free variables of a function.

etc…

The tagless final approach works for the polymorphisms of interpreting a given “grammar”, however, it lacks of the facilities to work with multuple separate interpretations, like

**compositing separate interpretations****resolving the dependency relationships among dependent interpretations****decoupling the dependent interpretations as much as possible**

To address these problems, I proposed

an operation(

`grow`

) among the algebras to make reductions,a module signature(

`FSYM`

) very close to the shape of`SYM`

to composite and decouple interpretations.

which could be taken advantage of to solve above 3 pain spots fairly easy.

## Introduction¶

The core idea is the generalization of `fold`

operation,
and I call it `grow`

.

`grow (grow (grow zero fst) snd) third`

, where `zero`

, `grow (zero, fst)`

, and similar stuffs are algebras(`module SYM`

),
however, all of them have distinct representations(usually written as `type repr`

in a `SYM`

).

In this design, for `grow(m, mexpander)`

, where `m`

is an algebra,
and `mexpander`

can produce a new algebra(e.g., `scope->scope+type`

) by composing with `m`

via `grow`

.

Something occurs naturally and simultaneously during expanding the algebra is, the expansion of the representation type(`type repr`

in an algebra).

```
A : module SYM with type repr = o (* base *)
B : module SYM with type repr = r (* result *)
```

We check the “delta” from `o = A.repr`

to `r = B.repr`

,
and represent it by type `c`

, in other words:

`grow`

transforms the type `repr`

in the algebra(`SYM`

) from `o`

to `r`

, with a delta `c`

, while transforming the interpretation from `A`

to `A+B`

.

We extract the type expanding function out:

```
val combine : o -> c -> r
```

And extract the interpretation expanding function out:

```
val grow: 'o 'c 'r.
(module SYM with type repr = 'o) ->
(module FSYM with type o ='o and type c = 'c and type r = 'r) ->
(module SYM with type repr = 'r)
```

So the current goal is to extract the structure of `FSYM`

, and make sure it satisfy our final goals:

compositing separate interpretations:

This is already explicit and natural, according the type of

`grow`

function.resolving the dependency relationships among dependent interpretations:

We’re supposed to make sure

`FSYM`

can use the interpreted result of`module SYM with type repr = o`

to implement the interpretion`module SYM with type repr=r`

.decoupling the dependent interpretations as much as possible:

We’re supposed to decouple the implementation of

`module SYM with type repr = o`

and`FSYM`

.That is to say, we don’t have to, and even shouldn’t know or aware the implementation of

`module SYM with type repr = o`

when we’re impelementing`FSYM`

.

We point out that, the following signature would suffice

```
module type FSYM = sig
type r
type c (*delta*)
type o
val combine : o -> c -> r
val project : r -> o
val letl : o -> string -> r -> r -> c
val lam : o -> string -> r -> c
val app : o -> r -> r -> c
val lit : o -> litype -> string -> c
val var : o -> string -> c
end
```

Deriving an `FSYM`

from `SYM`

is trivial:

Besides the common part

```
sig
type o
type c (* delta from o to c *)
type r
val combine : o -> c -> r
val project : r -> o
end
```

Any operator of type `a -> b -> ... -> r`

in `SYM`

,
is supposed to be transformed to `o -> a -> b -> ... -> c`

in `FSYM`

,
where the symbols `o, c, r`

keep the same meanings as the aforementioned:

`o`

is the original`repr`

of the original algebra`A`

,`r`

is the`repr`

of the result algebra(`A+B`

) transformed by`grow(A, B)`

,`c`

is the delta of the change from`o`

to`r`

. There’s no algebra like`SYM`

, but exactly a functor`A->A+B`

.

To elaborate, we can use `lam`

operator as an examplar:

in

`SYM`

/algebra/interpretation`A`

:val lam_1: string -> o -> o

in

,`FSYM`

`A->A+B`

:val lam_2: o -> string -> r -> c

in

`SYM`

/algebra/interpretation`A+B`

val lam_3: string -> r -> r

We now need to implement `lam_3`

via `lam_1`

and `lam_2`

.

Recall the last 2 of our goals which haven’t been accomplished:

decoupling the dependent interpretations as much as possible:

Which is to say,

`lam_2`

and`lam_3`

shouldn’t aware how`lam_1`

gets implemented, and it’s easy to satisfy:let lam_3 (argname: string) (body: r) = let body_o: o = project body in let o = lam_1 argname body_o in (* HIGHLIGHTING HERE! *) ...

Of course,

`lam_1`

, and anything else for implementing the prior interpretation(a.k.a`A`

), shouldn’t be referred in`lam_2`

’s implementation, or how`lam_3`

uses`lam_2`

.resolving the dependency relationships among dependent interpretations:

Hence,

`lam_2`

should use the result of the prior interpretation(a.k.a`A`

), and it’s quite easy as well:let lam_3 (argname: string) (body: r) = let body_o: o = project body in let o = lam_1 argname body_o in let c = lam_2 o argname body in (* HIGHLIGHTING HERE! *) combine o c

The whole code for `grow`

can be found at final.ml L28-L59,
but notice that the type `repr`

in `SYM`

is written in a shorter form `r`

.

* In fact, if we use lazy types as the `repr`

of each interpretation/phase,
the order of interpretation can be more flexible.

Check `Lamu0`

in the sub-section `Application`

.

## Application¶

Lamu0 gives a very simple example to compose the existing and decoupled frameworks for compilers.

### Scoping: Name Resolution¶

An existing simple framework, remu_scope, designed for name resolution, also written by me, provides following 3 major APIs:

```
val require: env -> scoperef -> name -> sym
val enter: env -> scoperef -> name -> sym
val subscope: env -> scoperef -> scoperef
```

For example, to solve the scope of following code:

```
let x = 1 in x
```

We can do:

```
let env = empty_env() in
let root: scoperef = 0 in
let let_scope = subscope env root in
let x_assign = enter let_scope "x" in
let x_load = require let_scope "x"
```

With this snippet, you can check `assert (x_assign = x_load)`

.

With tagless final extended by `FSYM`

abstraction and above existing framework, we can then implement a standalone but composable interpretation for name resolution:

```
module Scoping = Remu_scope.Solve
type scopedesc =
| Sym of Scoping.sym
| ScopeUnrelated (* for expressions that're not variables *)
type scopeinfo = {desc: scopedesc; i: Scoping.scoperef}
module type STScope = sig
type o
type c = scopeinfo Lazy.t
type r
val env : Scoping.env
val cur_scoperef : Scoping.scoperef ref
val combine: o -> c -> r
val project: r -> o
val get: r -> scopeinfo
end
module FSYMScope(ST : STScope) = struct
include ST
let letl : o -> string -> r -> r -> c = ...
let lam: o -> string -> r -> c = ...
let app: o -> r -> r -> c = ...
let lit: o -> litype -> string -> c = ...
let var: o -> string -> c = ...
end
```

The whole code can be found at lamu0_ast.ml L5-L60.

We unroll the implementation of `lam`

:

```
(*
let subscope () = Scoping.subscope ST.env (!ST.cur_scoperef)
let enter n = Scoping.enter ST.env (!ST.cur_scoperef) n
let with_scope si' f =
let si = !ST.cur_scoperef in
ST.cur_scoperef := si';
let ret = f() in
ST.cur_scoperef := si;
{desc=ret; i=si}
*)
let lam: o -> string -> r -> c = fun _ n e -> lazy begin
let si' = subscope () in
with_scope si' @@ fun () ->
let _ = enter n in
let _ = get e in
ScopeUnrelated end
```

It’s pretty easy, and can be composed into the compilation pipeline, for every programming language whose scope could be expressed by `remu_scope`

.

### Typing: Type Inference¶

Type inference requires already knowing the scope information.

So it depends on the previous phase, name resolution.

Firstly we check an existing framework providing type inference, remu_ts.

And we just use a very limited part of `remu_ts`

, here’s an example of this framework:

To infer the types of code,

```
val f : forall a. 'a -> 'a -> bool
let x = 1 in f x y
```

We write

```
open Remu_ts.Infer
open Remu_ts.Comm
open Remu_ts.Builder
module TC : TState = (val crate_tc empty_tctx : TState)
let _ = let open TC in
let intt = new_type "int" in
let boolt = new_type "bool" in
let x = new_tvar() in
let y = new_tvar() in
let f = Forall(["a"], Arrow(Fresh "a", Arrow(Fresh "a", boolt))) in
(* x = 1 *)
assert (unify x intt);
(* f x y *)
let arg1 = new_tvar() in
let arg2 = new_tvar() in
assert (unify arg1 x);
assert (unify arg2 y);
let func = Arrow(arg1, Arrow(arg2, boolt)) in
assert (unify f func);
let print_ty name x =
Printf.printf "%s: %s\n" name @@
dumpstr
(mk_show_named_nom (module TC)) @@
prune x
in
print_ty "x" x;
print_ty "y" y;
print_ty "func" func
```

After running this file, we got

```
x: ^int
y: ^int
func: ^int -> ^int -> ^bool
```

The implementation of `FSYM`

to leverage above existing framework is:

```
module Typing = Remu_ts.Infer
module type STType = sig
type o
type c = Typing.t Lazy.t
type r
val combine: o -> c -> r
val project: r -> o
(* type checking states *)
val tc: (module Typing.TState)
(* from repr to type *)
val rtype: r -> Typing.t
(* from symbol to type *)
val ntype: o -> Scoping.name -> Typing.t
(* annotate symbol's type *)
val ann: o -> Scoping.name -> Typing.t -> unit
(* basic types *)
val intt: Typing.t
val strt: Typing.t
val floatt: Typing.t
end
exception TypeError
module FSYMType(ST: STType) = struct
include ST
module TC = (val tc)
open TC
let letl : o -> string -> r -> r -> c = ...
let lam: o -> string -> r -> c = ...
let app: o -> r -> r -> c = ...
let lit: o -> litype -> string -> c = ...
let var: o -> string -> c = ...
end
```

The whole code of this could be found at Lamu0_ast.ml #L62-L129.

For a rough sketch, let’s check the implementation `lam`

again:

```
let lam o n e = lazy begin
let eo = project e in
let var_of_arg = new_tvar() in
ann eo n var_of_arg;
Typing.Arrow(var_of_arg, rtype e) end
```

Finally, we assembly things together, and make a type inferencer for `Lamu0`

at main.ml.

You can run the type infer REPL with: `dune exec lamu0 --profile release`

:

```
let x = 1 in x;;
=> expr0 : ^int
let y =
let f = fn x => fn y => y in
let g = f 1 2.0 in
f
in y;;
=> expr0 : ^int -> ^float -> ^float
let f = fn y => y "123" in f (fn x => x);;
=> expr0 : ^string
```