The Turbo Haskell Compiler
See the codeHaskell on Truffle/Graal.
I'm experimenting with using GHC as a frontend for a high performance Haskell implementation on the JVM. GHC does the parsing, type checking, desugaring and optimization. THC takes the resulting Core and gives Graal something it can specialize.
GHC already knows quite a lot about compiling Haskell. The intention is to keep that information around long enough to use it.
For native Windows, use the PowerShell build and test guide. It lists the required tools and current platform limits.
You need GHC 9.14.1 (including ghc-pkg and runghc), cabal-install 3.16,
GraalVM 25.3.4.1 / JDK 25, and Python 3.12+. Put GHC on your PATH and
point JAVA_HOME at GraalVM. On macOS, use the bundle's Contents/Home
directory. The Gradle wrapper downloads its dependencies on the first build.
Linux x86_64 setup includes clang and GMP development headers and libraries
(for example, libgmp-dev on Debian/Ubuntu) for native package dependencies.
The JDK is stock, but THC's default distribution includes patched Truffle API, runtime and Sulong JARs. Their patch inventory and shared-host effects are part of the embedding contract: some changes affect other languages using the same runtime, including across separate contexts and engines.
From the repository root:
git -c core.autocrlf=false submodule update --init --depth 1
export JAVA_HOME=/path/to/graalvm
export PATH="$JAVA_HOME/bin:$PATH"
make
This runs ./gradlew installDist for the JVM runtime and cabal build
for the Haskell library and driver. The thc library contains the GHC Core
plugin; Cabal builds it alongside the thc executable. Both builds are
incremental. make runtime and make haskell build either part separately.
make keeps Gradle's cache in .gradle-user-home; set GRADLE_USER_HOME to
share a cache across checkouts.
THC needs Core, GHC's intermediate representation, for both your program and
the Haskell libraries it calls. Standard GHC installations usually omit the
complete Core for their bundled libraries, including base and ghc-internal.
Compiling those libraries with
-fwrite-if-simplified-core
keeps every binding in their .hi interface files so THC can load it. The flag
must be used when building the libraries; adding it only to your program
cannot recover Core from libraries already installed without it.
With a standard GHC 9.14.1 installation, the default --installed-core pinned
provider rebuilds selected bundled libraries from the pinned GHC release and
caches their complete Core. Their versions, modules and dependencies must match
the selected installation. The first acquisition includes this library build.
Alternatively, build GHC's libraries with the flag above and select their
retained Core with thc run TARGET --installed-core required.
make check-ghc-core GHC=/path/to/ghc checks whether the installed libraries
contain the needed Core. The GHC build guide shows how to
apply the flag when building GHC 9.14.1, the version THC currently supports.
Fixture tests also require CMake 3.24+ and Ninja. make fixtures builds all
admitted fixture files. For routine development, select an exact class, for
example make test TESTS=thc.RuntimeTest.
make test-modes TESTS=thc.RuntimeTest runs both handoff modes from shared fixture
files; see the fixture graph and current limits.
make clean removes build products.
make distclean also removes the checkout's Gradle caches.
make run ARGS='--help' builds and runs the driver; the equivalent Cabal command
is cabal run thc -- --help.
Run the included smoke test through THC:
cabal run thc -- run completed --project-dir t/fixtures/run-pure \
--thc-root "$PWD" --dist-dir "$PWD/build/run-package"
This fixture checks a mutable reference and returns () without printing.
Inside your package, thc build [TARGETS...] follows Cabal build selection and
acquires dependency Core without launching an application. Omit targets for the
current package, use all, or select libraries and executables together.
Use thc run [TARGET], following cabal run target syntax, to build and execute.
For an installed driver in your own project:
thc run my-package:exe:my-program --thc-root /path/to/thc
Omitting the target selects the current package's sole buildable executable,
otherwise its sole buildable runnable component. Explicit PACKAGE:exe:NAME,
PACKAGE:test:NAME and PACKAGE:bench:NAME targets work; tests must use the
exitcode-stdio-1.0 interface. Use --project-dir or --project-file to select
another project. The command builds with Cabal, exports GHC Core, and executes
an accepted Main.main :: IO () in THC. Use cabal run thc -- --help for the
command-line options. The driver guide
has the options and integration check. thc acquire [TARGET] [FLAGS] uses the
same acquisition path but stops before auditing or executing the guest; a
produced manifest is not a claim that the program is runnable.
A directory containing cabal.project also works for a multi-package build.
The integration fixture includes a data library, a native Template Haskell
helper, an internal library and an executable:
cabal run thc -- run app-run:exe:completed \
--project-dir t/fixtures/run-project --thc-root "$PWD" \
--dist-dir "$PWD/build/run-project"
Cabal builds the native dependencies needed for the helper, and THC runs the executable's accepted Core. The driver uses Cabal's resolved unit IDs and per-component build information for the export.
For native imports, installed-library IO and callbacks, follow the
foreign-code setup. Platform permissions and
resource lifetimes are explicit; see Windows limits for the
native Windows path. General project acquisition (thc build) is currently
available on macOS and Linux. thc repl is not implemented.
Both the bytecode and AST backends execute lazy Core with sharing, closures, recursive bindings, typed constructor fields, local joins, unboxed tuples and sums, SIMD, arrays and mutable references. The bytecode backend is the default. Architecture explains the shared value model and the two execution paths.
Asynchronous exceptions, MVars and STM support concurrent Haskell programs. Interrupted shared thunks retain their unfinished work. Ordinary evaluation bounds nested calls and forcing through saved continuations. Load requests and ordinary executable launches default to async off on both backends: off speculates on a single guest admission origin until guest concurrency is admitted. Explicit async opt-in enables polling immediately. Delimited capture across STM, automatic weak finalization and GC-driven deadlock detection remain unsupported.
The public thc:runtime API exposes permissions,
thread and affinity observations, memory/GC statistics and structured tracing.
Availability and measurement scope are explicit. THC.Internal.JIT supplies
separate unstable diagnostics. Polyglot calls and the
JVM embedding API support JavaScript and host callers;
foreign code describes native imports and exports.
THC remains experimental. The behavior reference lists
current semantic and platform limits, and the generated
primop checklist inventories operations. Package support also
requires complete dependencies, including cold error paths. Use
--verify-artifacts for the driver's pre-launch audit and artifact checks.
Diagnostic mode leaves explicit traps at unsupported sites; completing one path
in that mode does not establish support for its whole closure.
The test script prepares native GHC fixtures and builds the runtime. The scalar
runner then calls one exported entry; --compile requests guest compilation and
checks that code was installed:
bin/try.sh
bin/run.sh build/core/THC.Prim.Test.json,build/core/Fixtures.json sumLoop 100000 --compile
The bytecode backend is the default. To use the AST backend:
THC_BACKEND=ast bin/run.sh build/core/THC.Prim.Test.json,build/core/Fixtures.json sumLoop 100000 --compile
THC launchers disable Graal's automatic loop vectorization by default. Explicit JDK
Vector API operations remain enabled. To enable automatic loop vectorization for a
run, set JAVA_OPTS=-Djdk.graal.VectorizeLoops=true. This is a JVM-wide compiler
setting, so it applies to both backends. Gradle application and test JVMs use the
same default and respect explicit JVM properties; embedders choose their own JVM
options.
Development checks and benchmarks live in src/diagnostics/. Build their separate
build/diagnostics/thc-tools.jar with ./gradlew toolsJar; the try scripts do
this alongside installDist. Direct Java launches add that JAR to the runtime
classpath. The production distribution and JVM API reference exclude these tools
and the embedding/polyglot examples in src/examples/.
For the native-checked library suite and diagnostic Map workload:
bin/try-libraries.sh
THC_DIAGNOSTIC_UNSUPPORTED=true bin/try-map.sh
The latter builds, updates, queries and folds a histogram using the actual
Data.Map.Strict implementation from containers-0.8.
Use native-GHC comparisons and compiler graphs to evaluate the workloads and configurations you intend to run. Typed runtime storage and successful guest compilation do not by themselves establish allocation removal or a speedup.
make -C bench kernels OUT="$PWD/work/bench-kernels"
THC_DIAGNOSTIC_UNSUPPORTED=true make -C bench map OUT="$PWD/work/bench-map"
THC_BACKEND=ast THC_DIAGNOSTIC_UNSUPPORTED=true make -C bench map OUT="$PWD/work/bench-ast"
Direct benchmark JVMs also disable automatic loop vectorization by default. Set
JDK_JAVA_OPTIONS=-Djdk.graal.VectorizeLoops=true for an explicit comparison
with it enabled; inherited JVM options preserve caller choices.
bash bin/test-benchmark-entrypoints.sh checks paths and launch defaults without
preparing fixtures or measuring.
Choose an unused output directory; the targets prepare and check their inputs before measuring, then overwrite files in the supplied directory. Benchmarks vary their inputs, consume the results, warm the JVM and compare against native GHC. Graph capture is a separate run. Diagnostic Map execution does not establish strict support for its entire dependency closure.
The current runtime guides describe implemented protocols and opt-in experiments.
src/compiler/ exports executable Core from GHC, with
representation and evaluation information.src/driver/ builds the
command-line driver; t/ contains its Cabal fixtures and checks.src/cbd/ implements compact Core storage and inspection.src/runtime/ provides the public Haskell runtime API.nih/pinned/ pins upstream Git submodules; nih/licenses/ collects notices.src/main/ and src/test/ contain the Truffle
runtime and its tests.src/examples/ contains Haskell programs and the native oracle.bin/ contains build, audit, benchmark and graph drivers.make docs (also needs Pandoc).thc build, thc acquire and
thc run, including their current limits.The older runtime experiments live on the legacy branch.
THC uses the same license as Cadenza: UPL-1.0 AND BSD-3-Clause. See LICENSE.txt and NOTICE.md for the terms and attribution notices.
Unless you explicitly state otherwise, contributions submitted for inclusion are provided under these same terms.
Contributions and bug reports are welcome!
Please feel free to contact me through GitHub
or on the ##thc IRC channel on
irc.libera.chat (Libera Chat).
-Edward Kmett
1,014 followers · starred Sep 2026
41 followers · starred Sep 2015
35 followers · starred Sep 2015
7 followers · starred Dec 2016
Java
65.3%
Haskell
23.7%
Python
9.3%
The Turbo Haskell Compiler
See the codeHaskell on Truffle/Graal.
I'm experimenting with using GHC as a frontend for a high performance Haskell implementation on the JVM. GHC does the parsing, type checking, desugaring and optimization. THC takes the resulting Core and gives Graal something it can specialize.
GHC already knows quite a lot about compiling Haskell. The intention is to keep that information around long enough to use it.
For native Windows, use the PowerShell build and test guide. It lists the required tools and current platform limits.
You need GHC 9.14.1 (including ghc-pkg and runghc), cabal-install 3.16,
GraalVM 25.3.4.1 / JDK 25, and Python 3.12+. Put GHC on your PATH and
point JAVA_HOME at GraalVM. On macOS, use the bundle's Contents/Home
directory. The Gradle wrapper downloads its dependencies on the first build.
Linux x86_64 setup includes clang and GMP development headers and libraries
(for example, libgmp-dev on Debian/Ubuntu) for native package dependencies.
The JDK is stock, but THC's default distribution includes patched Truffle API, runtime and Sulong JARs. Their patch inventory and shared-host effects are part of the embedding contract: some changes affect other languages using the same runtime, including across separate contexts and engines.
From the repository root:
git -c core.autocrlf=false submodule update --init --depth 1
export JAVA_HOME=/path/to/graalvm
export PATH="$JAVA_HOME/bin:$PATH"
make
This runs ./gradlew installDist for the JVM runtime and cabal build
for the Haskell library and driver. The thc library contains the GHC Core
plugin; Cabal builds it alongside the thc executable. Both builds are
incremental. make runtime and make haskell build either part separately.
make keeps Gradle's cache in .gradle-user-home; set GRADLE_USER_HOME to
share a cache across checkouts.
THC needs Core, GHC's intermediate representation, for both your program and
the Haskell libraries it calls. Standard GHC installations usually omit the
complete Core for their bundled libraries, including base and ghc-internal.
Compiling those libraries with
-fwrite-if-simplified-core
keeps every binding in their .hi interface files so THC can load it. The flag
must be used when building the libraries; adding it only to your program
cannot recover Core from libraries already installed without it.
With a standard GHC 9.14.1 installation, the default --installed-core pinned
provider rebuilds selected bundled libraries from the pinned GHC release and
caches their complete Core. Their versions, modules and dependencies must match
the selected installation. The first acquisition includes this library build.
Alternatively, build GHC's libraries with the flag above and select their
retained Core with thc run TARGET --installed-core required.
make check-ghc-core GHC=/path/to/ghc checks whether the installed libraries
contain the needed Core. The GHC build guide shows how to
apply the flag when building GHC 9.14.1, the version THC currently supports.
Fixture tests also require CMake 3.24+ and Ninja. make fixtures builds all
admitted fixture files. For routine development, select an exact class, for
example make test TESTS=thc.RuntimeTest.
make test-modes TESTS=thc.RuntimeTest runs both handoff modes from shared fixture
files; see the fixture graph and current limits.
make clean removes build products.
make distclean also removes the checkout's Gradle caches.
make run ARGS='--help' builds and runs the driver; the equivalent Cabal command
is cabal run thc -- --help.
Run the included smoke test through THC:
cabal run thc -- run completed --project-dir t/fixtures/run-pure \
--thc-root "$PWD" --dist-dir "$PWD/build/run-package"
This fixture checks a mutable reference and returns () without printing.
Inside your package, thc build [TARGETS...] follows Cabal build selection and
acquires dependency Core without launching an application. Omit targets for the
current package, use all, or select libraries and executables together.
Use thc run [TARGET], following cabal run target syntax, to build and execute.
For an installed driver in your own project:
thc run my-package:exe:my-program --thc-root /path/to/thc
Omitting the target selects the current package's sole buildable executable,
otherwise its sole buildable runnable component. Explicit PACKAGE:exe:NAME,
PACKAGE:test:NAME and PACKAGE:bench:NAME targets work; tests must use the
exitcode-stdio-1.0 interface. Use --project-dir or --project-file to select
another project. The command builds with Cabal, exports GHC Core, and executes
an accepted Main.main :: IO () in THC. Use cabal run thc -- --help for the
command-line options. The driver guide
has the options and integration check. thc acquire [TARGET] [FLAGS] uses the
same acquisition path but stops before auditing or executing the guest; a
produced manifest is not a claim that the program is runnable.
A directory containing cabal.project also works for a multi-package build.
The integration fixture includes a data library, a native Template Haskell
helper, an internal library and an executable:
cabal run thc -- run app-run:exe:completed \
--project-dir t/fixtures/run-project --thc-root "$PWD" \
--dist-dir "$PWD/build/run-project"
Cabal builds the native dependencies needed for the helper, and THC runs the executable's accepted Core. The driver uses Cabal's resolved unit IDs and per-component build information for the export.
For native imports, installed-library IO and callbacks, follow the
foreign-code setup. Platform permissions and
resource lifetimes are explicit; see Windows limits for the
native Windows path. General project acquisition (thc build) is currently
available on macOS and Linux. thc repl is not implemented.
Both the bytecode and AST backends execute lazy Core with sharing, closures, recursive bindings, typed constructor fields, local joins, unboxed tuples and sums, SIMD, arrays and mutable references. The bytecode backend is the default. Architecture explains the shared value model and the two execution paths.
Asynchronous exceptions, MVars and STM support concurrent Haskell programs. Interrupted shared thunks retain their unfinished work. Ordinary evaluation bounds nested calls and forcing through saved continuations. Load requests and ordinary executable launches default to async off on both backends: off speculates on a single guest admission origin until guest concurrency is admitted. Explicit async opt-in enables polling immediately. Delimited capture across STM, automatic weak finalization and GC-driven deadlock detection remain unsupported.
The public thc:runtime API exposes permissions,
thread and affinity observations, memory/GC statistics and structured tracing.
Availability and measurement scope are explicit. THC.Internal.JIT supplies
separate unstable diagnostics. Polyglot calls and the
JVM embedding API support JavaScript and host callers;
foreign code describes native imports and exports.
THC remains experimental. The behavior reference lists
current semantic and platform limits, and the generated
primop checklist inventories operations. Package support also
requires complete dependencies, including cold error paths. Use
--verify-artifacts for the driver's pre-launch audit and artifact checks.
Diagnostic mode leaves explicit traps at unsupported sites; completing one path
in that mode does not establish support for its whole closure.
The test script prepares native GHC fixtures and builds the runtime. The scalar
runner then calls one exported entry; --compile requests guest compilation and
checks that code was installed:
bin/try.sh
bin/run.sh build/core/THC.Prim.Test.json,build/core/Fixtures.json sumLoop 100000 --compile
The bytecode backend is the default. To use the AST backend:
THC_BACKEND=ast bin/run.sh build/core/THC.Prim.Test.json,build/core/Fixtures.json sumLoop 100000 --compile
THC launchers disable Graal's automatic loop vectorization by default. Explicit JDK
Vector API operations remain enabled. To enable automatic loop vectorization for a
run, set JAVA_OPTS=-Djdk.graal.VectorizeLoops=true. This is a JVM-wide compiler
setting, so it applies to both backends. Gradle application and test JVMs use the
same default and respect explicit JVM properties; embedders choose their own JVM
options.
Development checks and benchmarks live in src/diagnostics/. Build their separate
build/diagnostics/thc-tools.jar with ./gradlew toolsJar; the try scripts do
this alongside installDist. Direct Java launches add that JAR to the runtime
classpath. The production distribution and JVM API reference exclude these tools
and the embedding/polyglot examples in src/examples/.
For the native-checked library suite and diagnostic Map workload:
bin/try-libraries.sh
THC_DIAGNOSTIC_UNSUPPORTED=true bin/try-map.sh
The latter builds, updates, queries and folds a histogram using the actual
Data.Map.Strict implementation from containers-0.8.
Use native-GHC comparisons and compiler graphs to evaluate the workloads and configurations you intend to run. Typed runtime storage and successful guest compilation do not by themselves establish allocation removal or a speedup.
make -C bench kernels OUT="$PWD/work/bench-kernels"
THC_DIAGNOSTIC_UNSUPPORTED=true make -C bench map OUT="$PWD/work/bench-map"
THC_BACKEND=ast THC_DIAGNOSTIC_UNSUPPORTED=true make -C bench map OUT="$PWD/work/bench-ast"
Direct benchmark JVMs also disable automatic loop vectorization by default. Set
JDK_JAVA_OPTIONS=-Djdk.graal.VectorizeLoops=true for an explicit comparison
with it enabled; inherited JVM options preserve caller choices.
bash bin/test-benchmark-entrypoints.sh checks paths and launch defaults without
preparing fixtures or measuring.
Choose an unused output directory; the targets prepare and check their inputs before measuring, then overwrite files in the supplied directory. Benchmarks vary their inputs, consume the results, warm the JVM and compare against native GHC. Graph capture is a separate run. Diagnostic Map execution does not establish strict support for its entire dependency closure.
The current runtime guides describe implemented protocols and opt-in experiments.
src/compiler/ exports executable Core from GHC, with
representation and evaluation information.src/driver/ builds the
command-line driver; t/ contains its Cabal fixtures and checks.src/cbd/ implements compact Core storage and inspection.src/runtime/ provides the public Haskell runtime API.nih/pinned/ pins upstream Git submodules; nih/licenses/ collects notices.src/main/ and src/test/ contain the Truffle
runtime and its tests.src/examples/ contains Haskell programs and the native oracle.bin/ contains build, audit, benchmark and graph drivers.make docs (also needs Pandoc).thc build, thc acquire and
thc run, including their current limits.The older runtime experiments live on the legacy branch.
THC uses the same license as Cadenza: UPL-1.0 AND BSD-3-Clause. See LICENSE.txt and NOTICE.md for the terms and attribution notices.
Unless you explicitly state otherwise, contributions submitted for inclusion are provided under these same terms.
Contributions and bug reports are welcome!
Please feel free to contact me through GitHub
or on the ##thc IRC channel on
irc.libera.chat (Libera Chat).
-Edward Kmett
1,014 followers · starred Sep 2026
41 followers · starred Sep 2015
35 followers · starred Sep 2015
7 followers · starred Dec 2016
Java
65.3%
Haskell
23.7%
Python
9.3%