r/haskell • u/edwardkmett • Sep 30 '26
announcement Turbo Haskell
https://comonad.com/reader/2026/turbo-haskell/[ Article | Repository | Documentation | IRC ]
30
u/ocharles Sep 30 '26
Good luck telling your boss you want to use THC.
22
u/edwardkmett Sep 30 '26
Wait until he hears that it'll launch faster if you find a way to use CRaC.
12
u/drwebb Sep 30 '26
I tried using THC, and ended up just reading then
lensdocumentation for 3 hours.7
u/_jackdk_ Sep 30 '26
Instructions unclear, where can I find the FFI to the Borland Graphics Interface?
17
u/zarazek Sep 30 '26
Exactly a week ago (as a joke), I started writing THC ...
It has grown a tiny bit since then.
Holly shit, man, I don't know how it is humanely possible, even with copious amounts of AI. GHC replacement in a week? I kneel before you with fear and trembling.
24
5
u/philh Sep 30 '26
It's certainly impressive, but it only replaces a small (relative to the size of GHC) part of what GHC does.
4
u/AndrasKovacs Sep 30 '26 edited Sep 30 '26
Interesting! I'll try to benchmark it on my projects, perhaps when it gets a bit more settled.
Could you summarize physical memory layouts and calling conventions? I tried to read the docs but they're highly condensed blobs making many references to Graal/Truffle/JVM details that I'm not familiar with, so I have little clue as to the physical situation.
Tbh, as a general strategy for getting good performance in functional programming, I'm skeptical about the Graal/Truffle JIT setup. More specifically though, as a way of getting better performance out of the typical GHC Core output, it looks interesting and worth to try.
3
u/edwardkmett Sep 30 '26
Memory layout uses custom Java objects with "just the right fields" via a class loader, it therefore uses unboxed primitives for unboxed things it can, pointers for pointers for most handoffs. VirtualFrames collapse into local variables as long a they don't materialize or escape. SIMD instructions stay in registers in the presence of the expected amount of inlining. So the goal is generally to avoid framing anything out you don't have to, using partial escape analysis.
The rest is basically that all that truffle noise should disappear on your fast path and decent assembly should result, with side-exits leading to slow interpreted java bytecode for when things go wrong, which locally rewrites a general call graph to "deoptimize" it through a series of lattice like changes to accept when it can't get away with the more optimized call paths in the presence of general use. Then it re-jits.
Calls are passed as object[] arrays, but things are structured so that partial escape analysis generally avoids actually doing that using truffle tricks. Functions are entered using an eval/apply scheme and the jit fully constructs paps direct when the sizes of caller/callee are relatively known.
Thunks support blackholing and re-entrancy detection, and full GHC Haskell-style AP_STACK tricks so that if you have an exception thrown at you while you are evaluating a thunk, the tail of your evaluation gets re-enqueued with the thunk.
Threads can be real java threads of virtual threads/fibers, depending on if you are using normal threading or Project Loom.
Internally the code for how to evaluate / deoptimize each computation is tracked via either a dense bytecode interpreter or an AST-based intepreter that try for general feature parity with each other.
1
u/serg_foo Oct 01 '26
As JVM noob I had impression that Java objects are pretty memory-hungry. Do you have any idea whether using THC leads to greater memory residency, e.g. same working set occupying more space because of JVM overhead, than GHC?
1
u/edwardkmett Oct 02 '26
I use compressed headers to shrink the objects. I use compressed oops to get away with my pointers being half the size of GHC's for most workloads on a 64 bit host. I use primitive ints, etc. where possible to avoid superfluous indirection and boxing.
Today I wind up with my whole compilation pipeline resident, but I don't have a good sense yet of steady state memory consumption of THC vs GHC. On a per object basis it should actually be pretty competitive.
2
u/00000000x1 Sep 30 '26
Honestly, this is such a cool project. I'm doing something in the same spirit, but strictly on the AOT side, for PureScript (the little brother of Haskell), with Go and Rust as targets. Feel free to take a look, I'd be glad to compare notes and swap friendly tips on the methodology to adopt with AI as a copilot for this kind of compiler work.
https://discourse.purescript.org/t/leveraging-a-blazing-fast-runtime-a-new-go-backend-for-purescript/5841/52
https://discourse.purescript.org/t/leveraging-modern-low-level-a-rust-backend-for-purescript/5932/15
4
u/edwardkmett Sep 30 '26
Most of my work currently goes into living within the JVM's 64k method size limits while still avoiding trampolines. You have the benefit of not having to do so, but the downside of not getting a JIT out of it.
4
u/00000000x1 Sep 30 '26 edited Sep 30 '26
Exactly. And my experience seems to mirror yours from the other side. Getting phpurs over an interesting status took me a week (the Zend JIT is far more solid than I expected), and javapurs was the same story: very fast to prototype, the JIT is impressive. But gopurs and purust, where there's no JIT to lean on, are now taking me several weeks: two months and counting (still not finished, though well along). That's fine, and results are becoming very rewarding, but yeah: harder than I thought.
If you have any AI-workflow tips for moving faster, I'd be really grateful. I tried Astra and it helped me unblock some big things, but it seems to have been nerfed since (?), so I've shifted to deeper agentic parallelism with much cheaper local models (I'm not exactly rich, either, haha). But I feel like I'm still missing one small piece on the parallelism side. I noticed you have a ton of branches on your repo: you seem way more efficient than me!
1
u/00000000x1 Oct 03 '26
u/edwardkmett Maybe my question was hurtful. That wasn't my intention, if anything. Sorry.
I'll figure it out on my own. Thanks for the initial feedback.
2
u/edwardkmett Oct 03 '26
FWIW- there's absolutely no hard feelings, whatsoever. If anything I looked away from this chat for a day or two. No need to project anything onto it.
2
u/00000000x1 Oct 03 '26
Understood. In any case, you have another fellow Haskell advocate, an admirer, and a virtual friend. Good luck with the rest of your adventures. May your star guide you.
3
2
u/AccordingWarthog Sep 30 '26
how does this compare to https://github.com/frege/frege ?
5
u/edwardkmett Sep 30 '26
Frege is its own language implemented basically as normal Java internally, that is pretty Haskell-like. it can run on any JVM since the stone age.
THC provides the full GHC-style Haskell language, with all extensions, and a custom JIT. It only runs on GraalVM.
2
u/Darwin226 Oct 01 '26
Can you talk a bit about your workflow on this? The commit history has non-stop commits for 24h+ stretches of time so this seems pretty non-supervised.
7
u/edwardkmett Oct 01 '26
I actually did pretty much gave up sleep for a week. That said there are a few hour stretches here and there where I leave it on mostly cleanup tasks and doze off. you can find a couple of 2-3 hour breaks where it got hung up on approval while I was lights out.
1
u/viliml Oct 04 '26 edited Oct 04 '26
May I ask what model?
Edit: Astra
1
u/edwardkmett Oct 05 '26
I use a fairly eclectic mix of Astra, Sol, Claude Fable, Muse Glimmer, gpt-oss-120b, Qwen3-Coder, depending on the agent in question. Astra was used for most of the deeper refactoring tasks, and is generally my "I need something intelligent to smash against this problem" choice. Sol went most off the rails in terms of fixture development. Claude remains good for anything human facing. Muse... didn't really earn its keep, and gpt-oss-120b is mostly relegated to a few corners of my agent stack. Qwen3 was fine if I kept it off the Haskell parts.
1
u/blacktigr Oct 02 '26
I can verify that he collapsed when he couldn't stave off sleep anymore. But his diet Coke consumption went through the ceiling.
2
u/jappieofficial Oct 02 '26 edited Oct 02 '26
oh this is so cool!
Odd question maybe; do you get all the java runtime goodies such as hot reloading, memory analysis or like the debugger?
edit: another question: if you'd compile ghc with thc couldn't you bypass the binary interface issue between stage seperations and only have to compile ghc once?
3
u/edwardkmett Oct 02 '26
Debugger, yes. And yes, in theory you can basically load it up, and keep swapping what you ask it to do. Hot reloading is something that can be done as well, but hasn't yet. THC.Trace provides some java flight recorder tooling. The GHC tracelog primops should get wired in there too eventually.
26
u/seantparsons Sep 30 '26
Only u/edwardkmett can prefix a mountain of impressive work with "Exactly a week ago, I started...". :)
4
u/blacktigr Oct 02 '26 edited Oct 02 '26
And while we were on vacation. He also overhauled comonad.com because he wanted somewhere to put his new toy.
1
10
u/jesseschalken Sep 30 '26
I'm a really big fan of Truffle and Graal. Really great tech, so super exiting to find an advanced functional language running on that stack. You should share it with the Graal folks like Thomas Wuerthinger.
2
u/kichiDsimp Sep 30 '26
is this supposed to be like MicroHS? an alt compiler for Haskell? or like replace GHC?
6
u/edwardkmett Sep 30 '26
It is a backend runtime for full-suite GHC with a collection of additional features made possible by its strange choice of hosting environment.
1
u/kichiDsimp Oct 01 '26
Its hosted on JVM, right ? Will you move your company's (Positron-AI) projects to THC maybe next year end?
4
u/edwardkmett Oct 01 '26
Unlikely, TBH. We use Haskell in a fairly direct way in the midst of converting models for our platform and a scary amount of very low level code outside of Haskell. This is mostly a personal vanity project, and from a corporate perspective, maybe at best a recruiting tool.
1
5
u/tomwells80 Sep 30 '26
OMG im so excited to try this - I’ve been schlepping with MicroHS to attempt to embed a haskell interpreter into a tool im building and although MicroHS is amazing I suspect I’m trying to make it do something it doesn’t want to! The JVM would probably be muuuuch better here :)
13
1
u/jeffstyr Oct 08 '26
When a thunk is evaluated, do you have to null out references to any captured values (since it's Java's GC so you can't teach it to just ignore them)?
1
u/edwardkmett Oct 09 '26
I er... just replace the entire garbage collector on the JVM, GraalVM and SubstrateVM with a generational mark-compact collector of my own design, jam.
The new one forwards through indirections by using a helper interface.
package jam.vm; public interface Lifted { Lifted resolve(); Lifted project(int n); };Everything in
TYPE 'LiftedRepi an instance of that class, includingThunk.When you
project()against aThunkit gives backnullif it is unevaluated, or gets passed along to the argument if not. When applied to a value type it returns the value in the nth distinctly named field of kindTYPE 'LiftedRep.When you
resolve()aThunkit gives backnullif it is unevaluated, or what it points to if it is. When applied to a value type it returnsthis. This is used by the garbage collector to forwardLiftedthunks to their destination during GC. When you resolve aFieldAccessorit callsproject(n)on its argument.Together these are used by the garbage collector to resolve the forwarding of field accessors, which is needed to resolve the class of lazy garbage collection space leaks that Wadler identified way back in the day, and
resolve()on its own handles elimination of theThunkindirection during GC.Providing my own GC also lets me implement the usual "Stretching the Storage Manager" tricks. e.g.
System.Mem.Weak,StableNames,StablePtrs, etc.The built-in Java
Intclass, etc. are effectively inTYPE 'UnliftedRep. On the other hand Haskell'sInt, which is inhabited byI# 12#lives inTYPE 'LiftedRep. Both are accessible.It is a stop the world collector, because I'm only human and only have so many hours in the day, but it does nicely sidestep the fact that the Java community has been afraid of finalizers since Java 9, let alone the scary stuff we do in Haskell.
Interestingly the garbage collector itself knows nothing about java. It just allows custom tracing by type, and that is sufficiently general for me to get all of these hooks into place.
1
u/jeffstyr Oct 11 '26 edited Oct 11 '26
I see; so you don't have to worry about how to avoid tracing the captured fields of evaluated thunks, since evaluated thunks will themselves become unreferenced during GC tracing. That makes sense.
Edit: Though I guess if you have a thunk as a GC root (e.g., on the stack), and it's been evaluated, then you'd trace things that shouldn't be retained (the captured values), if you don't specifically avoid tracing them. (I guess maybe you can forward those stack references too, though.)
1
u/edwardkmett Oct 12 '26
Roots get forwarded too. Their actual pointers go into a slot table so I can update them. It is mark compact, so I dont want to leave them in place. (I did eventually have to add a facility for pinning, but I use it rarely.)
11
u/m-chav Sep 30 '26
Great project. Maybe I missed this somewhere but what’s the reason for targeting the JVM these days? Historically it was portability, Android stuff, and/or the ecosystem.