Embedding Haskell: Compilers, and compiling compilers

Posted on October 9, 2015 by Chris Hodapp
Tags: haskell, ramblings

My last post mentioned that some things need some better explanation, because I’m always trying to re-explain and clarify.

This blog is devoted to the use of Haskell with embedded systems. What does that even mean? We see a couple broad categories (which the slides on the last page, as well as our Links page, mirror):

  1. Full Compilation: Compiling Haskell code to an embedded target.
  2. Limited Compilation: Compiling some limited subset of Haskell code to an embedded target.
  3. Hosted EDSL & Compiler: Hosting, in Haskell, an EDSL and a compiler to an embedded target.

As far as I know, I made these categories up. If anyone happens to know a more established classification, better names, or an example of who wrote about it first, please tell me.

This might look like a lopsided, arbitrary grouping; it sort of is. The commonality is that in all cases one uses Haskell to express something (a program, a circuit, specifications, call it what you will) for an embedded target. More on that follows.

I exclude things like Cryptol and Idris from this because - while implemented in Haskell and usable for embedded platforms - they are different languages unto themselves. I might arbitrarily drop that distinction in the future if I feel like it…

Full Compilation

This is what normally comes to mind when people hear about using Haskell with embedded systems - compiling the Haskell code to run directly on an embedded target, bringing along the normal runtime with it (plus whatever required bootstrapping and support). The Compiling to Embedded Targets section of the Links page is concerned particularly with this.

However, this actually appears to be pretty rare. The nature of the Haskell language brings some formidable challenges. Particularly, one must make the Haskell runtime fit on the target and make the garbage collection and lazy evaluation behave in predictable and sane ways.

Ajhc, a JHC-derived compiler from Kiwamu Okabe of METASEPI, is the only example of this I found - it could compile and execute on ARM Cortex-M3/M4. Kiwamu has written a lot on his experiences with making Haskell run in this footprint. His subsequent switch to the ATS language may be a hint.

HaLVM from Galois might arguably fit in this category.

Limited Compilation

This uses an existing compiler for certain stages (such as the parsing and type-checking), but a custom back-end to actually produce code, often with a lot of static analysis. This may adapt or disallow certain constructs (for instance, floating-point, recursive functions, recursive datatypes: CλaSH Unsupported Haskell features).

GHC accomodates this by allowing developers to invoke GHC functionality, from Haskell, as a library.

The Compiling for FPGA/ASIC section of the Links page has a few examples of this.

Hosted EDSL & Compiler

The Code Generation EDSLs and Circuit Design EDSLs sections of the Links page cover the copious examples of this. Atom, the topic of a few of my prior posts, is in this category.

This category is the one I am most often having to explain. It typically uses an EDSL (embedded domain-specific language) inside of Haskell to direct the process of code generation to a lower-level representation. This is otherwise called: compiling.

To emphasize: The code that runs on the target is entirely decoupled from the Haskell runtime. The Haskell compiler here isn’t compiling anything for the target - it’s compiling another compiler and the input to that compiler. That input happens to be specifications of what will run on the target.

This is a limitation of one sort:

It’s also a benefit of another sort:

I said in the last post that in this category Haskell takes on the role of a metaprogramming or template language. While this may be true, I sort of ignored that it’s less relevant, because it’s the same in all the other categories.

Commonality

Lumping together these categories might seem like a stretch, especially considering that the last category involves extra stages and a shift in how one thinks about the software.

Ponder the following, though:

Is a trend clear? (No, it’s not monads. Signal is only applicative, and I suspect Lava behaves similarly.)

That list spans our three categories. In each of them, one builds up a program (in a very broad sense) simply by building up a value in Haskell. Beyond that, the only real differences are,

Ignoring the vague nature of the term, “declarative,” this relates pretty directly to the declarative nature of Haskell programs.

Seen from this perspective, one is still compiling Haskell to run on some embedded target. The compilation just might continue outside of the system’s Haskell compiler, and the running might not involve its runtime.