tsPEG TypeScript Parser Generator

repository·master·Indexed 42 days ago

https://github.com/eoindavey/tspeg

A PEG (Parsing Expression Grammar) parser generator for TypeScript that produces type-safe ASTs. It supports advanced features such as computed properties, position tracking via the @ expression, memoization for O(n) performance, and regex modifiers. Version 3.3.4.

Tokens
7.7K
Snippets
29
Records
43
Agent score
53%

What's inside tsPEG

  1. Structure of the Calculator Demo

    master

    The calculator implementation consists of three main parts:

    • grammar.peg: The tsPEG grammar specification defining the mathematical syntax.
    • eval.ts: Logic for traversing and evaluating the generated parse tree.
    • cli.ts: The CLI wrapper that handles user input and output.
  2. Define grammar rules and capture AST fields

    master

    Grammars use a syntax similar to EBNF. Rules are defined using :=.

    Rule Definition: ruleName := expression

    Capturing Data: By default, rules only return their kind. To capture specific parts of a match into the AST, use the = assignment operator within the rule description. Rules that are simple references to other rules are captured implicitly.

    Example:

    sum := left=num '\+' right=num
    num := '[0-9]+'

    This generates a sum interface/class:

    interface sum {
        left: string;
        right: string;
    }
  3. Differentiate AST nodes using Kind Checking

    master

    tsPEG uses discriminated unions to distinguish between different AST node types. Every AST node has a kind field containing a value from the ASTKinds enum. You can use this field to safely narrow types when processing the parse tree.

    Kind Naming Conventions:

    • Simple rules (Rule := name='regex'): ASTKinds.Rule.
    • Choice rules (Rule := choiceA='regexA' | choiceB='regexB'): ASTKinds.Rule_1, ASTKinds.Rule_2, etc., depending on which branch matched.
    • Sub-rules (Rule := sub={ name='regex' }): ASTKinds.Rule_$N where N is the index of the sub-rule.
    • Direct references: Rules that simply reference another rule (e.g., rule := otherrule) inherit the kind of the referenced rule.

    Example usage:

    import { Choice, Parser, ASTKinds } from "./parser";
    
    function makeChoice(c: Choice) { 
        if(c.kind === ASTKinds.Word) {
            console.log('Matched word ', c.word)
        } else if(c.kind === ASTKinds.Int) {
            console.log('Matched int ', c.val)
        }
    }
    import { Choice, Parser, ASTKinds } from  "./parser";
    
    function makeChoice(c: Choice) {
        if(c.kind === ASTKinds.Word) {
            console.log('Matched word ', c.word)
        } else {
            console.log('Matched int ', c.val)
        }
    }
  4. Create computed properties in your grammar

    master

    You can add fields to your AST that are calculated during the parsing process by appending a computed property expression to a rule. This allows you to perform logic (like type conversion or arithmetic) directly within the grammar instead of post-processing the AST.

    Syntax: .<propertyname> = <type> { <code to calculate property> }

    Example of computing a sum and parsing integers:

    sum := left=num '\+' right=num
           .sum = number { return this.left.value + this.right.value }
    num := literal='[0-9]+'
           .value = number { return parseInt(this.literal); }
  5. Use the lookahead operator (&) for context-sensitive languages

    master

    The & operator allows you to match a pattern without consuming the input. This is useful for parsing context-sensitive languages (languages that are not context-free) where you need to verify a pattern exists before proceeding with the actual consumption of tokens.

    In this specific demo, the lookahead operator is used to ensure that the number of 'a's, 'b's, and 'c's are equal (e.g., matching aabbcc or aaabbbccc but rejecting aaaaabbc or aaabbb) by checking the sequence without advancing the parser position prematurely.

    /* The demo logic uses the & operator to match but not consume input */
  6. Enforce end-of-input matching with `$`

    master

    By default, a parser might succeed even if there is trailing unconsumed input. To ensure the parser only succeeds if the entire input is consumed, append the $ symbol (representing EOF) to your rule.

    If the $ match fails, the parser will return a SyntaxErr containing an EOFMatch object in the expected matches.

    hello := 'Hello World' $
  7. Use tsPEG operators for complex grammars

    master

    tsPEG provides several operators to control matching logic:

    • | (Choice): Matches the left side, or if it fails, tries the right side. Example: rule := 'A' | 'B'.
    • ? (Optional): Makes the preceding match optional. Example: 'really '?.
    • + (One or more): Matches 1 or more copies. Results are attached as a list to the AST.
    • * (Zero or more): Matches 0 or more copies. Results are attached as a list to the AST.
    • [n] (Exact count): Matches exactly n times. Example: 'Hello'[2] matches HelloHello.
    • [n, ] (Lower bound): Matches at least n times.
    • [n, m] (Range): Matches between n and m times.
    • ! (Negative lookahead): Inverts the match; succeeds only if the expression does NOT match.
    • & (Positive lookahead): Tests for a match without consuming any input.
    • {} (Sub-rules): Groups expressions for inline logic. Example: rule := 'start' {optional}? 'finish'. To capture sub-rule values, assign a name: rule := 'start' middle={optional}? 'finish'.

    Note: tsPEG starts parsing with the first rule defined in the file.

  8. Run the Calculator with Computed Properties demo

    master

    To run this specific demo, you must first generate the parser from the grammar file, compile the TypeScript project, and then execute the CLI. This demo demonstrates how to use computed properties within a grammar to handle expression evaluation logic directly in grammar.gram instead of using external evaluation functions.

    # 1. Generate the parser
    tspeg grammar.gram parser.ts
    
    # 2. Compile the project
    tsc -p .
    
    # 3. Execute the CLI
    node jsbuild/cli.js
  9. Use a header to import types and functions

    master

    To use external TypeScript types or functions within your computed properties, you can define a header at the top of your grammar file. Anything placed between two lines of three dashes --- is inserted directly into the generated parser file.

    Example:

    ---
    import { myFunc, myType } from "./mypackage";
    ---
    rule := hello='Hello World'
            .value = myType { return myFunc(this.hello); }
  10. Run the Calculator Demo

    master

    The calculator demo is a CLI application that evaluates mathematical expressions (e.g., (10+2) / 4). To run the demo, you must first generate the parser from the grammar, compile the TypeScript project, and then execute the resulting JavaScript CLI.

    # 1. Generate the parser from the grammar
    tspeg grammar.peg parser.ts
    
    # 2. Compile the project
    tsc -p .
    
    # 3. Execute the CLI
    node jsbuild/cli.js
  11. Run the JSON parser demo

    master

    The JSON parser demo is an implementation of the RFC 4627 specification. It demonstrates advanced tspeg features such as computed properties, header modifiers, and regex modifiers.

    To generate the parser from the grammar and execute the included tests, follow these steps:

    # 1. Generate the parser from the grammar file
    tspeg grammar.peg parser.ts
    
    # 2. Compile the TypeScript project
    tsc -p .
    
    # 3. Run the test script
    node jsbuild/test.js