Some toy programming language implementations, mostly implemented in OCaml.
These projects are mostly my attempt to understand different techniques and approaches to implementing programming languages. Perhaps from these seedlings something new and interesting might germinate?
Elaboration is an approach to implementing language front-ends where a complicated, user friendly surface language is type checked and lowered to a simpler, typed core language. This approach to type checking is particularly popular and useful for implementing dependently typed programming languages, but is more widely applicable as well.
Simply typed:
- elab-stlc-bidirectional: An elaborator for a simply typed lambda calculus that uses bidirectional typing to allow some type annotations to be omitted.
- elab-stlc-bidirectional-stratify: An elaborator that partially stratifies a combined type and term language into a simply typed core language.
- elab-stlc-abstract: An LCF-style elaborator that moves the construction of well-typed terms behind a trusted interface.
- elab-stlc-unification: An elaborator for a simply typed lambda calculus where type annotations can be omitted.
- elab-stlc-letrec-unification: Extends the simply typed lambda calculus with recursive let bindings.
- elab-stlc-variant-unification: Extends the simply typed lambda calculus with structural variant types, inferring types eagerly using constraint based unification.
Polymorphically typed:
- elab-system-f-bidirectional: An elaborator for a higher-rank polymorphic lambda calculus.
Dependently typed:
- elab-dependent: An elaborator for a small dependently typed lambda calculus.
- elab-dependent-sugar: An elaborator for a small dependently typed lambda calculus with syntactic sugar.
- elab-record-patching: An elaborator of a dependently typed lambda calculus with singletons and record patching.
These are related to compilation. Mainly to stack-machines, but I’m interested in exploring more approaches in the future, and other compilation passes related to compiling functional programming languages.
- compile-arith: Compiling arithmetic expressions to stack machine instructions and A-Normal Form.
- compile-arithcond: Compiling arithmetic and conditional expressions to stack machine instructions and A-Normal Form.
- compile-closure-conv: Typed closure conversion and lambda lifting for a simply typed lambda calculus with booleans and integers.
Miscellaneous programming language experiments.
- lang-datalog: A simple Datalog interpreter.
- lang-doc-templates: A programmable document template language that elaborates to a typed lambda calculus.
- lang-fractal-growth: Experiments with using grammars and rewriting systems to model fractal growth.
- lang-fractal-tree-rewriting: Plant growth modelled as tree rewriting systems.
- lang-lc-interpreters: A comparison of lambda calculus interpreters using different approaches to name binding.
- lang-shader-graphics: An embedded DSL for describing procedural graphics, based on signed distance functions. These can be rendered on the CPU or compiled to GLSL shaders.
While most of the above projects need more work put into them, the following projects need more work put into them and a more incomplete in comparison.
- wip-elab-builtins: An elaborator that supports built-in types and operations.
- wip-compile-stlc: A type preserving compiler for the simply typed lambda calculus.
- wip-compile-stratify: Compiling a dependently typed lambda calculus into a stratified intermediate language.
- wip-compile-uncurry: Compiling single-parameter functions to multiparameter functions.
As I’ve been working in the area of programming languages I’ve often found myself in the position of:
- Explaining the same idea or technique over and over, but not having a minimal example I can point to.
- Re-implementing an existing technique (either from a paper, or based on some other existing code I’ve seen) in my own way, as a way of learning and understanding it more deeply.
- Wanting a place to experiment with an approach before committing to using it in a larger project, which can take time and may amount to nothing.
- Trying to recall a technique I’d spent time learning a long ago.
- Having an idea for a small language experiment that does not need to be part of a standalone project, but may require some build system setup.
My hope is that by collecting some of these projects and experiments together into a single repository they might be useful to others and my future self.
I’m also trying to get better at interleaved breadth-first/depth-first search as part of my learning process - somewhat like the search strategy employed by miniKanren. My goal is not to become an expert of one thing, but to be able to be good at combining ideas from different places and perspective. I want to be comfortable with a bit of bodging, of publishing more “scrappy fiddles”1, but not so much that they can't be turned into something more coherent later on.
The metaphor of a “garden” as related to knowledge work was inspired by the rising popularity of “digital gardening” (which apparently originates from Hypertext Gardens). While this project is less directly interconnected than other digital gardens, I still like the idea of each project being a “seedling” that can be nurtured and tended to over an extended period of time, with the learning from one project being transferred to the others. Perhaps a “language nursery” would have been a more fitting name.
I’ve also been particularly inspired by Mark Barbone’s small, self-contained gists implementing small type systems and solvers, and Andras Kovacs’ excellent elaboration-zoo (which was instrumental in helping me get my head around how to implement elaborators).
If you like this repository, you might find these interesting as well:
- github:andrejbauer/plzoo: Andrej Bauer’s minimnal programming language demonstrations
- github:AndrasKovacs/elaboration-zoo: Minimal implementations for dependent type checking and elaboration by Andras Kovacs
- github:mspertus/TAPL: Updated type system implementations from Benjamin Pierce's “Types and Programming Languages”
- github:pigworker/Samizdat: Conor McBride’s programming scrapbook
- github:tomprimozic/type-systems Implementations of various type systems in OCaml
Other project gardens:
- github:jake-87/project-garden: A garden for small projects
- github:qexat/PLAGE: Programming Language Adjacent General Experiments
- github:RiscInside/LanguageEtudes: Single-file typechecker/interpreter/compiler implementations
- github:yeslogic/fathom-experiments: Fathom related experiments
- sourcehut:icefox/determination: Type checkers for System F and System Fω
The predominant style in OCaml of leaving off type annotations makes understanding and porting code far more difficult. Instead I try to add type annotations to most top-level type signatures.
When open
is used I find it hard to figure out where identifiers are coming from without an editor.
Instead I prefer using an explicitly qualified path where possible.
In the past I’ve often found it hard to find related nodes in an AST when trying to understand other people’s code. For example, the following variants might all refer to different parts of a dependent pair type:
type tm =
...
| Sig of string * tm * tm
| Pair of tm * tm
| Fst of tm
| Snd of tm
Instead I prefer to use the following constructors:
type tm =
...
| Pair_type of string * tm * tm
| Pair_lit of tm * tm
| Pair_fst of tm
| Pair_snd of tm
OCaml’s variant constructors aren’t namespaced under the type like in Rust or Lean, so reusing the same variant name will result in ambiguities if you are relying on global type inference. Generally OCaml programmers will either:
- Wrap every type in a module
- Come up with an ad-hoc prefix for to prevent the conflict
I find the former convention often results in duplicated datatype definitions (mutually dependent modules require explicit module signatures), and the latter is a little arbitrary and ugly.
Instead I’ve decided to just disambiguate variants using the type. I realise this might make the code a more difficult to understand and if I come up with a better compromise I might revisit this in the future.
Using Lix (recommended) or Nix is not required, but can be useful for setting up a development shell with the packages and tools used in this project. With Nix flakes enabled:
nix run .#arith -- compile --target=anf <<< "1 + 2 * 27"
nix-direnv can be used to load development tools into your shell automatically. Once it’s installed, run the following commands to enable it in the project directory:
echo "use flake" > .envrc
direnv allow
You’ll want to locally exclude the .envrc
, or add it to your global gitignore.
After that, dune can be used to build, test, and run the projects:
dune build
dune test
dune exec arith -- compile --target=anf <<< "1 + 2 * 27"
Alternatively, opam package definitions are provided in the ./opam
directory. They drive the Nix flake, so should be up to date. I don’t use opam
however, so I’m not sure what the workflow is.