PLY (Python Lex-Yacc)

repository·master·Indexed 25 days ago

https://github.com/dabeaz/ply

A zero-dependency implementation of the traditional lex and yacc tools in Python. PLY uses the LALR(1) parsing algorithm to enable the construction of compilers, protocol decoders, and AST builders. It provides high-level `lex()` and `yacc()` functions for tokenization and parsing, as well as low-level classes like `Grammar`, `LRTable`, and `LRParser` for manual grammar construction and internal inspection.

Tokens
12.2K
Snippets
26
Records
63
Agent score
84%

What's inside PLY

  1. Overview of PLY (Python Lex-Yacc)

    master
    PLY is a pure-Python implementation of the traditional lex and yacc compiler construction tools. It is designed to be faithful to traditional tools, supporting LALR(1) parsing, extensive input validation, error reporting, and diagnostics. Unlike traditional Unix tools, PLY does not require a separate code-generation step; instead, it uses Python reflection (introspection) to build lexers and parsers directly from valid Python programs.
  2. Understand the PLY module structure

    master

    PLY is distributed as a Python package named ply and consists of two primary modules that work together:

    1. lex.py: Used to break input text into a collection of tokens based on regular expression rules.
    2. yacc.py: Used to recognize language syntax specified via a context-free grammar. It retrieves tokens from the lexer and invokes grammar rules.

    While yacc.py is often used to produce an Abstract Syntax Tree (AST), the output format is entirely determined by the user.

  3. Define empty productions in Yacc

    master

    To handle empty productions (rules that can match nothing), define a rule with an empty right-hand side. It is a best practice to define a specific empty rule and use the symbol empty in other rules to improve readability.

    def p_empty(p):
        'empty :'
        pass
    
    def p_optitem(p):
        'optitem : item
                  | empty'
  4. High-level workflow for creating a parser

    master

    If you are building an alternative PLY interface, you should follow this specific sequence of operations used by the yacc() function:

    1. Create a ParserReflect object and collect raw grammar specification data.
    2. Create a Grammar object and populate it with the specification data.
    3. Create an LRTable object to run the LALR algorithm over the Grammar object.
    4. Bind the productions in the LRTable to callables using the bind_callables() method.
    5. Create an LRParser object using the information from the LRTable.
  5. Create a Parser with `yacc()`

    master

    To parse tokens into an Abstract Syntax Tree (AST) or other structures, define grammar rules as Python functions. Each function must have a docstring containing the grammar rule.

    Key components:

    • p_ <rule_name>(p): A function representing a grammar rule. The argument p is a sequence representing the rule contents.
    • p[0]: The result of the rule (the value assigned to the non-terminal).
    • p[1], p[2], ...: The components of the rule (the symbols matched).
    • p_error(p): A function to handle syntax errors.
    • yacc(): Builds the parser object.
    • parser.parse(input): Executes the parsing process on the provided input string.
    from ply.yacc import yacc
    
    def p_expression(p):
        '''
        expression : term PLUS term
                   | term MINUS term
        '''
        p[0] = ('binop', p[2], p[1], p[3])
    
    def p_error(p):
        print(f'Syntax error at {p.value!r}')
    
    parser = yacc()
    ast = parser.parse('2 * 3 + 4')
  6. Configure lexer with custom modules or classes

    master

    By default, lex.lex() inspects the current module. You can specify a different source for your rules using the module argument.

    • External Module: Pass a module object (e.g., import my_rules; lex.lex(module=my_rules)).
    • Class Instance: Pass an instance of a class. Note that when using a class, you must construct the lexer from an instance, not the class itself, so that token rules are bound to that instance.
    import ply.lex as lex
    
    class MyLexer:
        tokens = ('NUMBER', 'PLUS')
        t_PLUS = r'\+'
    
        def t_NUMBER(self, t):
            r'\d+'
            t.value = int(t.value)
            return t
    
        def build(self, **kwargs):
            self.lexer = lex.lex(module=self, **kwargs)
    
        def test(self, data):
            self.lexer.input(data)
            while True:
                tok = self.lexer.token()
                if not tok: break
                print(tok)
    
    m = MyLexer()
    m.build()
    m.test("123 + 456")
  7. Implement panic mode error recovery in p_error()

    master

    Panic mode recovery involves manually discarding tokens in the p_error() function until a known safe state (like a closing brace) is reached, then restarting the parser.

    To implement this, you must have access to the parser instance (the object returned by yacc()) within your p_error() function.

    def p_error(p):
            print("Whoa. You are seriously hosed.")
            if not p:
                print("End of File!")
                return
    
            # Read ahead looking for a closing '}'
            while True:
                tok = parser.token()             # Get the next token
                if not tok or tok.type == 'RBRACE': 
                    break
            parser.restart()
  8. Combine multiple grammar rules into one function

    master

    To reduce function overhead, you can combine multiple grammar rules into a single Python function by using a multi-line docstring. This is useful when rules share similar structures.

    Note on Performance: While combining rules can make the code more compact, it adds conditional logic (like if/elif or len(p) checks) that duplicates work the parser has already done. For high-performance parsing, use separate functions for each rule.

    def p_expression(p):
        '''expression : expression PLUS term
                      | expression MINUS term'''
        if p[2] == '+':
            p[0] = p[1] + p[3]
        elif p[2] == '-':
            p[0] = p[1] - p[3]
  9. Create a Lexer with `lex()`

    master

    To tokenize input, define a set of tokens, regex-based matching rules (either as string variables or functions with docstrings), and an error handler. Use lex() to build the lexer object.

    Key components:

    • tokens: A tuple of all token names.
    • t_ignore: A string containing characters to be ignored (e.g., ' ').
    • t_<NAME>: A string variable containing the regex for a token.
    • def t_<NAME>(t): A function used for tokens requiring actions; the regex must be in the function's docstring.
    • t_error(t): A function to handle illegal characters.
    • t_ignore_newline(t): A function to handle specific patterns like newlines (e.g., updating t.lexer.lineno).
    from ply.lex import lex
    
    tokens = ( 'PLUS', 'MINUS', 'TIMES', 'DIVIDE', 'LPAREN', 'RPAREN', 'NAME', 'NUMBER' )
    t_ignore = ' \t'
    t_PLUS = r'\+'
    
    def t_NUMBER(t):
        r'\d+'
        t.value = int(t.value)
        return t
    
    def t_error(t):
        print(f'Illegal character {t.value[0]!r}')
        t.lexer.skip(1)
    
    lexer = lex()
  10. Debug the lexer

    master

    To inspect how PLY is building its internal regular expressions and rules, initialize the lexer with debug=True:

    lexer = lex.lex(debug=True)

    This will output debugging information including the master regular expressions used for matching.

  11. Understand Parsing Basics with yacc.py

    master

    PLY's yacc.py module is used to parse language syntax using a BNF (Backus-Naur Form) grammar.

    Key Concepts

    • Terminals: Symbols that correspond to input tokens (e.g., NUMBER, +, -, *, /).
    • Non-terminals: Identifiers that refer to grammar rules composed of terminals and other rules (e.g., expression, term, factor).
    • Syntax Directed Translation: A technique where attributes (like a .val attribute) are attached to grammar symbols. When a rule is recognized, a semantic action is triggered to perform computations based on these attributes.

    Parsing Mechanism

    yacc.py implements LR-parsing (also known as shift-reduce parsing), a bottom-up technique:

    1. Shift: Moving grammar symbols from the input onto a stack.
    2. Reduce: When the top of the stack matches the right-hand side of a grammar rule, the symbols are replaced by the rule's left-hand side (the non-terminal), and any associated semantic actions are executed.

    A parse is successful only if the parser reaches a state where the symbol stack is empty and all input tokens have been processed.

  12. Resolve grammar ambiguity with token precedence and associativity

    master

    To resolve shift/reduce conflicts in expression grammars, define a precedence variable in your grammar file. This allows you to assign precedence levels and associativity (left, right, or nonassoc) to tokens.

    Tokens are ordered from lowest to highest precedence within the declaration. The precedence of a grammar rule is determined by the precedence of its right-most terminal symbol.

    Associativity rules:

    • left: The rule is reduced if the current token and the rule have the same precedence.
    • right: The token is shifted if the current token and the rule have the same precedence.
    • nonassoc: Prevents chaining of operators (e.g., a < b < c will trigger a syntax error).
    precedence = (
        ('nonassoc', 'LESSTHAN', 'GREATERTHAN'),
        ('left', 'PLUS', 'MINUS'),
        ('left', 'TIMES', 'DIVIDE'),
        ('right', 'UMINUS'),
    )