# Ohm: a PEG parsing toolkit where grammars and semantic actions stay separate

> Ohm pairs a parsing expression grammar language with a JavaScript library, so the same grammar can drive a parser, an interpreter or a compiler. The design choice that matters most is that the grammar file carries no embedded actions.

**ohmjs/ohm** — A library and language for building parsers, interpreters, compilers, etc.

- Repository: https://github.com/ohmjs/ohm
- Stars: 5,551 · Forks: 227
- Language: JavaScript
- License: MIT
- Published: 2026-09-22 · Updated: 2026-09-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/ohmjs-ohm

## The problem Ohm takes on: syntax that changes while you build

Writing a parser by hand means writing a tokenizer, then a recursive descent loop, then error handling, then the same again every time the language gains a construct. Ohm splits that work in two. The grammar is a separate artifact written in the Ohm language, and the JavaScript side only supplies semantic actions. The README states the intent directly: Ohm "completely separates grammars from semantic actions", and that separation is described as improving modularity and extensibility.

The audience is narrow but real. If you are building an interpreter for a teaching language, a compiler for a domain-specific notation, or a linter for a config format nobody has standardized, Ohm gives you a grammar file you can read on its own. The examples directory in the repository is a map of that audience: math, csv, markdown, ecmascript, typescript, simple-lisp, nl-datalog-syntax, indentation-sensitive, incremental, operators, prettyPrint.mjs and viz. A grammar-only project is not the target. Ohm assumes you will also write the semantics.

## How a PEG grammar turns into a parse tree and then into actions

Ohm grammars are parsing expression grammars, described in the README as "a formal way of describing syntax, similar to regular expressions and context-free grammars". The library is the JavaScript interface that turns those grammars into parsers, interpreters and compilers.

The flow has three stages. First you instantiate a grammar with ohm.grammar(), passing the grammar source as a string. Second you call match() on the grammar with an input string, which returns a MatchResult carrying succeeded() and failed(). Third, if you need structure rather than a yes or no, you attach semantics to the grammar and walk the parse tree. The README's own example stops at stage two and prints a greeting.

Two properties of the design are worth calling out because they change how you write rules. Full support for left-recursive rules means left-associative operators can be defined the way you would write them on paper, instead of being rewritten into an iterative form. Object-oriented grammar extension means a grammar can be extended with new syntax, which is how the ecmascript and typescript examples relate to each other rather than duplicating a base grammar. The documentation also points at an interactive visualization of packrat parsing, which shows the parser's execution rather than just its output.

## Installing ohm-js and getting a first match out of a grammar

In Node.js the package is ohm-js. The README lists the install command for each of the three common package managers, and they are equivalent.

```bash
npm install ohm-js
```

The README also gives yarn add ohm-js and pnpm add ohm-js. After installation the package can be required or imported as an ES module: `const ohm = require('ohm-js');` or `import * as ohm from 'ohm-js';`. Ohm is also importable from Deno by URL, `https://unpkg.com/ohm-js@17`, and in a browser it can be loaded from unpkg with a single script tag, either dist/ohm.js or the minified dist/ohm.min.js, which creates a global named `ohm`.

A first real grammar is small. The README defines one inline with a tagged template so that backslashes in the grammar survive JavaScript string handling.

```js
const ohm = require('ohm-js');
const myGrammar = ohm.grammar(String.raw`
  MyGrammar {
    greeting = "Hello" | "Hola"
  }
`);
```

With the grammar object in hand, match() takes the input and returns a MatchResult. The result is not a boolean, so the check is a method call.

```js
const m = myGrammar.match('Hello');
if (m.succeeded()) {
  console.log('Greetings, human.');
} else {
  console.log("That's not a greeting!");
}
```

If the grammar lives in its own file, the README shows reading it with fs.readFileSync('myGrammar.ohm', 'utf-8') and passing the contents to ohm.grammar(). That is the shape most projects end up with, since a grammar in a template literal stops being readable once it grows past a few rules.

There is also a CLI path that avoids a local Node.js installation entirely, using the published image. The README gives this invocation, mounting the working directory at /local so the grammar file is visible inside the container.

```sh
docker run --rm -v $(pwd):/local ohmjs/ohm:latest compile my-grammar.ohm
```

The repository keeps the extended instructions in doc/docker.md, including how to build the image locally and how to set up a development container.

## Where Ohm stops: no generated parser, and no schema for your data

Ohm is a runtime. The grammar is interpreted by the library at the moment you call ohm.grammar(), which means the ohm-js package ships with your application and the grammar source has to be available at runtime, whether as a string, a file read from disk, or a bundler import. If your constraint is a single self-contained parser file with no dependency, this is the wrong tool, and the README does not offer a code-generation path that would change that.

The second boundary is less obvious. Ohm recognizes syntax; it does not decide whether the recognized structure is valid. A grammar that accepts a numeric literal will accept one with a stray digit pattern you did not think about, and nothing in the library will object. Validation of values belongs in your semantic actions or in a separate check. Teams that expect a grammar to behave like a schema will find themselves writing the semantic layer anyway.

