Purs-backend-es v1.3.0 released

I’m super excited to announce the latest 1.3 release of purs-backend-es, an alternative backend for modern ES code based on the purescript-backend-optimizer pipeline. This release contains a host of ES-specific improvements to bring it up to parity with the current JS backend (and beyond).

The JS backend employs a few optimizations specific to Effect and ST for writing fairly low-level code which generates more performant imperative JS. Previous versions of purs-backend-es did not support these optimizations, making performance potentially regress in some cases.

  • Loop inlining (foreachE, forE, whileE, etc). By inlining loops and their bodies, we can avoid thunk allocations for Effect on each iteration.
  • STRef unboxing, which can eliminate the {value: ...} box for references in some specific cases.

purs-backend-es can now perform these optimizations, but also a lot more!

  • General Ref unboxing, which applies to both Effect and ST.
  • Inlining of STArray and STObject operations (array push, object update, etc).
  • More robust and reliable ST transformations (due to the powerful inliner in purs-backend-optimizer).

As an anecdote, several years ago I wrote halogen-vdom with the goal of writing as much in PureScript as possible while still retaining production quality performance. I was fairly successful with about 95% of the code being written in PureScript, however, the core diffing algorithms were written in FFI for performance. I can confidently say that is no longer necessary! Compare the original FFI, and code that can be generated by purs-backend-es:

This is equivalent, and likely faster given that for/of iteration over Object.keys is more performant than for/in iteration nowadays. My hope and goal is that “write FFI code for performance” should not be a thing.

23 Likes

A less intense example shows that lower level array operations could be implemented in ST and achieve comparable code to current FFI. Here is an implementation of Array’s bind:

test2 :: forall a b. (a -> Array b) -> Array a -> Array b
test2 f as = STArray.run do
  bs <- STArray.new
  ST.foreach as \a -> do
    let as' = f a
    ST.foreach as' (void <<< flip STArray.push bs)
  pure bs

And the generated code:

const test2 = f => as => {
  const bs = [];
  for (const a of as) {
    for (const $0 of f(a)) {
      bs.push($0);
    }
  }
  return bs;
};

Which is exactly what I’d hope for :smile:

3 Likes

Curious: how much would be different had browser actually supported proper tail calls back in ECMAScript 2015?

1 Like

Proper tail calls can make functional abstractions safe (as long as they are CPS’ed) and composable, but not necessarily fast (though likely faster than userland trampolines). You could setup a comparison in JSC (which supports PTCs). Using JSC is one reason why bun is interesting to me.

1 Like

This is very exciting stuff, super happy that the community has the tools, time and effort available to focus on these kinds of things now.

2 Likes

This is sooo exciting, I’m jumping up and down.

Do you know if the ES backend is being used in any large-ish production environments yet?

2 Likes

We are hopefully integrating this into the next major release of the Arista NDR product, which I would consider a large environment.

4 Likes