Skip to main content

aka "The One that Compiles to Wasm".

After nearly a year of development, we're excited to announce the first beta release of Ohm v18 — the biggest change to Ohm since its initial release. We've totally reworked the core parsing engine to be WebAssembly-based, making parsing around 20x faster on real-world grammars while using a fraction of the memory.

What's new

Every version of Ohm up to v17 worked the same way under the hood: when you call grammar.match(), Ohm walks a tree of parsing expression objects (PExprs), calling eval() on each node. (It's a so-called tree-walking interpreter.) In the process, it builds up a huge parse tree, with each node a separate object that must be managed by the GC.

v18 takes a completely different approach. At build time, the new @ohm-js/compiler translates your grammar into a WebAssembly module, compiling each rule to its own function. The parse tree is allocated into Wasm linear memory, and nodes used a packed representation which is much, much more memory efficient.

Also, the runtime (ohm-js) is now separate from the compiler (@ohm-js/compiler):

npx ohm2wasm my-grammar.ohm   # compile at build time
import {Grammar} from 'ohm-js';

// load and run at runtime
const g = await Grammar.instantiate(fs.readFileSync('my-grammar.wasm'));

How much faster?

We've been benchmarking with two real-world workloads:

  1. Our official ES5 grammar compiling a large (742K) JavaScript file.
  2. Shopify's LiquidHTML grammar, parsing all the Liquid templates from their Dawn theme.

One these two benchmarks, v18 parses about 22x faster than v17, and requires less than 20% of the memory. 🔥

Breaking changes

Along with the new runtime, v18 has a significantly reworked API. Check out the migration guide for all the details on the new API, what's changed, and what's not in v18 yet.

And please note: the new API is still in flux, so expect some changes before the stable release.

Try it out

npm install ohm-js@beta                      # Runtime (production dependency)
npm install --save-dev @ohm-js/compiler@beta # Compiler (dev dependency)

If you want to kick the tires without changing your build setup, there's a compat helper that parses, compiles, and instantiates in one step — just like the old ohm.grammar():

import {grammar} from '@ohm-js/compiler/compat';

const g = grammar('MyGrammar { start = "hello" }');
using result = g.match('hello');

(This compiles on every call, so it's great for prototyping but probably not what you want for production.)

We'd love to hear your feedback on v18. Give it a spin, and let us know what you think on Discord or GitHub Discussions.