WebAssembly Backend Redux!

Hi folks!

I’d like to share a project I’ve been working on: a WebAssembly backend for PureScript.
You can see the codebase including some examples, documentations and benchmarks here:
https://github.com/purs-wasm/purescript-backend-wasm

Actually, I tried to take on the same challenge about three years ago, but at the time it didn’t go very far due to my lack of compiler development experience. Since then, though, I’ve spent a lot of time studying compiler theory, building a few language implementations of my own, and—perhaps most importantly—the arrival of coding agents (honestly, that’s probably been the biggest factor :smile:). With all of that behind me, the project is finally moving again!

It’s still very much at the proof-of-concept stage, but it’s already capable of handling a fair amount of PureScript:

  • Higher-order recursive functions and partial application—the essentials of functional programming
  • ADTs, pattern matching, arrays, and records
  • Typeclass polymorphism
  • Full Prelude support
  • A few optimizations such as typeclass dictionary elimination and primitive inlining (still a work in progress!)

There’s still a long way to go before it’s truly practical, but given how quickly things have been progressing over the last few days, I’m starting to think it won’t be too long before we can write reasonably sized applications in PureScript, compile them to wasm binary, and run them from browser or node host environment.

I’ll keep working on it and share more updates as things progress. So please stay tuned!

6 Likes

I’m glad to see this being built. I’ve personally been looking for something like this for years now. Thanks for taking it on. Good luck!

2 Likes

Bring it on!

Post must be at least 20 characters.

3 Likes

i’m thrilled to announce the v0.1 release of purs-wasm! :tada::tada::tada:

You can install it via npm:

npm i -g purs-wasm

Or via Nix if you prefer:

nix run github:purs-wasm/purescript-backend-wasm#purs-wasm -- build -e Main

Documentation for both app developers and future contributors is available at https://purs-wasm.github.io/documentation*

Please note that this is still a very early alpha release with several limitations, but you can already use it to compile your PureScript applications to WebAssembly.

If you run into any issues or unexpected behavior, please let us know by opening an issue!

3 Likes

I decided to take a look and ended up building a WASM branch of my SHA3 module. The requirements of SHA3 uncovered some missing bitwise operators for Integers. So, I forked and added those.

Then, in attempting to benchmark my SHA3 implementation, I uncovered some foreign effects being silently dropped. So, I drilled down and carefully fixed that and built some tests to prevent regressions (since those failures were not covered by tests). I then opened a pull request and tried to fulfill all of the requirements and guidelines including regression tests and benchmarks against the snapshot.

Looks good! Hopefully more people will come along and slowly wear the rough edges away.
It was definitely a learning experience. If I crammed too much into one PR, I apologize. The changes just kept piling up and I can’t/don’t quit until things work better or the same as before I started.

I’ll keep trying to use it in places that make sense and submitting pull requests as I think of them.

Thanks for building this!

4 Likes

Thank you for opening the first community contributor PR to purs-wasm!
It’s also really exciting to hear that you’re already using it in your PureScript projects — that means a lot :pray:
I’ve merged the PR. Thanks again, and I’m looking forward to seeing more feedback and contributions in the future!:bowing_woman:

3 Likes

No problem! It’s truly an honor to be part of the first community contribution on such a cool/necessary project.

Here’s where I used it this week: purescript-sha3/src at wasm · rowtype-yoga/purescript-sha3 · GitHub

Quick digression:
I’ve been implementing the SHA3 algorithm to help me learn about alternative back ends and benchmark them against each other. It’s nice to have those NIST test vectors to verify against. I personally think that it’s important for Purescript to grow beyond the restrictions of JavaScript even though it may never truly escape it. WASM is a huge step in that direction…as well as the Chez Scheme backend and the others I haven’t yet tried. The racket version of the compiler is another one that piques my interest. THAT’s how good metaprogramming looks.

2 Likes

Thank you! Getting benchmark results from a real, practical implementation like SHA3 — rather than a toy project — and seeing purs-wasm used and compared against other backends is incredibly concrete and valuable feedback for me.

I’m happy to see that the no-FFI version is already about 7× faster than JS.

At the same time, the reality that the Chez native FFI version is still nearly 50× faster is honestly motivating as well. Surpassing that may be difficult, but I definitely want to keep pushing to close the gap.

As for obvious areas where purs-wasm still has room to grow, support for things like Int64 and specialized containers such as I32Array / F64Array look like promising directions that we can probably experiment with relatively soon.

3 Likes

That’s sublime work @katsujukou. Probably one of my favorite repos ever.

3 Likes

:wink: (Int64, I32Array, and F64Array in base)

And a branch where I added both as well as a bootstrap command/shellhook to prevent non-deterministic behavior for anyone that would clone this repo. It is now fully tested and is verified with no regression from the snapshots benchmark:

My personal goal is to get that SHA3 algorithm as close as possible to the Chez-Scheme benchmark by end of next week. According to my estimates, Purescript-wasm can get to (at best) 50% of chez-scheme backend’s performance (thank you, Cisco) but probably not better than that. :slight_smile:

3 Likes

I added some more primitives and low-hanging fruit in my forks to squeeze some more performance out of WASM. Basically, I’ll end up writing four different pull requests (maybe I can compress it into two):

I ended up achieving my goal for the week of getting SHA3 to run a solid benchmark against the other backends. Nothing beats Chez-Scheme’s 60.6 MB/s but purs-wasm comes super close to native NodeJS speeds.

3 Likes

Congrats! That’s super good news @harryprayiv

3 Likes

