# MicroHs: a Haskell subset that compiles to combinators and can bootstrap itself

> MicroHs is an implementation of an extended subset of Haskell whose runtime uses combinators, so the whole thing needs only a C compiler and can be built for microcontrollers. It can be compiled by GHC or by itself, it runs in the browser, and its Integer type defaults to a slow pure Haskell implementation that can be swapped for GMP.

**augustss/MicroHs** — Haskell implemented with combinators

- Repository: https://github.com/augustss/MicroHs
- Stars: 794 · Forks: 89
- Language: Haskell
- License: NOASSERTION
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/augustss-microhs

## What the project actually is

The MicroHs repository contains an implementation of an extended subset of Haskell, and it uses combinators for the runtime execution. That single design decision explains most of the project's characteristics.

The runtime system has minimal dependencies and can be compiled even for microcontrollers, and the boards/ directory contains samples, for example sample code for an STM32F407G-DISC1 board. The compiler can compile itself, which is the property that makes a Haskell implementation interesting to study: you can take the compiler source, build it with an existing Haskell compiler, and then use the result to build itself again.

There is a browser version you can try without installing anything, and the author has presented the work at the Haskell Symposium 2024 with a video and a short paper at doc/hs2024.pdf.

The licence field on the repository is classified as a custom one, and the tree has a LICENSE file. The last push was on 2026-10-21, and there are no GitHub releases, although the tree carries release notes for v0.15.0.0, v0.16.0.0 and v0.17.0.0 alongside an updateversion.sh script.

The top-level entries show the shape of the project: src for the compiler source and the runtime, generated for the machine generated combinator file, lib for the Prelude and other base library files, lib/ and the ghc/ directory for GHC specific overrides, the web-mhs/ directory for the browser version, boards/, benchmarks/, tests/, unicode/, Tools/, cpphssrc/ and cpphscompat/ for the preprocessor, and several Makefiles.

## Two ways to build the compiler, and what each one costs

There are two different ways of compiling MicroHs. You can use GHC, with the Makefile target bin/gmhs, or you can use the included combinator file and runtime, with the Makefile target bin/mhs. These different ways of compiling need slightly different imports and so on, and the reason is that GHC looks in the ghc/ subdirectory first for any extras or overrides.

The README is clear that compiling MicroHs is really best done using make, but there is also a MicroHs.cabal file for use with cabal and mcabal, and that only builds what corresponds to the first target. Doing cabal install will install the compiler.

Two limits are stated. The mhs built with ghc does not have all the functionality. And there is no need to have a Haskell compiler to run MicroHs: all you need is a C compiler, and MicroHs can bootstrap given the included combinator file.

That is the point of the combinator design. The runtime is a C program, and the Haskell semantics are encoded as combinators that the runtime interprets, so a machine with a C compiler and nothing else can run the compiler and therefore run Haskell programs.

## Installing the compiler and the GMP switch

To install mhs you use make minstall, which installs mhs in ~/.mcabal in the same way as mcabal, MicroCabal, would have. It installs a compiler binary and a compiled base package, and you have to add ~/.mcabal/bin to your PATH.

The alternative is make oldinstall, which by default copies the files to /usr/local and can be overridden with make PREFIX=dir oldinstall. You also need to set the environment variable MHSDIR.

On Windows you need cl in the path, and then you use nmake with Makefile.windows.

The compiler can also be used with emscripten to produce JavaScript and WebAssembly, which is how the browser version is built.

The Integer implementation is the one performance trap worth knowing about early. The default implementation of the Integer type is written in Haskell and is quite slow, and it is possible to use the GMP library instead. To do that you uncomment the first few lines in the Makefile and also modify the definition that directs the C compiler where to find GMP. The Makefile shows the three lines to change, with separate values for the GHC build, the mcabal build and the C compiler flags, and a comment pointing at the homebrew paths for macOS after brew install gmp, and the Ubuntu equivalent after apt-get install -y libgmp-dev.

The note that matters is the one about switching: to switch between using and not using GMP you need to do make clean, and you might also need to do make USECPPHS=cpphs bootstrapcpphs if there are complaints.

## A worked example and what the Prelude leaves out

The example file in the repository is a factorial program, and it is the shape of a Haskell program any Haskell developer will recognise:

```Haskell
module Example(fac, main) where

fac :: Integer -> Integer
fac 0 = 1
fac n = n * fac(n - 1)

main :: IO ()
main = do
  let rs = map fac [1,2,3,10,52]
  putStrLn "Some factorials"
  print rs
```

The build and run sequence is three steps. First make sure the compiler is built by doing make. Then compile the file by bin/mhs Example -oEx which produces Ex. Finally run the binary file by ./Ex, which prints the factorials including the one for 52, a number large enough to show what a pure Haskell Integer has to do.

The libraries section states the important scope limit. The Prelude contains the functions from the Haskell Report and a few extensions, with the notable exception that Foldable and Traversable are not part of the Prelude, and they can be imported separately.

There is also a documented way to avoid the Prelude entirely, with the incantation import qualified Prelude(). The README explains that this is needed in base to avoid circular imports, and that it will not even try to open Prelude.hs, and that it also avoids the automatic qualified import of Mhs.Builtin that normally happens.

If no module name is given the compiler enters interactive mode, where you can enter expressions to be evaluated or top level definitions including import, and simple line editing is available.

## The cross compiler config and the repository layout

