GHC’s support for compiling multiple units in a single invocation is essential for tooling to work well with real-world Haskell projects. Loading your whole project into a single GHCi session allows you to get feedback quickly on changes to any part of your project, without having to restart the REPL. Until now, not all of GHCi worked with multiple home units, and this was a source of confusion for many users.

We’re now happy to announce that in 9.14.1, GHCi will fully support multiple home units. This post contains a brief overview of the changes.

Multiple Home Units

Work on multiple home units has been ongoing for a while. This is the latest chapter in our efforts to update the ecosystem to support this feature.

The main way to start a multi-unit GHCi session is by using cabal repl --enable-multi-repl with a selector that selects multiple components in the project, such as all:

> cabal repl --enable-multi-repl all

This will start a GHCi session with a home unit for each selected component. Until now, support in the REPL was essentially limited to reloading modules to get feedback about changes. Almost all other commands were unsupported when using multiple home units.

GHCi Supports Multiple Home Units

Following our changes, GHCi now fully supports multiple home units in its REPL. The experience of a user is now the same whether they are using a single home unit or multiple home units. In particular, the following features have been fixed or enabled:

  • Usual REPL usage such as evaluating expressions
  • All GHCi commands
    • :seti/:set
    • :browse
    • :module [+/-] [*]Mod1 ...
    • … and many more!
  • The GHCi debugger
    • :break, :steplocal, :continue, etc…

Implementing Multi Unit Support in GHCi

To fully support multiple home units, GHCi needed a new internal model of how different contexts interact during a session. There are three key contexts:

  • the prompt (the context in which expressions are evaluated),
  • the script context (in which scripts loaded by :load are executed), and
  • the unit context (the home units specified on the command line, e.g. the components of the Cabal packages being loaded).

Distinguishing these three different contexts is the key to our design. Before, each GHCi session only had a single home unit, and so commands would always be interpreted relative to that unit. In a multi-unit session, one of the units was chosen as the “active” unit, and commands would be interpreted relative to that unit. Now since it is possible to talk precisely about the different contexts, the dependencies between them and where commands should be interpreted, we can properly implement all GHCi commands.

Virtual home units

Our design adds virtual home units for the prompt and script contexts. Therefore, every GHCi session is a multi-unit session, and all commands are modified to support this.

This virtual home unit for the prompt is called interactive-ghci. All user input is interpreted in the context of interactive-ghci (it is the “active” unit). Since it always depends on all user-given home units (i.e. those given on the command line), we can import modules, run code, and execute GHCi commands as usual.

The virtual home unit for scripts is called interactive-session. It is similar in structure to interactive-ghci, namely that it depends on all user-given home units. This allows scripts to use packages from the current GHCi REPL session. Additionally, interactive-ghci depends on interactive-session, allowing the user to load and execute the script modules from the prompt.

Why do we need two virtual home units? When a script is loaded via :load Mod.hs, this Mod.hs needs to be interpreted relative to some home unit. We do not want to guess which home unit Mod.hs should be added to, since the behaviour is hard to predict in a multiple home unit session. However, we also can’t add Mod.hs to the interactive-ghci home unit, as we want to be able to maintain a different set of GHC options for the prompt (i.e. interactive-ghci) and scripts.

Adding these two virtual home units to the GHCi REPL session yields the following Home Unit Graph. We mark interactive-ghci to indicate that it is the “active” context of the GHCi prompt.

GHCi’s Home Unit Graph, showing two virtual units interactive-ghci and interactive-session, where the former depends on the latter. Both of these depend on any number of user-given home units, indicated by the names pkg1 … pkgN.

Examples

Now that we know how the GHCi session will work, let’s show a couple of concrete examples.

We assume a regular cabal project, initialised via the command:

> mkdir mhu-example && cd mhu-example
> cabal init -n --tests --libandexe

This creates a cabal project with three components:

  • lib:mhu-example: The main library.
  • exe:mhu-example: An executable.
  • test:mhu-example-test: A test-suite.

From the perspective of GHC, a unit is essentially identical to a single component (with some hand-waving).

Example of a cabal project with multiple components. GHC treats each component as a separate unit.

When we load only the library into a GHCi session, then the library is the single user-specified home unit in the GHCi session. For example, the cabal invocation

cabal repl lib:mhu-example

invokes the following GHC command:

ghc --interactive -this-unit-id lib-mhu-example -package base -package containers ...

This creates a home unit graph with three home units: interactive-ghci, interactive-session and mhu-example-library.

Home Unit Graph with a single user-specified Home Unit. There are three units, interactive-ghci, interactive-session and lib:mhu-example. interactive-ghci depends on interactive-session and lib:mhu-example, while interactive-session depends on lib:mhu-example.

In the case of more than one user-specified home unit, the graph is extended in an intuitive way. For example, the cabal invocation

cabal repl --enable-multi-repl lib:mhu-example exe:mhu-example test:mhu-example-test

will result in the following GHC invocation:1

ghc --interactive -unit @lib-mhu-example -unit @exe-mhu-example -unit @test-mhu-example-test

GHCi internally structures this as the following:

Home Unit Graph with a multiple user-specified home units. There are five units, called interactive-ghci, interactive-session, lib:mhu-example, exe:mhu-example and test:mhu-example-test.

Naturally, home units can have dependencies on other home units, e.g. test:mhu-example-test and exe:mhu-example both depend on lib:mhu-example.

Setting REPL Options

The GHCi commands :set and :seti are used to change the GHC options of the home units and the ghc options for the prompt respectively. In the new architecture, the :set command applies the new options to all home units except interactive-ghci. :seti, on the other hand, applies changes only to the interactive-ghci home unit.

In the future, we may want to extend the capabilities of the :set command to change the GHC options only for certain home units.

Summary

GHCi is now fully compatible with multiple home units, including all GHCi commands and the GHCi debugger. Our new design generalises the architecture of GHCi so that multi-unit and single-unit sessions are handled in the same way. The uniform handling will make sure that multi-unit sessions work correctly as GHCi evolves.

This work has been performed in collaboration with Mercury, who have a long-term commitment to the scalability and robustness of the Haskell ecosystem. Well-Typed are always interested in projects and looking for funding to improve GHC and other Haskell tools. Please contact info@well-typed.com if we might be able to work with you!


  1. The unit arguments are passed using response files. The file exe-mhu-example contains the arguments for the exe:mhu-example home unit, and similarly for the other files.↩︎