Pyllk
A python LL(k) parser with a twist where tokens can be any arbitrary objects. The current implementation uses the backtracking algorithm. I'll eventually implement predictive parsing but the API is designed to remain backward compatible.
Important: It is not a tokenizer. It comes with a very basic string tokenizer that assumes a single character is a token. The whole point is to be able to use a third party tokenizer so that you can match them to terminal tokens. Use TokenStream to push the tokenized list into the parser.
Installation
pip install pyllk
Example. A simple calculator
This is an example of the typical calculator. In this case, we use characters as tokens. The idea is to parse things like ((10+2)*15)/(7-2) and be able to pull the result.
Here are the steps involved in the example:
- Create the function that converts terminal token definitions into instances of instances of pyllk.TerminalToken
- Create the fnuction that converts action definitions into functions to execute
- Create the EBNF grammar for the calculator
- Execute
Convert terminal token definitions
In this simple text-based example, terminal tokens are simply a single character. The matches function
simply needs to execute the regular expression to determine if a give character matches this terminal. The token
definition is set as JSON in the EBNF grammar. The attributes are completely arbitrary and you just need to use
them here. In this example, we chose to use a single attribute called regex that describes the regular expression.
import os
import re
import pyllk
from pyllk.parser import Context, Parser
from pyllk.token import TerminalToken
class CharacterTerminalToken(TerminalToken):
def __init__(self, config):
self.regex = f"^{config['regex']}$"
super(CharacterTerminalToken, self).__init__(self.regex)
def matches(self, obj):
return True if re.search(self.regex, f"{obj}") else False
def character_tokenizer(definition: dict) -> CharacterTerminalToken:
return CharacterTerminalToken(definition)
Convert action definition (i.e. dict) into appropriate functions
In this case, we use 5 functions. One that pushes numbers into the stack and four for the various operators we want to support.
The action definition will be specified as JSON in the EBNF grammar. It's completely arbitrary, in this case, we chose to use
the fn attribute to indicate which function we're trying to execute. It then returns the corresponding function as an action to
execute when the production rule matches. In order to do this, it uses the context object (an instance of Context, aka a dict) to
carry the state of the FSM.
def make_action(definition: dict) -> None:
def push_number(ec):
s = ""
for token in ec.tokens:
s = s + token.representation
ec.context['stack'].append(float(s))
def add(e):
b = e.context['stack'].pop()
a = e.context['stack'].pop()
e.parser.log("Ex: {} + {}".format(a, b))
e.context['stack'].append(a + b)
def sub(e):
b = e.context['stack'].pop()
a = e.context['stack'].pop()
e.parser.log("Ex: {} - {}".format(a, b))
e.context['stack'].append(a - b)
def mul(e):
b = e.context['stack'].pop()
a = e.context['stack'].pop()
e.parser.log("Ex: {} x {}".format(a, b))
e.context['stack'].append(a * b)
def div(e):
b = e.context['stack'].pop()
a = e.context['stack'].pop()
e.parser.log("Ex: {} / {}".format(a, b))
e.context['stack'].append(a / b)
if definition["fn"] == 'push_number':
return push_number
elif definition["fn"] == 'add':
return add
elif definition["fn"] == 'sub':
return sub
elif definition["fn"] == 'mul':
return mul
elif definition["fn"] == 'div':
return div
else:
return None
The grammar itself
This defines a bunch of terminal tokens. As stated above, it uses JSON with a single attribute called regex. Similarly, actions are
describes as JSON with a single attribute called fn. See comments in the above sections. Actions are executed when the production rule they annotate resolves.
DIGIT = { regex: '\d' }
LPAREN = { regex: '\(' }
RPAREN = { regex: '\)' }
PLUS = { regex: '\+' }
MINUS = { regex: '\-' }
MULT = { regex: '\*' }
DIV = { regex: '\/' }
parser : calc
number : DIGIT number
| DIGIT
expr : LPAREN calc RPAREN
| number {'fn': 'push_number'}
calc : expr PLUS expr {'fn': 'add'}
| expr MINUS expr {'fn': 'sub'}
| expr MULT expr {'fn': 'mul'}
| expr DIV expr {'fn': 'div'}
Execute
grammar = pyllk.load(path_to_grammar_file, token_fn=character_tokenizer, action_fn=make_action)
context = Context()
context.stack = []
parser = Parser(grammar)
parser.parse_string("((10+2)*15)/(7-2)", context)
result = context.stack.pop()
print(result)
Dev setup
Run make init
Metadata
Release files for pyllk 0.0.13
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pyllk-0.0.13.tar.gz | 13.3 kB | Without provenance | Details for pyllk-0.0.13.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pyllk-0.0.13-py3-none-any.whl | Python 3 | none | any | Details for pyllk-0.0.13-py3-none-any.whl |
Total release size: 27.2 kB
Release files / pyllk-0.0.13.tar.gz
| Download URL | pyllk-0.0.13.tar.gz |
|---|---|
| Size | 13.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ef294a41f617dd8fa6fb66dce3a563b18d0189eafd45dcc8814f39553ca3edaa
|
|
BLAKE2b-256 checksum How to use checksums |
faa15c5c971558a1150b9497269fdfb20b1d5bb484c08f1a4945b54eb2e04c18
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/3.1.1 pkginfo/1.5.0.1 requests/2.23.0 setuptools/47.1.1 requests-toolbelt/0.9.1 tqdm/4.46.1 CPython/3.8.1
|
Release files / pyllk-0.0.13-py3-none-any.whl
| Download URL | pyllk-0.0.13-py3-none-any.whl |
|---|---|
| Size | 13.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fbe8c76b116f73f5e9ebd4a7db2044830be858eb5890aafbac03b096d1a61264
|
|
BLAKE2b-256 checksum How to use checksums |
4c37c471f1940df2e5b97d18f42c18d5db878c9c6f80fcbd7660ccfe6ed82b75
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/3.1.1 pkginfo/1.5.0.1 requests/2.23.0 setuptools/47.1.1 requests-toolbelt/0.9.1 tqdm/4.46.1 CPython/3.8.1
|
Security for pyllk 0.0.13
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.