The README is also silent on several things a production user would ask about: there is no documented error-recovery strategy, no statement about incremental re-parsing in the README itself, and no performance guidance. The examples directory contains an incremental example and an indentation-sensitive example, so the capability is demonstrated somewhere in the repository, but the README does not describe either. Treat the examples as the documentation for those features.

## Ohm compared with Nearley and Chevrotain

The two tools most often reached for in the same situation are Nearley and Chevrotain, and the difference is architectural rather than a matter of feature lists.

Nearley is also a JavaScript parser toolkit, but its grammars are written in a notation closer to BNF and its compiled output is a parser you can keep in your build. The practical consequence is that the grammar and the actions tend to live together, and extending a grammar means editing the same file that holds the action code. Ohm's separation means the grammar stays a grammar, and you can attach a second set of semantics to the same grammar without touching it. If you have ever wanted a formatter and an interpreter to share one grammar, that is the case Ohm is built for.

Chevrotain takes a third position: you write the parser in JavaScript or TypeScript using its API rather than in a grammar file. That gives you the full language for error handling and custom lookahead, at the cost of the grammar no longer being a standalone artifact you can read or visualize. Ohm's editor and its execution visualization have no equivalent in a hand-written parser, because there is no separate grammar to display. Which of the three fits depends on whether you value a readable grammar file or direct control over the parsing loop.

## Maintenance, releases and what the MIT licence leaves to you

The repository is not archived, and the last push was on 2026-06-19. Recent releases are v17.3.0 on 2025-12-20, v17.4.0 on 2026-02-12 and v17.5.0 on 2026-02-14. Release cadence is therefore uneven: two releases two days apart in February, then a gap. The repository uses release-please, visible in .release-please-manifest.json, which means version bumps and changelogs are generated from commit messages. Upgrade cost is mostly bounded by the grammar syntax rather than the JavaScript API, and the README does not document a migration path between major versions, so check the release notes before moving across a major boundary.

The project is a pnpm workspace with packages/ and pnpm-workspace.yaml at the top level, and package.json defines build, test, lint and format scripts that run across the workspace. That matters if you intend to contribute: the README points contributors at CONTRIBUTING.md, and the top-level scripts are the entry points rather than per-package commands.

Ohm is MIT licensed. That permits commercial use and modification, and it means the project carries no copyleft obligation on your grammar or your semantic actions. The MIT text is in LICENSE at the repository root. This is a description of the licence identifier, not legal advice; if your organization has specific requirements about attribution or about dependencies bundled into a distributed artifact, have someone check them.

## Conclusion

Adopt Ohm when you have a grammar you expect to revise, or when you want the parse tree to be inspected and rewritten by more than one consumer. Skip it when you need a parser emitted as standalone code with no runtime dependency, or when the input is a data format that a schema validator already covers. Before committing, open your grammar in the Ohm Editor, then run ohm.grammar() on the full file and check that match() succeeds on a real sample rather than a trimmed one.

## FAQ

### What is ohm-js used for?

It is a parsing toolkit for building parsers, interpreters and compilers, most often for custom file formats or small programming languages. The library is the JavaScript interface, and the Ohm language is how you write the grammar. The repository's examples cover math, csv, markdown, ecmascript, typescript and simple-lisp, among others.

### How do I install Ohm?

In Node.js the package is ohm-js, installed with npm install ohm-js, yarn add ohm-js or pnpm add ohm-js. In a browser you can load dist/ohm.js or dist/ohm.min.js from unpkg with a script tag, which creates a global named ohm. Deno users can import from https://unpkg.com/ohm-js@17.

### Do I need to install Node.js to use the Ohm CLI?

No. The README documents a Docker image, ohmjs/ohm:latest, that runs the CLI without a local Node.js installation, for example docker run --rm -v $(pwd):/local ohmjs/ohm:latest compile my-grammar.ohm. The repository keeps fuller instructions in doc/docker.md, including building the image locally.

### Does Ohm support left-recursive grammar rules?

Yes. The README lists full support for left-recursive rules as a feature, which is what lets you define left-associative operators in the natural way rather than rewriting the rule into an iterative form.

### Is there an online editor for Ohm?

Yes, the Ohm Editor at ohmjs.org/editor/ provides instant feedback and an interactive visualization of the parser's execution. The README recommends it as the easiest way to get started, alongside two JSFiddle examples for basic parsing and arithmetic with semantics.

## Sources

- [Issues](https://github.com/ohmjs/ohm/issues)
- [License: MIT](https://github.com/ohmjs/ohm/blob/main/LICENSE)
- [ohmjs/ohm on GitHub](https://github.com/ohmjs/ohm)
- [README](https://github.com/ohmjs/ohm/blob/main/README.md)
- [Releases](https://github.com/ohmjs/ohm/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ohmjs-ohm
