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:
- Our official ES5 grammar compiling a large (742K) JavaScript file.
- 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.