NPeg: Nim Parsing Expression Grammars

repository·master·Indexed 43 days ago

https://github.com/zevv/npeg

A pure Nim pattern matching library that uses Parsing Expression Grammars (PEGs). NPeg compiles patterns and grammars into Nim procedures via macros, providing tools for efficient string and data sequence parsing. It features two primary macros, `patt()` for simple patterns and `peg()` for recursive grammars, along with support for string captures, code block captures, and experimental precedence operators for building Pratt parsers.

Tokens
8.6K
Snippets
30
Records
37
Agent score
6%

What's inside NPeg

  1. Capture data using Code Block Captures

    master

    Code block captures allow you to execute embedded Nim code when a rule matches. This is done by ending a rule with a colon : followed by an indented block of Nim code.

    Accessing Captured Data

    Inside the code block, you can access captured strings via the capture[] sequence of type seq[Capture].

    • capture[0] is the total subject matched by the rule.
    • capture[1], capture[2], etc., correspond to explicit > string captures made by the rule or its children.

    Syntactic Sugar

    To simplify access, NPeg provides shorthand variables:

    • $0 to $9: Rewritten to capture[n].s (the captured string).
    • @0 to @9: Rewritten to capture[n].si (the integer index/offset in the subject).

    Note: Code blocks are executed during parsing, even if the match is later invalidated by backtracking. Code block captures also consume all embedded string captures, making them unavailable in the final MatchData.captures list.

    Pushing new captures

    You can manually add new captures to the result by calling push(s: string) within the code block (experimental feature).

    let p = peg foo:
      foo <- >(1 * >1) * 1:
        echo "$0 = ", $0
        echo "$1 = ", $1
        echo "$2 = ", $2
           
    echo p.match("abc").ok
    # Output:
    # $0 = abc
    # $1 = ab
    # $2 = b
  2. Understand PEG anchoring and searching behavior

    master

    Unlike regular expressions, PEGs in NPeg are always matched in anchored mode. This means the pattern must match from the start of the subject string. For example, the pattern "bar" will not match the string "foobar".

    To perform a search (finding a pattern anywhere in a string), you can use a recursive construct or the built-in @P operator.

    Manual search pattern:

    p <- "bar"
    search <- p | 1 * search
    p <- "bar"
    search <- p | 1 * search
  3. Match the end of a string in NPeg

    master

    By default, PEGs do not care if there is remaining text in the subject string after a successful match. To ensure a pattern matches exactly to the end of the string, use the idiomatic notation !1 (meaning "succeed only if there is not a single character left to match").

    rule <- pattern !1
  4. NPeg Syntax Overview

    master

    NPeg uses a syntax similar to standard PEG notation but with specific modifications to ensure compatibility with the Nim compiler. Key differences include:

    • Prefix Quantifiers: Operators like *, +, -, and ? are used as prefixes (e.g., *P) rather than suffixes.
    • Ordered Choice: Uses the | operator instead of / to avoid operator precedence conflicts.
    • Explicit Sequence: Uses the * infix operator for concatenation (e.g., P1 * P2).

    NPeg patterns are composed of Atoms (the basic building blocks), Operators (which combine atoms), and Precedence Operators (for building Pratt parsers).

  5. Handle non-consuming atoms and captures

    master

    The lookahead (&) and not (!) operators are non-consuming; they do not advance the parser state. Any captures made inside these blocks are discarded when the parser state is rolled back.

    Note: If you need to capture content within a non-consuming block, use a code block capture, as these are always executed even if the parser state is rolled back.

  6. Define recursive grammars with peg()

    master

    The peg macro allows you to define named rules that can refer to one another, enabling recursion.

    Rule Ordering and Performance: The order in which you define rules in a peg block affects how the parser is generated:

    • Inlining: If rule P1 refers to P2 and P2 is defined before P1, P2 is inlined into P1. This increases code size but improves performance.
    • Subroutines: If rule P1 refers to P2 and P2 is defined after P1, P2 is generated as a subroutine. This reduces code size but may result in a slower parser.
    let parser = peg "ident":
      lower <- {'a'..'z'}
      ident <- *lower
    doAssert parser.match("lowercaseword").ok
  7. Parse non-string types (Experimental)

    master

    NPeg can parse subjects of type openArray[T] (e.g., a token stream) instead of just strings.

    Requirements:

    • The peg() macro must be passed an additional argument specifying the base type T of the subject.
    • If no type is provided, it defaults to char (parsing string).

    Grammar Adjustments for Non-Strings:

    • Literals: Standard string/character atoms may not work. Use square brackets [literal] to specify any Nim code as a literal (e.g., [foo], [1+1], or ["foo"]).
    • Captures: Captures are limited to a single element of the base type T.

    Note: This is an experimental feature and the API may change.

  8. NPeg Atoms

    master

    Atoms are the fundamental building blocks of an NPeg grammar. They match specific characters, strings, or quantities of data.

    Integer Literals

    • 0: Matches always and consumes nothing.
    • 1: Matches any single character.
    • n: Matches exactly n characters/bytes.

    Character and String Literals

    • 'x': Matches the literal character 'x'.
    • "xyz": Matches the literal string "xyz".
    • i"xyz": Matches the literal string "xyz" case-insensitively.

    Character Sets

    Uses Nim-style set notation to match any character within a defined set or range:

    • {'x'..'y'}: Matches any character in the range from 'x' to 'y'.
    • {'x','y','z'}: Matches any character from the specific set 'x', 'y', or 'z'.
    • {'0'..'9','a'..'f'}: Combines multiple ranges and characters in one expression.
    0              # matches always and consumes nothing
    1              # matches any character
    n              # matches exactly n characters
    'x'             # matches literal character 'x'
    "xyz"           # matches literal string "xyz"
    i"xyz"         # matches literal string, case insensitive
    {'x'..'y'}      # matches any character in the range from 'x'..'y'
    {'x','y','z'}   # matches any character from the set
  9. Use templates for parameterized rules

    master

    To follow the DRY (Don't Repeat Yourself) principle, you can create templates. Templates are parameterized rules that perform a basic search-and-replace of arguments within the rule definition.

    Defining a simple template:

    commaList(item) <- item * *( ',' * item )
    numberList <- commaList(+Digit)
    wordList <- commaList(+Alpha)

    Defining nested/recursive templates:

    list(item, sep) <- item * *( sep * item )
    commaList(item) <- list(item, ',')
    numberList <- commaList(+Digit)
    wordList <- commaList(+Alpha)
  10. Use Precedence Operators for Pratt Parsers

    master

    NPeg supports experimental precedence operators to build Pratt parsers (precedence climbing) for programming languages. This allows you to define associativity and precedence levels directly in the grammar.

    • Left associative precedence of N: P ^ N
    • Right associative precedence of N: P ^^ N

    When P ^ N is matched, if N is higher than or equal to the current precedence, parsing fails and backtracks. If N is lower, the current precedence is updated to N. P ^ 0 resets the precedence to 0, which is useful for sub-expressions in parentheses.

    Example Pratt Parser Structure

    exp <- prefix * *infix
    
    parenExp <- ( "(" * exp * ")" ) ^ 0
    
    prefix <- number | parenExp
    
    infix <- {'+','-'} * exp ^ 1 |
             {'*','/'} * exp ^ 2 |
             {'^'}     * exp ^^ 3
  11. Use Backreferences with the R operator

    master

    Backreferences allow you to match a string that was previously matched by another part of the grammar.

    NPeg provides the R operator for two purposes:

    1. R(name, P): Creates a named reference for pattern P. This stores the result of P under name.
    2. R(name): Matches the exact content that was previously stored in the named reference name.

    Example: Matching a string where the first and last characters are identical:

    # Pattern: R("c", 1) * *(1 - R("c")) * R("c") * !1
    # 1. R("c", 1) matches any single character and stores it as "c"
    # 2. *(1 - R("c")) matches zero or more characters that are NOT "c"
    # 3. R("c") matches the character stored in "c"
  12. Avoid left recursion in grammars

    master

    NPeg does not support left recursion, which will cause infinite loops.

    Examples of problematic patterns:

    • Direct left recursion: A <- A | 'a'
    • Mutual left recursion: A <- B | 'a' A; B <- A
    • Loops matching empty strings: *0 will cause the parser to stall and loop infinitely.
    A <- A | 'a'
    
    A <- B | 'a' A
    B <- A
    
    *0