nearley

repository·master·Indexed 25 days ago

https://github.com/kach/nearley

A fast, modular, Earley-based parsing toolkit for JavaScript that works in Node.js and the browser. It handles any grammar defined in BNF, including complex, ambiguous, and left-recursive grammars. The toolkit includes the nearleyc compiler and CLI tools such as nearley-test for grammar testing, nearley-unparse for generating samples, and nearley-railroad for creating visual railroad diagrams.

Tokens
8.4K
Snippets
24
Records
58
Agent score
86%

What's inside nearley

  1. Overview of nearley parsing toolkit

    master

    nearley is a fast, modular parsing toolkit for JavaScript that works in both Node.js and the browser. It uses the Earley parsing algorithm, which allows it to handle any grammar defined in BNF, including left-recursive grammars that typically cause other parser generators (like PEGjs or Jison) to fail.

    Key features include:

    • Streaming parsing: Supports streaming input.
    • Error handling: Graceful error catching.
    • Ambiguity support: Provides all possible parsings for ambiguous grammars.
    • Lexer compatibility: Works with various lexers (the moo lexer is recommended).
    • Tooling: Includes tools for creating tests, railroad diagrams, and fuzzers.
  2. Understand Nearley parsing terminology

    master

    To use Nearley effectively, it is important to understand the core terminology used in its grammar definitions and parsing processes:

    Core Components

    • token: The smallest meaningful unit (the "words") of your language.
    • lexer: A function that converts a sequence of characters to a sequence of tokens.
    • string: A sequence of tokens.
    • language: A set of strings.
    • grammar: A set of production rules that together specify a language.

    Grammar Elements

    • symbol: A generic term for a member of a production rule, which can be either a terminal or a nonterminal.
    • terminal: A symbol that directly specifies a set of tokens that match.
    • nonterminal: A symbol that specifies a set of other production rules that match.
    • production rule: A set of strings specified as a sequence of symbols. A rule matches a string if it is a concatenation of strings matched by the respective symbols.
    • epsilon: The empty production rule, matching only the empty string.
    • nullable rule: A production rule that matches the empty string, even if it is not explicitly the epsilon rule (e.g., the concatenation of epsilon with epsilon).

    Parsing and Output

    • recognizer: A function that takes a grammar and a string and returns whether the grammar matches the string (yes/no).
    • parser: A function that takes a grammar and a string and returns a derivation of that string.
    • derivation: The recursive application of production rules to obtain a string.
    • parse tree: A tree representation of a derivation.
    • abstract syntax tree (AST): A version of a parse tree that has been simplified by postprocessors (e.g., omitting whitespace).
    • ambiguity: A situation where more than one derivation exists for a single string. Note that nearley returns all possible derivations in the case of ambiguity.

    Technical Concepts

    • Earley algorithm: The parsing algorithm used by nearley to efficiently parse all context-free grammars.
    • preprocessor: The dialect of JavaScript targeted by nearleyc (e.g., emitting TypeScript instead of plain JavaScript).
    • postprocessor: A function associated with a production rule used to transform a parse tree into an AST.
    • left-recursion: A situation where a nonterminal refers to a production rule whose first symbol matches the same nonterminal.
  3. Handle left recursion and associativity

    master

    Unlike recursive-descent parsers, nearley handles left recursion efficiently.

    • Efficiency: Prefer left recursion (a -> a "something") over right recursion (a -> "something" a) for better performance.
    • Associativity: Use left recursion for left-associative operators and right recursion for right-associative operators.
    • Avoid EBNF overlap: Do not use left recursion in places where the EBNF :* or :+ modifiers are more appropriate.
  4. Use nearley in the browser

    master

    Both the nearley parser and compiled grammars are compatible with browsers. For standard usage, you should precompile your grammars using the nearley compiler and then include both nearley.js and your generated grammar.js file in your HTML using <script> tags.

    Note: The nearley compiler is not designed for browser environments. It is recommended to serve only the precompiled JavaScript files to your users.

  5. Compile grammars dynamically in the browser

    master

    If you need to compile grammars dynamically in a browser (e.g., for an IDE), you can bundle the nearley NPM package using a module bundler like Webpack or Rollup. You can then use the nearley/lib/compile, nearley/lib/generate, and nearley/lib/nearley-language-bootstrapped modules to perform the compilation process manually.

    const nearley = require("nearley");
    const compile = require("nearley/lib/compile");
    const generate = require("nearley/lib/generate");
    const nearleyGrammar = require("nearley/lib/nearley-language-bootstrapped");
    
    function compileGrammar(sourceCode) {
        // Parse the grammar source into an AST
        const grammarParser = new nearley.Parser(nearleyGrammar);
        grammarParser.feed(sourceCode);
        const grammarAst = grammarParser.results[0]; // TODO check for errors
    
        // Compile the AST into a set of rules
        const grammarInfoObject = compile(grammarAst, {});
        // Generate JavaScript code from the rules
        const grammarJs = generate(grammarInfoObject, "grammar");
    
        // Pretend this is a CommonJS environment to catch exports from the grammar.
        const module = { exports: {} };
        eval(grammarJs);
    
        return module.exports;
    }
    
    const grammar = compileGrammar("main -> foo | bar");
    
    const parser = new nearley.Parser(nearley.Grammar.fromCompiled(grammar));
  6. Lexing with Moo

    master

    By default, Nearley uses scannerless parsing (splitting input into characters). To improve performance and grammar clarity, you can use a tokenizer (lexer) to split input into larger units called tokens. Nearley recommends using Moo for this purpose.

    To use a lexer:

    1. Define the lexer inside a Javascript block using moo.compile.
    2. Use the @lexer directive to pass the lexer object to Nearley.
    3. Match tokens in your grammar using either %type (to match by token type) or "text" (to match by the literal text of the token).

    When using a lexer, you call parser.feed(data) as usual.

    @{%
    const moo = require("moo");
    
    const lexer = moo.compile({
      ws:     /[ \t]+/,
      number: /[0-9]+/,
      word: { match: /[a-z]+/, type: moo.keywords({ times: "x" }) },
      times:  /\*/
    });
    %}
    
    @lexer lexer
    
    expr -> multiplication {% id %} | trig {% id %}
    
    # Use %token to match by type
    multiplication -> %number %ws %times %ws %number {% ([first, , , , second]) => first * second %}
    
    # Use literal strings to match by text
    trig -> "sin" %ws %number {% ([, , x]) => Math.sin(x) %}
  7. Structure nearley grammars top-down

    master
    Organize your .ne files from the top down. The first rules should define the general outline of the language (e.g., Sourcefile -> (S-expression | Comment):*). Detailed rules, such as terminals for whitespace or literals, should be placed at the bottom of the file. This ensures that high-level rules reference lower-level rules, following a logical hierarchy.
  8. Use EBNF modifiers instead of manual recursion

    master
    To match one or more occurrences of a nonterminal, always use nearley's EBNF modifiers (:*, :+, :?) rather than writing manual recursive rules. Manual recursion (e.g., lotsofletters -> "a" | lotsofletters lotsofletters) is prone to creating exponential ambiguity and is harder to maintain.
  9. Postprocess grammar output for efficiency

    master

    By default, nearley returns a nested array structure. To optimize memory and simplify your AST (Abstract Syntax Tree) processing:

    • For whitespace/junk: Use a postprocessor that returns null to discard unnecessary data.
    • For syntactic sugar: Use postprocessors to construct object literals. This allows you to discard syntax elements like parentheses and makes your downstream code independent of the specific grammar structure.
  10. Write a nearley grammar

    master

    Nearley grammars are written in .ne files and describe the structure of the input text. You can use nearleyc to compile these .ne files into JavaScript modules.

    Key concepts:

    • Terminals: Constant strings or tokens (e.g., "if").
    • Nonterminals: Sets of possible strings (e.g., ifStatement).
    • Rules: Definitions of nonterminals. A nonterminal can have multiple rules separated by the pipe | character.
    • Epsilon Rule: Use the keyword null to match nothing (zero occurrences).
    • Starting Rule: Nearley attempts to parse the first nonterminal defined in the grammar by default.
    expression ->
        number "+" number
      | number "-" number
      | number "*" number
      | number "/" number
    number -> [0-9]:+
    
    a -> null | a "cow"