I'm writing toy parsers for toy languages to understand how parsers work.
Assuming a language as follows (in sketchy EBNF patois)
expression := lparen [datum] [expression [datum]] rparen
datum := any string of characters other than ( or )
rparen := (
lparen := )
These are some valid expressions
(hello(one(two(three)a)b)c)
(()abc)
((()))
whereas the following strings are illegal
)(
abc
((abc)
((abc)(
((abc)(abc))
So, these are strings consisting of balanced parentheses, with alphanumeric strings possible between both open and closed parentheses. The data can be arbitrarily nested, but not repeating (so each pair of parentheses contains at most one pair of parentheses at its toplevel -- see the last example in the block above).
I wrote a Python implementation of a parser for this language as follows:
import inspect
class Parser:
def __init__(self, expression, verbose=True):
self.expression = expression
self.position = 0
self.tokens = []
self.paren_state = 0
self.verbose = verbose
def peek(self):
if self.position + 1 == len(self.expression):
return "EOF"
else:
return self.expression[self.position + 1]
def advance(self):
self.pretty_print_stack()
if (self.position + 1) >= len(self.expression):
return self.tokens
else:
self.position += 1
def current(self):
return self.expression[self.position]
def parse_paren(self):
self.pretty_print_stack()
if self.current() == "(":
self.parse_lparen()
elif self.current() == ")":
self.parse_rparen()
else:
raise Exception(f"Expected parentheses at {self.position}")
def parse_lparen(self):
self.pretty_print_stack()
if self.current() == "(":
self.paren_state += 1
self.advance()
else:
raise Exception(f"Expected left parentheses at position {self.position}")
def parse_rparen(self):
self.pretty_print_stack()
if self.current() == ")":
self.paren_state -= 1
if self.paren_state < 0:
raise Exception(f"Imbalanced parentheses")
else:
self.advance()
else:
raise Exception(f"Expected right parentheses at position {self.position}")
def is_paren(self):
return self.current() in ["(", ")"]
def parse_datum(self):
self.pretty_print_stack()
buffer = ""
while not self.is_paren():
self.pretty_print_stack()
buffer += self.current()
self.advance()
if buffer:
self.tokens.append(buffer)
def parse_expression(self):
self.pretty_print_stack()
if self.peek() != "EOF":
self.parse_paren()
self.parse_datum()
self.parse_expression()
else:
if self.paren_state > 1 or self.current() != ")":
raise Exception("Imbalanced parentheses")
return self.tokens
def pretty_print_stack(self):
if self.verbose:
pretty_printed = ""
for idx, char in enumerate(self.expression):
if idx == self.position:
pretty_printed += f">{char}<"
else:
pretty_printed += char
current_frame = inspect.currentframe()
caller_frame = inspect.getouterframes(current_frame, 2)
print(f"{pretty_printed:<20} Position {self.position}, in {caller_frame[1][3]}, paren_state: {self.paren_state} peek: {self.peek()}")
This has the expected behavior, which is to bomb on imbalanced parentheses, and build up a list of "tokens" (actually "datums", which are the substrings between the parentheses).
I feel it has some issues
The way I detect imbalanced parentheses (adding 1 to
paren_state
everytime anlparen
is encountered and subtracting 1 if anrparen
is encountered, then making sure the value ofparen_state
is always non-negative) feels like a hack. Is there a better way to do this?The code in
Parser.parse_expression
doesn't "read" like the grammar as recursive descent parsers are supposed to, and overall the implementation feels clumsyRelated to the grammar, I have a vague sense that it would help if I could refactor the grammatical definition of
expression
so that it isn't recursive, but I'm not sure how to do this (or if it is even possible) since the language has a recursive structure
Happy and grateful for any thoughts and feedback on this.