When mhs is built, a file called mhs.conf is generated with a number of predefined targets, and the README shows the shape of a unix target:

```ini
[unix]
cc = "cc"
ccflags = "-w -Wall -O3 "
cclibs = " -lm"
conf = "unix"
```

You can add other targets to this file, changing which compiler command is used and which runtime is selected, and then use the -t argument to select which target you would like. The file is generated from a template, mhs.conf.in, which is in the tree.

The directory structure is the clearest description of the project. Tools/ holds a few useful tools for compressions and such, bin/ is where executables are put, generated/ contains the machine generated combinator file for the compiler, lib/ contains the Prelude and other base library files, src/MicroHs/ is the compiler source, src/runtime/ is the runtime source, and tests/ holds the tests.

Beyond those, the tree carries the alternative toolchain integrations: a ghc/ directory for GHC extras and overrides, a hugs/ directory with Hugs related files, cpphssrc/ and cpphscompat/ for the C preprocessor, unicode/ for text handling, benchmarks/ for performance work, boards/ for the embedded samples, and web-mhs/ for the browser version. The Makefile itself defines the compiler flags, the GHC package list, the Hugs flags, and a sanitiser set with the undefined, address, pointer-compare and pointer-subtract checks, which is a sign that memory safety is tested rather than assumed.

## The preprocessor, the tests and how to add a primitive

The C preprocessor is treated as a first class problem. Sadly, compiling a lot of Haskell packages needs the C preprocessor, and to that end the distribution contains the combinator code for cpphs. Doing make bin/cpphs will create the binary for the preprocessor.

Bootstrapping cpphs is a separate step, make bootstrapcpphs, and this assumes that you have git to download the needed packages. The README notes that at the moment the downloaded packages are forks of the original to make it compile with mhs.

For identification, to know that it is MicroHs that is the compiler, it defines the symbol __MHS__.

The test suite lives in the tests/ directory, and the mechanics are simple enough to explain. To add a new test you create a MyTest.hs file and a corresponding MyTest.ref file for the expected output, and then you add it to the test rule in tests/Makefile:

```makefile
	$(TMHS) MyTest     && $(EVAL) > MyTest.out     && diff MyTest.ref MyTest.out
```

There is a second mechanism for negative tests. If you want to test that a module fails to compile with a certain error message, you add it to tests/errmsg.test, and the expected message is written below a separator line.

There are three ways to run the suite. make runtest uses the GHC-compiled compiler, make runtestmhs uses the MicroHs-compiled compiler, and make runtestemscripten uses the MicroHs-compiled compiler targeting JavaScript.

Contributions are described in the same practical spirit. When modifying the compiler, run make newmhs or make newmhsz, the latter compressing the binary, to generate a compiler that includes your changes. And adding a new primitive means touching four things in the runtime: add a variant to the enum node_tag in src/runtime/eval.c, add an entry to the primops table mapping the name of your primitive to the tag, add a case in printrec, and implement the primitive itself.

## Conclusion

MicroHs fits a Haskell developer who wants a small, self-hosting implementation they can read end to end, or an embedded project that needs Haskell on a microcontroller with a C toolchain and nothing else. It does not fit a production Haskell project, since it implements an extended subset rather than the language, and it does not fit a codebase that needs the base package libraries GHC ships. Before using it, read the README section on libraries, because the Prelude omits Foldable and Traversable by design, and check the ghc subdirectory build first, since the README notes that an mhs built with GHC does not have all the functionality.

## FAQ

### What is MicroHs and what makes it unusual?

It is an implementation of an extended subset of Haskell that uses combinators for the runtime execution, so the runtime has minimal dependencies and can be compiled even for microcontrollers. The compiler can compile itself, and a browser version is available to try without installing anything.

### How do I build MicroHs from source?

There are two ways. Using GHC with the Makefile target bin/gmhs, or using the included combinator file and runtime with the target bin/mhs, and the two need slightly different imports because GHC looks in the ghc/ subdirectory first. A MicroHs.cabal file is also provided for cabal and mcabal, and it only builds the first target.

### Do I need a Haskell compiler to run MicroHs?

No. All you need is a C compiler, and MicroHs can bootstrap given the included combinator file. To install mhs use `make minstall`, which puts it in ~/.mcabal the way mcabal would, and you then add ~/.mcabal/bin to your PATH. The alternative is `make oldinstall`, which copies to /usr/local by default and needs the MHSDIR environment variable set.

### Why is Integer slow in MicroHs and how do I fix it?

The default Integer implementation is written in Haskell and is quite slow, but you can use the GMP library instead by uncommenting the first few lines in the Makefile and modifying the definition that directs the C compiler where to find GMP. Switching between using and not using GMP requires a make clean, and you might also need make USECPPHS=cpphs bootstrapcpphs if there are complaints.

### How does the MicroHs test suite work?

The tests live in the tests/ directory. A new test is a MyTest.hs file plus a MyTest.ref file with the expected output, added to the test rule in tests/Makefile which compiles it, evaluates it and diffs the result against the reference. To check a compile failure you add the case to tests/errmsg.test with the expected message. The suite runs in three modes: make runtest, make runtestmhs and make runtestemscripten.

## Sources

- [augustss/MicroHs on GitHub](https://github.com/augustss/MicroHs)
- [Issues](https://github.com/augustss/MicroHs/issues)
- [README](https://github.com/augustss/MicroHs/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/augustss-microhs
