ruby-syntax-tree.github.io

Over the weekend I cobbled together ruby-syntax-tree.github.io, and I thought I’d share a quick post about what it is, how it works, and what I learned while I built it.

What is it?

A lot of good tools exist in the Ruby ecosystem that allow you to run some version of Ruby in the browser. I’m talking about tools like try.ruby-lang.org, runruby.io, and sorbet.run.

Usually getting Ruby to run in the browser entails using emscripten to compile C to WebAssembly or using Opal to compile Ruby to JavaScript. Recently, however, the Ruby Association funded a project to compile Ruby to WebAssembly using the WASI ABI. Using this new functionality, you can compile Ruby itself or a Ruby application into a .wasm file that you can execute natively in the browser or through a polyfill. (You can actually execute it on any WebAssembly runtime, but for my purposes the browser will do.) For more information on the WASI Ruby project, check out the final report.

So, to get to the titular question of this section. ruby-syntax-tree.github.io is a website that uses the new WASI ABI functionality of Ruby to compile a .wasm file containing both the Ruby runtime and the source for the Syntax Tree gem. It then boots a virtual machine within the browser and uses it to transpile your Ruby into equivalent s-expressions.

How it works

Let’s start from the ground up. The first part of building the site was to build the .wasm file containing the Ruby runtime and the Ruby files necessary to run Syntax Tree. Following instructions from the ruby/ruby.wasm README, I ran a bunch of commands locally to get my own machine up and running. Once I verified that I had everything I needed, I replicated that process in a Rakefile.

One of the trickier parts was including Syntax Tree itself. I briefly considering including it as a git submodule so that it could be mounted as part of the wasi-vfs build process. I ended up scrapping that solution since dependabot wouldn’t be able to automatically update it, and I realized that if I ever wanted any other gems loaded I wanted a reproducable solution.

Instead, I ended up using bundler as normal to install the dependencies. Once they were installed, I knew they existed somewhere on the system. I also knew that require "bundler/setup" sets up the load paths so that you can require gems my name. So I decided to piggy-back on this functionality to copy the gem contents into the mounted directory. I found the right directory based on the $: load path global variable.

With everything in place, I used wasi-vfs to build the file. For packaging this file into the built web application, I used esbuild. They don’t have built-in support for .wasm files, but adding support isn’t hard. You can write your own plugin by mostly copy-pasting from their docs. That resulted in the esbuild plugin here. That makes it so that you can import .wasm files as you would normally import modules. The default export is a function that accepts the imports for the module, and it asynchronously returns the module. You can then use the ruby-head-wasm-wasi npm package that Ruby now ships to wrap up the module and provide an eval function to evaluate RUby code.

Once the module is imported, it’s a matter of requiring the correct files at the top of the file. That’s accomplished by requiring the native gems that we need, then adding lib directory we put the Syntax Tree gem into early to the load path, then requiring it. All of that is encapsulated in the createRuby.ts file. The actual web application is a relatively standard React/TypeScript application. Since it’s not the novelty of this post, I won’t cover it, but you can check out the source here.

What I learned

I learned a bunch of stuff with this experiment! Here are a couple of things that I found useful that I feel are worth sharing:

At some point I’d like to add the ability to format the source, add a better editor, and general improve the styling and UX. But for now, the current state is up at ruby-syntax-tree.github.io.

← Back to home