Leveraging a blazing-fast runtime: a (new) Go backend for PureScript

Hello everyone! :wave:

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 official tests are already starting to show green and positive results. :green_circle:

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:

  1. The optimizer & bootstrapping :brain:
    While purescript-native was written in Haskell and parsed raw CoreFn, gopurs is written 100% in PureScript. It plugs directly into the purescript-backend-optimizer (which I also used for phpurs). This means we do not reinvent the wheel for classical optimizations. The gopurs compiler 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 via spago and npm).

  2. Heap vs. Stack: a new memory layout for Go :zap:
    Dynamic typing in statically typed languages like Go often relies heavily on interface{} (or any). However, assigning primitive values to interfaces forces them to escape to the heap (Boxing), generating massive Garbage Collector pressure. For gopurs, I ran extensive benchmarks and decided to completely ditch any. Instead, the runtime uses a universal flat Value struct (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 dynamic any approach took ~9 seconds, and the Value struct solution completed in ~240ms! The performance difference is staggering, completely bypassing the GC overhead in heavy iterative loops.

  3. Up-to-date with modern PureScript :package:
    purescript-native hasn’t seen major activity in a while. gopurs aims 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.

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. :heart:

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 :sunflower: (it will still be pretty messy until then)

Love :heart: