parser: Simplify parser construction

- Replace several different parse nodes with Match, which matches the
  exact text of a token but not the token type
- Allow arrays to be used in place of Sequence
This commit is contained in:
James Westman 2022-01-04 16:59:19 -06:00
parent ad6a2cf538
commit 8d587b62a0
No known key found for this signature in database
GPG key ID: CE2DBA0ADB654EA6
11 changed files with 182 additions and 211 deletions

View file

@ -155,18 +155,18 @@ a11y_prop = Group(
A11yProperty, A11yProperty,
Statement( Statement(
UseIdent("name"), UseIdent("name"),
Op(":"), ":",
value.expected("a value"), value.expected("a value"),
) )
) )
a11y = Group( a11y = Group(
A11y, A11y,
Sequence( [
Keyword("accessibility", True), Keyword("accessibility"),
OpenBlock(), "{",
Until(a11y_prop, CloseBlock()), Until(a11y_prop, "}"),
) ]
) )

View file

@ -52,28 +52,23 @@ class Item(BaseTypedAttribute):
item = Group( item = Group(
Item, Item,
Sequence( [
Optional( Optional([
Sequence(
UseIdent("name"), UseIdent("name"),
Op(":"), ":",
) ]),
),
value, value,
) ]
) )
items = Group( items = Group(
Items, Items,
Sequence( [
Keyword("items", True), Keyword("items"),
OpenBracket(), "[",
Delimited( Delimited(item, ","),
item, "]",
Comma() ]
),
CloseBracket(),
)
) )

View file

@ -49,22 +49,22 @@ class FilterString(AstNode):
def create_node(tag_name: str, singular: str): def create_node(tag_name: str, singular: str):
return Group( return Group(
Filters, Filters,
Sequence( [
Keyword(tag_name, True), Keyword(tag_name),
UseLiteral("tag_name", tag_name), UseLiteral("tag_name", tag_name),
OpenBracket(), "[",
Delimited( Delimited(
Group( Group(
FilterString, FilterString,
Sequence( [
UseQuoted("name"), UseQuoted("name"),
UseLiteral("tag_name", singular), UseLiteral("tag_name", singular),
) ]
), ),
Comma(), ",",
), ),
CloseBracket(), "]",
) ]
) )

View file

@ -53,7 +53,7 @@ layout_prop = Group(
LayoutProperty, LayoutProperty,
Statement( Statement(
UseIdent("name"), UseIdent("name"),
Op(":"), ":",
value.expected("a value"), value.expected("a value"),
) )
) )
@ -61,9 +61,9 @@ layout_prop = Group(
layout = Group( layout = Group(
Layout, Layout,
Sequence( Sequence(
Keyword("layout", True), Keyword("layout"),
OpenBlock(), "{",
Until(layout_prop, CloseBlock()), Until(layout_prop, "}"),
) )
) )

View file

@ -51,94 +51,94 @@ menu_contents = Sequence()
menu_section = Group( menu_section = Group(
Menu, Menu,
Sequence( [
Keyword("section"), "section",
UseLiteral("tag", "section"), UseLiteral("tag", "section"),
Optional(UseIdent("id")), Optional(UseIdent("id")),
menu_contents menu_contents
) ]
) )
menu_submenu = Group( menu_submenu = Group(
Menu, Menu,
Sequence( [
Keyword("submenu"), "submenu",
UseLiteral("tag", "submenu"), UseLiteral("tag", "submenu"),
Optional(UseIdent("id")), Optional(UseIdent("id")),
menu_contents menu_contents
) ]
) )
menu_attribute = Group( menu_attribute = Group(
MenuAttribute, MenuAttribute,
Sequence( [
UseIdent("name"), UseIdent("name"),
Op(":"), ":",
value.expected("a value"), value.expected("a value"),
StmtEnd().expected("`;`"), Match(";").expected(),
) ]
) )
menu_item = Group( menu_item = Group(
Menu, Menu,
Sequence( [
Keyword("item"), "item",
UseLiteral("tag", "item"), UseLiteral("tag", "item"),
Optional(UseIdent("id")), Optional(UseIdent("id")),
OpenBlock().expected("`{`"), Match("{").expected(),
Until(menu_attribute, CloseBlock()), Until(menu_attribute, "}"),
) ]
) )
menu_item_shorthand = Group( menu_item_shorthand = Group(
Menu, Menu,
Sequence( [
Keyword("item"), "item",
UseLiteral("tag", "item"), UseLiteral("tag", "item"),
OpenParen(), "(",
Group( Group(
MenuAttribute, MenuAttribute,
Sequence(UseLiteral("name", "label"), value), [UseLiteral("name", "label"), value],
), ),
Optional(Sequence( Optional([
Comma(), ",",
Optional(Sequence( Optional([
Group( Group(
MenuAttribute, MenuAttribute,
Sequence(UseLiteral("name", "action"), value), [UseLiteral("name", "action"), value],
), ),
Optional(Sequence( Optional([
Comma(), ",",
Group( Group(
MenuAttribute, MenuAttribute,
Sequence(UseLiteral("name", "icon"), value), [UseLiteral("name", "icon"), value],
), ),
)) ])
)) ])
)), ]),
CloseParen().expected("')'"), Match(")").expected(),
) ]
) )
menu_contents.children = [ menu_contents.children = [
OpenBlock(), Match("{"),
Until(AnyOf( Until(AnyOf(
menu_section, menu_section,
menu_submenu, menu_submenu,
menu_item_shorthand, menu_item_shorthand,
menu_item, menu_item,
menu_attribute, menu_attribute,
), CloseBlock()), ), "}"),
] ]
menu = Group( menu = Group(
Menu, Menu,
Sequence( [
Keyword("menu"), "menu",
UseLiteral("tag", "menu"), UseLiteral("tag", "menu"),
Optional(UseIdent("id")), Optional(UseIdent("id")),
menu_contents menu_contents
), ],
) )

View file

@ -60,18 +60,18 @@ class Widget(AstNode):
widgets = Group( widgets = Group(
Widgets, Widgets,
Sequence( [
Keyword("widgets", True), Keyword("widgets"),
OpenBracket(), "[",
Delimited( Delimited(
Group( Group(
Widget, Widget,
UseIdent("name"), UseIdent("name"),
), ),
Comma(), ",",
), ),
CloseBracket(), "]",
) ]
) )

View file

@ -61,15 +61,12 @@ item = Group(
strings = Group( strings = Group(
Items, Items,
Sequence( [
Keyword("strings", True), Keyword("strings"),
OpenBracket(), "[",
Delimited( Delimited(item, ","),
item, "]",
Comma() ]
),
CloseBracket(),
)
) )

View file

@ -46,18 +46,18 @@ class StyleClass(AstNode):
styles = Group( styles = Group(
Styles, Styles,
Sequence( [
Keyword("styles", True), Keyword("styles"),
OpenBracket(), "[",
Delimited( Delimited(
Group( Group(
StyleClass, StyleClass,
UseQuoted("name") UseQuoted("name")
), ),
Comma(), ",",
), ),
CloseBracket(), "]",
) ]
) )

View file

@ -239,7 +239,7 @@ class Err(ParseNode):
""" ParseNode that emits a compile error if it fails to parse. """ """ ParseNode that emits a compile error if it fails to parse. """
def __init__(self, child, message): def __init__(self, child, message):
self.child = child self.child = to_parse_node(child)
self.message = message self.message = message
def _parse(self, ctx): def _parse(self, ctx):
@ -258,7 +258,7 @@ class Fail(ParseNode):
""" ParseNode that emits a compile error if it parses successfully. """ """ ParseNode that emits a compile error if it parses successfully. """
def __init__(self, child, message): def __init__(self, child, message):
self.child = child self.child = to_parse_node(child)
self.message = message self.message = message
def _parse(self, ctx): def _parse(self, ctx):
@ -277,7 +277,7 @@ class Group(ParseNode):
""" ParseNode that creates a match group. """ """ ParseNode that creates a match group. """
def __init__(self, ast_type, child): def __init__(self, ast_type, child):
self.ast_type = ast_type self.ast_type = ast_type
self.child = child self.child = to_parse_node(child)
def _parse(self, ctx: ParseContext) -> bool: def _parse(self, ctx: ParseContext) -> bool:
ctx.skip() ctx.skip()
@ -288,7 +288,7 @@ class Group(ParseNode):
class Sequence(ParseNode): class Sequence(ParseNode):
""" ParseNode that attempts to match all of its children in sequence. """ """ ParseNode that attempts to match all of its children in sequence. """
def __init__(self, *children): def __init__(self, *children):
self.children = children self.children = [to_parse_node(child) for child in children]
def _parse(self, ctx) -> bool: def _parse(self, ctx) -> bool:
for child in self.children: for child in self.children:
@ -301,7 +301,7 @@ class Statement(ParseNode):
""" ParseNode that attempts to match all of its children in sequence. If any """ ParseNode that attempts to match all of its children in sequence. If any
child raises an error, the error will be logged but parsing will continue. """ child raises an error, the error will be logged but parsing will continue. """
def __init__(self, *children): def __init__(self, *children):
self.children = children self.children = [to_parse_node(child) for child in children]
def _parse(self, ctx) -> bool: def _parse(self, ctx) -> bool:
for child in self.children: for child in self.children:
@ -325,7 +325,7 @@ class AnyOf(ParseNode):
""" ParseNode that attempts to match exactly one of its children. Child """ ParseNode that attempts to match exactly one of its children. Child
nodes are attempted in order. """ nodes are attempted in order. """
def __init__(self, *children): def __init__(self, *children):
self.children = children self.children = [to_parse_node(child) for child in children]
def _parse(self, ctx): def _parse(self, ctx):
for child in self.children: for child in self.children:
@ -339,8 +339,8 @@ class Until(ParseNode):
the child does not match, one token is skipped and the match is attempted the child does not match, one token is skipped and the match is attempted
again. """ again. """
def __init__(self, child, delimiter): def __init__(self, child, delimiter):
self.child = child self.child = to_parse_node(child)
self.delimiter = delimiter self.delimiter = to_parse_node(delimiter)
def _parse(self, ctx): def _parse(self, ctx):
while not self.delimiter.parse(ctx).succeeded(): while not self.delimiter.parse(ctx).succeeded():
@ -362,7 +362,7 @@ class ZeroOrMore(ParseNode):
times). It cannot fail to parse. If its child raises an exception, one token times). It cannot fail to parse. If its child raises an exception, one token
will be skipped and parsing will continue. """ will be skipped and parsing will continue. """
def __init__(self, child): def __init__(self, child):
self.child = child self.child = to_parse_node(child)
def _parse(self, ctx): def _parse(self, ctx):
@ -379,8 +379,8 @@ class Delimited(ParseNode):
""" ParseNode that matches its first child any number of times (including zero """ ParseNode that matches its first child any number of times (including zero
times) with its second child in between and optionally at the end. """ times) with its second child in between and optionally at the end. """
def __init__(self, child, delimiter): def __init__(self, child, delimiter):
self.child = child self.child = to_parse_node(child)
self.delimiter = delimiter self.delimiter = to_parse_node(delimiter)
def _parse(self, ctx): def _parse(self, ctx):
while self.child.parse(ctx).matched() and self.delimiter.parse(ctx).matched(): while self.child.parse(ctx).matched() and self.delimiter.parse(ctx).matched():
@ -392,60 +392,36 @@ class Optional(ParseNode):
""" ParseNode that matches its child zero or one times. It cannot fail to """ ParseNode that matches its child zero or one times. It cannot fail to
parse. """ parse. """
def __init__(self, child): def __init__(self, child):
self.child = child self.child = to_parse_node(child)
def _parse(self, ctx): def _parse(self, ctx):
self.child.parse(ctx) self.child.parse(ctx)
return True return True
class StaticToken(ParseNode): class Eof(ParseNode):
""" Base class for ParseNodes that match a token type without inspecting """ ParseNode that matches an EOF token. """
the token's contents. """
token_type: T.Optional[TokenType] = None
def _parse(self, ctx: ParseContext) -> bool: def _parse(self, ctx: ParseContext) -> bool:
return ctx.next_token().type == self.token_type token = ctx.next_token()
return token.type == TokenType.EOF
class StmtEnd(StaticToken):
token_type = TokenType.STMT_END
class Eof(StaticToken):
token_type = TokenType.EOF
class OpenBracket(StaticToken):
token_type = TokenType.OPEN_BRACKET
class CloseBracket(StaticToken):
token_type = TokenType.CLOSE_BRACKET
class OpenBlock(StaticToken):
token_type = TokenType.OPEN_BLOCK
class CloseBlock(StaticToken):
token_type = TokenType.CLOSE_BLOCK
class OpenParen(StaticToken):
token_type = TokenType.OPEN_PAREN
class CloseParen(StaticToken):
token_type = TokenType.CLOSE_PAREN
class Comma(StaticToken):
token_type = TokenType.COMMA
class Op(ParseNode): class Match(ParseNode):
""" ParseNode that matches the given operator. """ """ ParseNode that matches the given literal token. """
def __init__(self, op): def __init__(self, op):
self.op = op self.op = op
def _parse(self, ctx: ParseContext) -> bool: def _parse(self, ctx: ParseContext) -> bool:
token = ctx.next_token() token = ctx.next_token()
if token.type != TokenType.OP:
return False
return str(token) == self.op return str(token) == self.op
def expected(self, expect: str = None):
""" Convenience method for err(). """
if expect is None:
return self.err(f"Expected '{self.op}'")
else:
return self.err("Expected " + expect)
class UseIdent(ParseNode): class UseIdent(ParseNode):
""" ParseNode that matches any identifier and sets it in a key=value pair on """ ParseNode that matches any identifier and sets it in a key=value pair on
@ -529,17 +505,22 @@ class UseLiteral(ParseNode):
class Keyword(ParseNode): class Keyword(ParseNode):
""" Matches the given identifier. """ """ Matches the given identifier and sets it as a named token, with the name
def __init__(self, kw, set_token=False): being the identifier itself. """
def __init__(self, kw):
self.kw = kw self.kw = kw
self.set_token = True self.set_token = True
def _parse(self, ctx: ParseContext): def _parse(self, ctx: ParseContext):
token = ctx.next_token() token = ctx.next_token()
if token.type != TokenType.IDENT:
return False
if self.set_token:
ctx.set_group_val(self.kw, True, token) ctx.set_group_val(self.kw, True, token)
return str(token) == self.kw return str(token) == self.kw
def to_parse_node(value) -> ParseNode:
if isinstance(value, str):
return Match(value)
elif isinstance(value, list):
return Sequence(*value)
else:
return value

View file

@ -32,8 +32,8 @@ def parse(tokens) -> T.Tuple[ast.UI, T.Optional[MultipleErrors]]:
gtk_directive = Group( gtk_directive = Group(
ast.GtkDirective, ast.GtkDirective,
Statement( Statement(
Keyword("using").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"), Match("using").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"),
Keyword("Gtk").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"), Match("Gtk").err("File must start with a \"using Gtk\" directive (e.g. `using Gtk 4.0;`)"),
UseNumberText("version").expected("a version number for GTK"), UseNumberText("version").expected("a version number for GTK"),
) )
) )
@ -41,7 +41,7 @@ def parse(tokens) -> T.Tuple[ast.UI, T.Optional[MultipleErrors]]:
import_statement = Group( import_statement = Group(
ast.Import, ast.Import,
Statement( Statement(
Keyword("using"), "using",
UseIdent("namespace").expected("a GIR namespace"), UseIdent("namespace").expected("a GIR namespace"),
UseNumberText("version").expected("a version number"), UseNumberText("version").expected("a version number"),
) )
@ -56,7 +56,7 @@ def parse(tokens) -> T.Tuple[ast.UI, T.Optional[MultipleErrors]]:
ast.Property, ast.Property,
Statement( Statement(
UseIdent("name"), UseIdent("name"),
Op(":"), ":",
AnyOf( AnyOf(
*OBJECT_HOOKS, *OBJECT_HOOKS,
object, object,
@ -69,15 +69,15 @@ def parse(tokens) -> T.Tuple[ast.UI, T.Optional[MultipleErrors]]:
ast.Property, ast.Property,
Statement( Statement(
UseIdent("name"), UseIdent("name"),
Op(":"), ":",
Keyword("bind"), "bind",
UseIdent("bind_source").expected("the ID of a source object to bind from"), UseIdent("bind_source").expected("the ID of a source object to bind from"),
Op("."), ".",
UseIdent("bind_property").expected("a property name to bind from"), UseIdent("bind_property").expected("a property name to bind from"),
ZeroOrMore(AnyOf( ZeroOrMore(AnyOf(
Sequence(Keyword("sync-create"), UseLiteral("sync_create", True)), ["sync-create", UseLiteral("sync_create", True)],
Sequence(Keyword("inverted"), UseLiteral("invert-boolean", True)), ["inverted", UseLiteral("inverted", True)],
Sequence(Keyword("bidirectional"), UseLiteral("bidirectional", True)), ["bidirectional", UseLiteral("bidirectional", True)],
)), )),
) )
) )
@ -86,47 +86,47 @@ def parse(tokens) -> T.Tuple[ast.UI, T.Optional[MultipleErrors]]:
ast.Signal, ast.Signal,
Statement( Statement(
UseIdent("name"), UseIdent("name"),
Optional(Sequence( Optional([
Op("::"), "::",
UseIdent("detail_name").expected("a signal detail name"), UseIdent("detail_name").expected("a signal detail name"),
)), ]),
Op("=>"), "=>",
UseIdent("handler").expected("the name of a function to handle the signal"), UseIdent("handler").expected("the name of a function to handle the signal"),
OpenParen().expected("argument list"), Match("(").expected("argument list"),
Optional(UseIdent("object")).expected("object identifier"), Optional(UseIdent("object")).expected("object identifier"),
CloseParen().expected("`)`"), Match(")").expected(),
ZeroOrMore(AnyOf( ZeroOrMore(AnyOf(
Sequence(Keyword("swapped"), UseLiteral("swapped", True)), [Keyword("swapped"), UseLiteral("swapped", True)],
Sequence(Keyword("after"), UseLiteral("after", True)), [Keyword("after"), UseLiteral("after", True)],
)), )),
) )
) )
child = Group( child = Group(
ast.Child, ast.Child,
Sequence( [
Optional(Sequence( Optional([
OpenBracket(), "[",
Optional(Sequence(Keyword("internal-child"), UseLiteral("internal_child", True))), Optional(["internal-child", UseLiteral("internal_child", True)]),
UseIdent("child_type").expected("a child type"), UseIdent("child_type").expected("a child type"),
CloseBracket(), "]",
)), ]),
object, object,
) ]
) )
object_content = Group( object_content = Group(
ast.ObjectContent, ast.ObjectContent,
Sequence( [
OpenBlock(), "{",
Until(AnyOf( Until(AnyOf(
*OBJECT_CONTENT_HOOKS, *OBJECT_CONTENT_HOOKS,
binding, binding,
property, property,
signal, signal,
child, child,
), CloseBlock()), ), "}"),
) ]
) )
# work around the recursive reference # work around the recursive reference
@ -138,22 +138,20 @@ def parse(tokens) -> T.Tuple[ast.UI, T.Optional[MultipleErrors]]:
template = Group( template = Group(
ast.Template, ast.Template,
Sequence( [
Keyword("template"), "template",
UseIdent("name").expected("template class name"), UseIdent("name").expected("template class name"),
Optional( Optional([
Sequence( Match(":"),
Op(":"),
class_name.expected("parent class"), class_name.expected("parent class"),
) ]),
),
object_content.expected("block"), object_content.expected("block"),
) ]
) )
ui = Group( ui = Group(
ast.UI, ast.UI,
Sequence( [
gtk_directive, gtk_directive,
ZeroOrMore(import_statement), ZeroOrMore(import_statement),
Until(AnyOf( Until(AnyOf(
@ -161,7 +159,7 @@ def parse(tokens) -> T.Tuple[ast.UI, T.Optional[MultipleErrors]]:
template, template,
object, object,
), Eof()), ), Eof()),
) ]
) )
ctx = ParseContext(tokens) ctx = ParseContext(tokens)

View file

@ -23,16 +23,16 @@ from .parse_tree import *
class_name = AnyOf( class_name = AnyOf(
Sequence( [
UseIdent("namespace"), UseIdent("namespace"),
Op("."), ".",
UseIdent("class_name"), UseIdent("class_name"),
), ],
Sequence( [
Op("."), ".",
UseIdent("class_name"), UseIdent("class_name"),
UseLiteral("ignore_gir", True), UseLiteral("ignore_gir", True),
), ],
UseIdent("class_name"), UseIdent("class_name"),
) )
@ -51,31 +51,31 @@ ident_value = Group(
flags_value = Group( flags_value = Group(
ast.FlagsValue, ast.FlagsValue,
Sequence( [
Group(ast.Flag, UseIdent("value")), Group(ast.Flag, UseIdent("value")),
Op("|"), "|",
Delimited(Group(ast.Flag, UseIdent("value")), Op("|")), Delimited(Group(ast.Flag, UseIdent("value")), "|"),
), ],
) )
translated_string = Group( translated_string = Group(
ast.TranslatedStringValue, ast.TranslatedStringValue,
AnyOf( AnyOf(
Sequence( [
Keyword("_"), "_",
OpenParen(), "(",
UseQuoted("value").expected("a quoted string"), UseQuoted("value").expected("a quoted string"),
CloseParen().expected("`)`"), Match(")").expected(),
), ],
Sequence( [
Keyword("C_"), "C_",
OpenParen(), "(",
UseQuoted("context").expected("a quoted string"), UseQuoted("context").expected("a quoted string"),
Comma(), ",",
UseQuoted("value").expected("a quoted string"), UseQuoted("value").expected("a quoted string"),
Optional(Comma()), Optional(","),
CloseParen().expected("`)`"), Match(")").expected(),
), ],
), ),
) )