More projects · 05 / 05Programming languages · OCaml

Interpreter in OCaml

An interpreter for a small functional language, written in OCaml: an abstract syntax tree, environments and closures, recursive functions, a dictionary type of its own, and two evaluators that run the same program under static and dynamic scoping.

Scoping, live. Click to replay.
line being evaluatedbinding in the environment
2evaluators, static and dynamic scoping
30constructs in the abstract syntax tree
6dictionary operations, built into the language
1 vs 2the same program, two scoping rules
1OCaml file, no external libraries
01 / The language

A language, as a data type.

In OCaml a program can be described exactly as data: the abstract syntax tree is an algebraic type with one constructor per construct. The language has integers, booleans and strings; arithmetic (sum, difference, product, division, modulo, unary minus); logic and comparisons; if-then-else; let bindings; first-class functions and their application; recursive functions with let rec; and a dictionary type with its own operations.

Pattern matching then makes the interpreter read almost like the language's definition: one case per constructor, each saying what that construct means.

02 / The runtime

Environments, closures, values.

  • Environments map identifiers to values, as a list where the newest binding shadows the older ones: bind adds, lookup finds.
  • Expressible values are integers, booleans, strings, dictionaries and functions. Under static scope a function is a closure that carries the environment of its definition; a recursive one carries its own name too.
  • Dynamic type checking. Every operation checks the types of its operands at run time and raises a clear error for a type mismatch, a non-boolean guard, a non-functional value or a division by zero.
03 / Scoping

Where does a function look for its variables?

The interpreter comes in two versions that differ in one decision. With static scope (eval), a function uses the environment where it was defined; with dynamic scope (rt_eval), the one where it is called. The project's test program, animated at the top, shows the difference in four lines: f reads x, and x is redefined between the definition of f and its call. Static scope answers 1, dynamic scope answers 2.

OCAML-interpreter.ml · eval and rt_eval
(* Interpreter with static scope rule: a function keeps the environment where it is defined *)let rec eval (e: exp) (r: 't env) : evT =    match e with    |   Den i -> lookup r i    |   Let (i, e1, e2) -> eval (e2) (bind r i (eval e1 r))    |   Fun (param, fbody) -> Closure (param, fbody, r)    |   Apply (eF, eArg) ->            let fclosure = eval eF r in (                match fclosure with                |   Closure (arg, fbody, fDecEnv) ->                        let aVal = eval eArg r in                            let aEnv = bind fDecEnv arg aVal in                                eval fbody aEnv                |   _ -> failwith ("Non functional value")            )(* Interpreter with dynamic scope rule: a function uses the environment where it is called *)let rec rt_eval (e : exp) (r : 't env): evT =    match e with    |   Fun (param, fbody) -> DynFun (param, fbody)    |   Apply (eF, eArg) ->            let fval = rt_eval eF r in (                match fval with                |   DynFun (arg, fbody) ->                        let aVal = rt_eval eArg r in                            let aEnv = bind r arg aVal in                                rt_eval fbody aEnv                |   _ -> failwith ("Non functional value")            )
The cases that make the difference, from the two evaluators: the static one builds a closure and applies it in its definition environment, the dynamic one applies the function in the current environment.
04 / Dictionaries

A data structure in the language itself.

Beyond the usual constructs, the language has a dictionary type with six operations, each one a case of the interpreter:

Dict and Get

Create a dictionary from key-value pairs and read the value of a key, with a KeyNotFound error for a missing one.

Add and Remove

Add a new pair, with an ExistingKey error if the key is already there, or remove one.

Clear

Empty a dictionary.

ApplyOver

Apply a function to every value: for example fun x -> x + 3 over a whole dictionary.

A language, from scratch.

The full interpreter, with both evaluators and the test expressions for dictionaries and scoping, is a single OCaml file on GitHub. This was the OCaml half of the Programming II project; the Java half is the Secure Data Container.

Next project · 01 / 05WORTH — Real‑Time Kanban in Java