Skip to main content

parsergen

A simple library for creating parsers and lexers.

Quickstart

pip install parsergen

Defining a Lexer

Tokens have different regular expressions. They can also have modifier functions, for example the INT tokens get their values turned into an int.

from parsergen import *
class CalcLexer(Lexer):
    
    @token(r"0x[0-9a-fA-F]+", r"[0-9]+")
    def INT(self, t):
        if t.value.startswith("0x"):
            t.value = int(t.value[2:], base=16)
        else:
            t.value = int(t.value)
        return t

    ADD    =  r"\+"
    SUB    =  r"\-"
    POW    =  r"\*\*" # must be first, as is longer than 'MUL' token!
    MUL    =  r"\*"
    DIV    =  r"\/"
    SET    =  r"set"
    TO     =  r"to"
    ID     =  r"[A-Za-z_]+"
    LPAREN =  r"\("
    RPAREN =  r"\)"
    
    ignore = " \t"
    ignore_comment = r"\#.*"

Creating a Parser

Grammar Expressions

Grammar Expressions describe the syntax that can be parsed. For our basic example calculator, you will get a terminal to type math expressions

> 2 + 3 * 4
14
> (2 + 3) * 4
20
> 2 ** 2 ** 3
256

It is important that the precedence of the arithmetic operators is correct, we have to account for this when designing our grammar rules. Here is the grammar:

statement       :  assign | expr
assign          :  SET ID TO expr
expr            :  prec3
prec3           :  prec2 (ADD | SUB prec2)*
prec2           :  prec1 (MUL | DIV prec1)*
prec1           :  factor (POW prec1)?
factor          :  INT | ID
factor          :  LPAREN expr RPAREN

the rules prec3 and prec2 are left associative, whereas prec1 is right associative because it implements the pow operator We can then define our parser.

class CalcParser(Parser):

    tokens = CalcLexer.tokens
    starting_point = "statement"

    def __init__(self):
        self.names = {}

    @grammar("assign | expr")
    def statement(self, p):
        print(p[0])
    
    @grammar("SET ID TO expr")
    def assign(self, p):
        self.names[p[1]] = p[3]
    
    @grammar("prec3")
    def expr(self, p):
        return p[0]
    
    @grammar("prec2 (ADD | SUB prec2)*") # left associative
    def prec3(self, p):
        r = p[0]
        for op, num in p[1]:
            if op == "+":
                r += num
            else:
                r -= num
        return r
    
    @grammar("prec1 (MUL | DIV prec1)*") # left associative
    def prec2(self, p):
        r = p[0]
        for op, num in p[1]:
            if op == "*":
                r *= num
            else:
                r /= num
        return r
    
    @grammar("factor (POW prec1)?") # right associative
    def prec1(self, p):
        if p[1]:
            return p[0] ** p[1][1]
        return p[0]
    
    @grammar("INT")
    def factor(self, p):
        return p[0]
    
    @grammar("ID")
    def factor(self, p):
        try:
            return self.names[p[0]]
        except KeyError:
            raise Exception(f"variable '{p[0]}' is not defined.")

    @grammar("LPAREN expr RPAREN")
    def factor(self, p):
        return p[1]

# We can then create a simple runtime loop
l = CalcLexer()
p = CalcParser()

while True:
    s = input("> ")
    l_result = l.lex_string(s)
    try:
        p.parse(l.lex_string(s))
    except Exception as e:
        print(e)

Handling Newlines

The Lexer, by default knows nothing about line numbers. You have to tell it what to do.

class MyLexer(Lexer):
    @token(r"\n+")
    def NEWLINE(self, t):
        self.lineno += len(t.value)
        self.column = 0
        return t
    ...

Overcoming issues with left recursion

Recursive descent parsers are unable to handle direct or indirect left recursion. This is an issue when writing expressions for left associative operators. The following example is directly left recursive:

expr  :  expr PLUS term

and when attempting to process this rule it will fall into an infinite loop. There are different ways to solve this problem, my solution is below:

expr  :  term (PLUS term)*

The disadvantage to this is that there is then some processing required after the pattern matching to reach the original desired strucutre or action.

@grammar("term (PLUS term)*")
def expr(self, p):
    rv = p[0]
    for op, term in p[1]:
        rv += term
    
    return rv

See here for more details.

Writing expressions for right-associative operators

Some operators are right associative, for example the ** operator. Right recursion can be implemented more normally in the grammar expression:

expr  :  term (POW expr)?

This behaves as expected, after pattern matching you do have to perform a check in your code as seen next:

@grammar("term (POW expr)?")
def expr(self, p):
    if p[1]:
        return p[0] ** p[1][1]
    return p[0]

Printing the grammar for your parser

It is sometimes helpful to see the entire grammar for you parser. This can be done as shown below:

from parsergen import get_grammar
print(get_grammar(CalcParser))

See example_calc.py and example.py for more examples, or look at the source code.

Metadata

Release files for parsergen 1.0.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for parsergen 1.0.3
File Size Uploaded
parsergen-1.0.3.tar.gz 10.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for parsergen 1.0.3
File Interpreter ABI Platform
parsergen-1.0.3-py3-none-any.whl Python 3 none any Details

Total release size: 19.5 kB

Release files / parsergen-1.0.3.tar.gz

Download URL parsergen-1.0.3.tar.gz
Size 10.3 kB
Tags Source
SHA-256 checksum
How to use checksums
0aa461cdb961395289f8fcc031d9de76122bfacef8216b9e29a1b8c91e716933
BLAKE2b-256 checksum
How to use checksums
1a49091e444f5817d522fec7520dde0d33bb4caec7037c1647ac850793e75e49
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.4.1 importlib_metadata/4.3.0 pkginfo/1.7.0 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.61.0 CPython/3.8.3

Release files / parsergen-1.0.3-py3-none-any.whl

Download URL parsergen-1.0.3-py3-none-any.whl
Size 9.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
59dca524c6bcb07973ccd93eceef5525abf821e898699b828c1262115cd9e90c
BLAKE2b-256 checksum
How to use checksums
c2aee5d62cf02bccd3bb01d424bc0c8e16dd95e3d42c6f87292cfc1de9b7de44
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.4.1 importlib_metadata/4.3.0 pkginfo/1.7.0 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.61.0 CPython/3.8.3
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page