Leveraging modern low-level: a Rust backend for PureScript

Hello everyone,

As some of you might know, I recently worked on phpurs and gopurs. Building these backends has been an incredible journey, but it also highlighted some hard boundaries. Specifically, gopurs showed me the limits of Go when it comes to implementing advanced memory management techniques like Perceus and FBIP (Functional But In-Place), inspired by Koka/Lean.

I am currently in a sabbatical phase of my career, without an official position, and I am ready to invest the time to create pure tools that will sustainably serve the ecosystem. I absolutely love PureScript, and I believe it deserves the best tools.

Looking at the current landscape, the planets are perfectly aligned to attempt something experimental but ambitious: a native Rust backend: purust.

Here is why I think the timing is right:

  • PureScript is strict: Unlike Haskell, PureScript’s strict evaluation makes it the perfect candidate for Perceus-style static Liveness Analysis.
  • The backend-optimizer is proven: Having a flattened, uncurried, and optimized AST ready to be consumed removes the heaviest burden of writing a backend.
  • TASTs are available: Having the type information attached to the CoreFn is the missing piece of the puzzle. Still local (on my computer), but completely unlocked the perfs on the Go compiler.
  • The Value architecture worked: gopurs proved that dynamically managing Boxed vs. Unboxed data structures works well. In Rust, we can take this even further.
  • AI accelerates the process: Today’s AI tools allow us to iterate, prototype, and test complex compiler ideas much faster than before.

At first glance, Rust might seem like a hostile target. The Borrow Checker, lifetimes, and the heaviness of standard Rc or Arc seem to clash with massive graph sharing and closures. However, the initial roadblocks for phpurs and gopurs also seemed insurmountable at first, yet they were overcome. By generating highly specific unsafe Rust code encapsulating a custom PerceusBox, we can bypass the Borrow Checker entirely and leverage LLVM’s raw power.

I love mathematics, and in math, we know that when abstract shapes seem to fit together, even from a distance, intuition is often the only argument that leads us to incredible new territories. That’s how I feel today, thinking about this potential PureScript-to-Rust compiler.

Engineering often begins with a dream, much like reading Jules Verne before building the submarine.

I am currently working on the foundations of this project. The repository will be made public soon, provided that the initial proofs of concept, facts, and benchmarks prove the architecture is credible.

It is absolutely worth a try. In the worst-case scenario, I will potentially save time for anyone else who might have the same idea, by explaining and documenting the limits of this exercise.

At best, a whole new avenue of joy opens up for PureScript.

Hi. Wow. I can hardly believe my eyes. You’re building yet another backend.

The memory-ownership argument makes sense to me: a tracing GC does rule out precise Perceus-style ownership, and that’s a real limit rather than a design mistake on your part.

What I don’t follow is why that points at Rust. If the plan is unsafe code with a custom PerceusBox bypassing the borrow checker, then lifetimes, ownership and the aliasing discipline are all switched off, and what’s left is LLVM with Rust syntax on top. Koka, which is the reference implementation of the technique you’re after, targets C. purec already exists here and could use some love perhaps. So what does Rust give you once the checker is out of the picture that C or Zig or direct LLVM IR wouldn’t?

The part that worries me is that Rust’s unsafe is stricter than C rather than looser. &mut emits noalias, and provenance and Stacked Borrows make aliasing patterns that are fine in C into UB that LLVM is entitled to miscompile. A refcounted aliased object graph is close to the worst case for that. If you go this route I’d want to know the soundness plan early: is the whole thing running under Miri in CI, and what are the documented invariants on PerceusBox? That’s the sort of thing that’s very hard to retrofit.

Separately, and this is the thing I’d most like to see regardless of which backend wins: tcorefn is still local on your machine and it’s now a dependency of three projects. It’s the most reusable thing you’ve built and the only piece that survives a pivot. Even a rough patch and a format sketch in a public repo would let other people build on it and would make both backends reproducible.

And a straight question, no judgement in it: is the gopurs release still happening? You mentioned cleanup, Aff and an official release yesterday, and I’d like to know whether to treat gopurs as something to plan around or as a finished experiment.

Hi @harryprayiv,

Thank you so much for this high-quality feedback. These are exactly the kind of technical challenges and hard questions I was hoping to discuss. You hit the nail on the head on every point. Again, I think we’re kind of synchronised on the questions to deal with.

Let me start with your last question because it sets the context for everything else: gopurs is my absolute priority.
The official release, the cleanup, and the Aff integration for gopurs are happening. You can treat it as a project to plan around. purust is currently an exploratory side-project (a non-priority baby that I tinker with), meant to push boundaries and see if the Perceus architecture can theoretically land in the PureScript ecosystem.

Regarding your technical points, here is my thought process:

1. Why Rust over C, Zig, or direct LLVM IR?
You are completely right: bypassing the borrow checker leaves us with LLVM + Rust syntax. If memory ownership was the only metric, C or Zig would give me much more freedom.
However, this choice is entirely driven by the Ecosystem and Developer Experience (DX).
By targeting Rust, purust gets to leverage Cargo for seamless dependency management and cross-compilation. I’d like to open this growing world in the most direct and seamless manner. It gives us a golden path to map PureScript’s Aff directly onto the Rust async ecosystem (like tokio), and allows developers to easily write FFI bindings to world-class crates (serde, axum, etc.). Rust is treated here as a high-level, portable assembler equipped with the best package manager in the industry (IMHO).

2. The Strictness of Rust’s unsafe and Aliasing (UB)
This is the most critical hurdle, and you are 100% right to point out that Rust’s &mut emitting noalias is stricter than C. A refcounted aliased object graph is indeed a minefield for Stacked Borrows.
The soundness plan relies strictly on the mathematical guarantee of Perceus/FBIP: we never form a Rust &mut reference unless the reference counter is exactly 1 (meaning we have absolute proof of unique ownership). For any shared state (ref count > 1), the data is accessed via raw pointers (*const T) without ever upgrading them to Rust references.
But you are entirely correct: human intuition is not enough here. Running the generated code under Miri in CI will be a mandatory requirement to mathematically prove the soundness of the PerceusBox invariants and avoid LLVM miscompilations. Still playing with it.

3. The tcorefn (TAST) format
I couldn’t agree more. The typed AST is indeed the most universal and reusable piece of architecture that came out of this journey. It completely unlocked the performance of the Go compiler for me. Releasing a sketch/draft of this format in a public repo so that other people can build on it is definitely on my roadmap. It survives any backend pivot and should belong to the community.

Thanks again for the peer review and the Miri advice. It’s incredibly valuable to have this kind of stress-test before diving too deep into the code! You’re definitely a friendly ally to me, in these experiments.