Ohm Parsing Toolkit

repository·main·Indexed 26 days ago

https://github.com/ohmjs/ohm

A parsing toolkit consisting of a library and a domain-specific language based on Parsing Expression Grammars (PEGs). Ohm allows developers to build parsers, interpreters, and compilers with support for left-recursion and modular semantic actions. The ecosystem includes ohm-js for the runtime, @ohm-js/compiler for compiling grammars to WebAssembly (Wasm), and @ohm-js/es-grammars for pre-defined ECMAScript grammars.

Tokens
21.4K
Snippets
56
Records
179
Agent score
89%

What's inside Ohm

  1. Understand Ohm's separation of grammars and semantic actions

    main

    Ohm distinguishes itself by completely separating grammars from semantic actions.

    • Grammars: Define the language structure (the syntax).
    • Semantic Actions: Specify what to do with valid inputs (the meaning).

    This separation allows you to use the same pure grammar for multiple purposes—such as parsing, syntax highlighting, and compiling—by applying different semantic actions independently. This improves readability, modularity, and portability across different language implementations (e.g., Ohm/JS vs Ohm/Scheme).

  2. Leverage lazy semantic action evaluation

    main

    Ohm applies semantic actions lazily, meaning an action is only evaluated when its result is explicitly required. This provides several advantages for developers:

    • Backtracking Safety: You do not need to worry about backtracking when writing semantic actions, which is particularly important for side-effectful code.
    • Controlled Evaluation: Similar to the visitor pattern, semantic actions control the evaluation of sub-expressions, allowing you to specify logic to run before or after sub-expressions are evaluated.
    • Efficiency:
      • Semantic actions are never evaluated if the input is invalid.
      • Semantic actions for failed branches in an alternation (|) are not evaluated.
      • If a semantic action does not reference a specific sub-expression, that sub-expression's actions are never executed.
  3. Run the Ohm compiler via Docker

    main

    The ohmjs/ohm:latest image provides the Ohm compiler CLI in a self-contained environment. You can compile .ohm grammar files to .wasm modules without installing Node.js or pnpm locally.

    When running the container, your current directory is mounted at /local inside the container, allowing you to use relative paths to your grammar files.

    docker run --rm -v $(pwd):/local ohmjs/ohm:latest compile my-grammar.ohm
  4. Access Ohm Extras

    main

    Ohm provides additional helper methods and semantics through the ohm-js/extras module. These are not part of the core library but are useful for common grammar-related tasks like AST conversion.

    To access them, require the extras module:

    const extras = require('ohm-js/extras');
    // Use methods like extras.toAST(...)
    const extras = require('ohm-js/extras');
  5. Set up the Ohm development environment

    main

    To develop Ohm, ensure you have a recent version of Node.js (Active LTS or Maintenance LTS) and pnpm installed.

    Follow these steps to clone and prepare the repository:

    1. Clone the repository:
      git clone https://github.com/cdglabs/ohm.git
    2. Install dependencies:
      cd ohm
      pnpm install

    Note: Running pnpm install automatically installs a git pre-commit hook that runs tests and ESLint checks.

    git clone https://github.com/cdglabs/ohm.git
    cd ohm
    pnpm install
  6. Work with iteration nodes in semantic actions

    main

    Repetition operators (*, +, ?) produce iteration nodes. You can process them in two ways:

    1. Array operations: Use .children to perform map, filter, etc. iterNode.children.map(c => c.prettyPrint())

    2. _iter actions: Define an _iter action on your operation to allow calling it directly on the node: iterNode.prettyPrint()

    Handling Optional Nodes

    Optional nodes (?) are iteration nodes with at most one child. Use optional chaining for clean code:

    optNode.child(0)?.myOperation();
    optNode.child(0)?.myOperation();
  7. Publish the ohm-js package

    main

    To version and publish a new release of ohm-js, follow these steps:

    1. Manually update the version number in package.json.
    2. Ensure CHANGELOG.md is updated with the relevant changes.
    3. Execute pnpm publish.
    4. Push changes and tags to GitHub: git push && git push --tags.
    5. Create a new release on GitHub, using the contents of the changelog.
    pnpm publish
    git push && git push --tags
  8. Enable incremental parsing using Matcher objects

    main

    To perform incremental parsing (re-parsing an input quickly after an edit operation), do not use the Grammar's match method directly. Instead, instantiate a Matcher object. This allows you to use methods like replaceInputRange to apply edits and re-parse efficiently.

    ```js
    const m = grammar.matcher();
    
    m.setInput('(1 + 2) + (3 - 4)');
    // ... perform edits
    m.replaceInputRange(1, 2, '0'); // Replace 1 with 0
    const newMatch = m.match();
    ```埋
  9. Generate grammar-specific TypeScript type definitions

    main

    Starting with Ohm v16, you can generate TypeScript type definitions specific to your grammar. This enables the TypeScript compiler to validate semantic actions (checking argument counts and return types) and provides IDE autocompletion for action names and node types (IterationNode, NonterminalNode, or TerminalNode).

    To enable this:

    1. Install @ohm-js/cli as a development dependency.
    2. Ensure your grammar is in a standalone .ohm file.
    3. Use the ohm generateBundles --withTypes command to create a grammar bundle (.ohm-bundle.js) and its corresponding type definitions (.d.ts).
    4. Import the generated bundle directly in your TypeScript code.
    npx ohm generateBundles --withTypes 'src/*.ohm'
    import grammar from './my-grammar.ohm-bundle'
  10. Handle greedy matching with negative lookahead

    main

    Ohm's repetition operators (* and +) are greedy and consume as much input as possible. To prevent a repetition from consuming too much, use the negative lookahead operator (~).

    For example, to match all 'a' characters except the last one in a string, use:

    allButLastA = (~("a" end) "a")*

    This ensures the rule only matches an 'a' if it is not followed by the end of the input.

  11. Export primary and variant languages in a single package

    main

    To package a single primary language alongside other variants, expose the primary language from the top-level module and export the variants as separate modules within the same package structure.

    const smalltalk = require('./your-smalltalk-package');
    const smalltalk72 = require('./your-smalltalk-package/smalltalk72');