That’s a really exciting benchmark result! I’m honestly surprised to see Node + native FFI already within reach for us.

Also, thank you for continuing to send PRs — I really appreciate it!

Unfortunately, work has been keeping me a bit busier lately, so I haven’t been able to dedicate as much time to purs-wasm development as before. I’ll get to reviewing and merging them soon, so please bear with me for a little while.

P.S.
Recently, alongside purs-wasm development, I’ve started working toward another ambitious idea again.
That one might catch your interest too :wink:

2 Likes

This is really exciting work!

I’m planning to try running these benchmarks against your backend myself at some point to see if it supports all the test cases, once mature. However, please feel free to beat me to it and open a PR or an issue on the repo if you’d like!

1 Like

I read an article yesterday about how Zig bootstrapped their compiler with WASM. Super interesting. They basically recreated their C/C++ compiler in WASI so they could have a device agnostic compiler/compiler VM.

https://ziglang.org/news/goodbye-cpp/

Compile the compiler to a simple virtual machine - Occasionally, I talk shop with Drew DeVault since he’s working on Hare. We started chatting about compiler bootstrapping and he mentioned OCaml’s strategy off the cuff:

andrewrk: do you care about the “stage 0” bootstrapping process?
ddevault: marginally
andrewrk: idea is to start with literally only a text editor and get up and running
ddevault: text editor? pfft! back in my day we used a magnetized needle and a steady hand
andrewrk: classic xkcd
andrewrk: did I say text editor? I meant hex editor
ddevault: anyway, there’s a bootstrapping path taken by, uh, ocaml I think
ddevault: which is kind of interesting
ddevault: they have a backend for a tiny VM target
ddevault: you implement that VM on your desired platform and you can then compile ocaml proper
andrewrk: ooooooh

This got my creative juices flowing. A bespoke backend just for bootstrapping is a neat idea, but I think the sweet spot for Zig looks different than what it does for OCaml.

There is exactly one VM target available to Zig that is both OS-agnostic and subject to LLVM’s state-of-the-art optimization passes, and that is WebAssembly, using WASI as the operating system abstraction layer.

Absolutely fascinating. I’m wondering if/dreaming whether this technique might be somehow relevant to Purescript despite our differences with OCaml and Zig.

1 Like

Hey @katsujukou and @harryprayiv,

I’ve been following your progress on purs-wasm with great interest, and it’s truly fantastic work. Since we are both tackling the unique challenges of building AOT (Ahead-Of-Time) backends for PureScript, I wanted to share a recent architectural breakthrough I had with gopurs that might directly help with the “typeclass dictionary elimination” and “primitive inlining” optimizations you mentioned.

Like you, I initially built my backend consuming standard CoreFn. But I quickly hit a hard performance ceiling: because CoreFn strips away static type information, the backend is forced into dynamic boxing and heavy runtime checks, which severely limits what an AOT compiler can optimize.

To break this ceiling, I created a fork of the compiler to emit a Typed AST (tcorefn: essentially CoreFn + dataDecls array + ann.type on every single node). Having the strict type available at compile time changed everything:

  • It allowed the backend to completely eliminate primitive boxing.
  • It made it possible to translate typeclass dictionaries directly into native, statically-typed structs (zero-cost abstraction).

This single architectural shift resulted in a huge performance jump, putting the generated Go code on par with V8 on a single thread (at least, because I still work on optimizations). I detailed the process and the benchmark results in my devlog here:

Given how strict WebAssembly is with primitive types (i32, i64, f64), I imagine having access to a fully typed AST would allow purs-wasm to emit native instructions directly without relying on workarounds, and finally solve the dictionary elimination problem?

I am currently preparing a PR to the official PureScript compiler to add --codegen tcorefn as a standard option. I’d love to hear your thoughts on this. If tcorefn could also unlock WebAssembly’s true speed, I think a friendly “coalition” of AOT backends pushing for this feature would be a huge step forward for the whole ecosystem.

Feel free to let me know if you’d like some help exploring TAST for the Wasm project. I’d be more than happy to jump in, or even run some tests on my end to see if it brings a tangible performance gain for WebAssembly.

Keep up the amazing work!

1 Like

I am currently preparing a PR to the official PureScript compiler to add --codegen tcorefn as a standard option.

Sounds great!

I built a Haskell tool for generating a compressed context of a codebase for the sake of an LLM’s limited context window (currently works with Unison, Haskell, and Purescript). GitHub - harryprayiv/chase at unison · GitHub

The naive implementation of Purescript part relied heavily on ‘’CoreFn’’. But I reached a context window just as you did. But instead of extending Purescript, I reached for the actual Purescript CST parser and vendored that. I’m pretty sure that the context I could provide would improve dramatically with TAST’s and I could go back to a less hacky method here using them.

1 Like

That’s great: one more reason to propose this PR. I’ll let you know how it goes!

1 Like

Before things possibly become more official, I thought it might be helpful for you to get an idea of how TASTs are generated: here is the main update.

@harryprayiv You could even try them with chase :package:

I’ll probably do a test against the actual Purescript compiler that I currently vendor. Do you think TAST’s offer advantages there? I ended up using the actual Purescript compiler so I can’t imagine I can get even more type information since I wasn’t going the CoreFn route (since I found it as restrictive as you). Since Chase is proper Haskell it was pretty easy to just use the Purescript compiler and have it roll along with my little tool.

As far as WASM. I think you should try it in a fork to perhaps demonstrate the viability. WASM is already easy to keep in sync with thanks to the Nix lock files. So, you can just fork it and roll with the changes with no worries (as I did when I was trying to squeeze some performance out of it).