Hello everyone! ![]()
A few weeks ago, I introduced phpurs (here), an alternate backend to compile PureScript into modern PHP.
Today, I’d like to announce that I’m taking all these hard-won lessons and applying them to a new project: gopurs, a modern PureScript-to-Go compiler. Still an experimental WIP, but the first 50% official tests are already starting to show green and positive results. ![]()
Some of you might immediately (and legitimately) think: “Wait, doesn’t purescript-native (here) already do this?”. And yes, it does!
However, reading through the discussions and challenges raised by users in its thread (like initialization orders, performance limits of interface{} or module qualifications), I realized that the ecosystem has evolved drastically over the last few years. This unlocked new architectural paradigms that make (IMHO) building a completely new Go backend highly relevant today, specifically to address these exact limitations:
-
The optimizer & bootstrapping

Whilepurescript-nativewas written in Haskell and parsed rawCoreFn,gopursis written 100% in PureScript. It plugs directly into thepurescript-backend-optimizer(which I also used forphpurs). This means we do not reinvent the wheel for classical optimizations. Thegopurscompiler can then strictly focus on translating this highly-optimized AST into idiomatic, performant Go code, adding further optimizations when it can, for Go (specifically). And it remains fully accessible to anyone in the PureScript ecosystem (installable viaspagoandnpm). -
Heap vs. Stack: a new memory layout for Go

Dynamic typing in statically typed languages like Go often relies heavily oninterface{}(orany). However, assigning primitive values to interfaces forces them to escape to the heap (Boxing), generating massive Garbage Collector pressure. Forgopurs, I ran extensive benchmarks and decided to completely ditchany. Instead, the runtime uses a universal flatValuestruct (a tagged union), inspired by V8 or LuaJIT. This ensures that dynamic operations stay mostly on the stack, when possible.
To give you an idea, on a 1 billion operations benchmark, native static Go took ~250ms, a dynamicanyapproach took ~9 seconds, and theValuestruct solution completed in ~250ms too. The performance difference is staggering, completely bypassing the GC overhead in heavy iterative loops. But it’s for a simple micro-benchmark where the Go compiler can aggressively optimize the tight loop. On real-world, complex code (with deep ADTs, nested Records, or higher-order functions), the runtime overhead of dynamic tag checks accumulates and limits AOT optimizations (see the solution of TASTs in the following messages). -
Up-to-date with modern PureScript

purescript-nativehasn’t seen major activity in a while.gopursaims to be fully aligned with the current v0.15+ ecosystem (and v0.16+ soon). I am currently mirroring the standard libraries (gopurs-prelude,gopurs-effect, etc.) to provide native Go FFIs. -
Up-to-date with modern Go

Go has evolved significantly since the early days of purescript-native. By targeting modern Go (1.18+),gopurstakes full advantage of recent advancements: from the much-improved Garbage Collector and Goroutine scheduler (makingAffmappings practically free, more details on 5.), to the introduction of Generics that open new doors for safe, typed, and performant FFI definitions. -
Native Parallelism behind
Aff
One of the major historical hurdles was properly mapping PureScript’s complex asynchronous monad (Aff) without introducing massive overhead or runtime friction. From what the README says,purescript-nativedoes not support it. Today, the game has changed: asynchronous code and multi-core parallelism are now practically free and feel completely native, thanks to goroutines.
I want to deeply acknowledge the fantastic work done by Andy on purescript-native. It really paved the way. It is always much easier to come second and learn from the limits encountered by the pioneers. This project is a response, an extension of his work, an apology, a love letter. It simply couldn’t exist without it. All my gratitude for his initial effort. ![]()
As we all know, Go offers exceptional concurrency (goroutines fit perfectly with Aff), amazing speed, and rock-solid native binaries.
Being able to bring a mesomorphic approach to this ecosystem is a great opportunity: the solid and safe part of PureScript, with the liquid and battled-test part of Go’s runtime (and FFIs). I often use this metaphor when talking about PureScript: like our modern screens (i.e. liquid crystals), it’s a language that allows us to elevate the discussions and rise above the dialectical processes, hybridizing the strengths rather than canceling them out.
The repo may be ready for production soon, this summer
(it will still be pretty messy until then)
Love ![]()
(Edit)
Since I posted this first message, here are the main milestones achieved (in the following messages):
- 100% of the official tests are green (more details here)
- Successive optimizations (e.g. TCO, inlining, partial monomorphization…) + Typed AST, a.k.a. TAST (
tcorefn.json), unlocking massive performance gains (x10) over naive compilation. Single-fiber benchmark here. More details here, or here - Complete overhaul of the FFI DX with a WASM parser, allowing you to write idiomatic Go (uncurried functions, native types…) without boilerplate and compiler leaks. More details here
- Full and native support for
Affvia goroutines, providing true multi-core parallelism for free (another xN factor for perfs, with N cores in your machine, as you can see here). More details here. This is quite similar to the performance gain seen by TS devs on v7. But here, that’s not limited to comptime: this is pure runtime progress. - Successful validation of the compiler on 100% of the unit tests for a real-world, complex project. More details here
- Same than just above, but with integration tests (Postgres, S3, RabbitMQ…)
-
Module validation: Validate existing official tests module by module (
gopurs-*, ~ 50 modules). More details here
WIP:
-
Module validation #2: Add non-official tests for perfect coverage, module by module (
gopurs-*, ~ 50 modules)
TODO:
- General code cleanup (it’s still quite messy)
- … (probably more to come)
