diff --git a/blueprintcompiler/ast_utils.py b/blueprintcompiler/ast_utils.py index 4895744..1962587 100644 --- a/blueprintcompiler/ast_utils.py +++ b/blueprintcompiler/ast_utils.py @@ -33,7 +33,10 @@ class Children: def __iter__(self): return iter(self._children) def __getitem__(self, key): - return [child for child in self._children if isinstance(child, key)] + if isinstance(key, int): + return self._children[key] + else: + return [child for child in self._children if isinstance(child, key)] class AstNode: diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 68c3d8a..feeb301 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -2,6 +2,7 @@ templates. """ from .attributes import BaseAttribute, BaseTypedAttribute +from .expression import Expr from .gobject_object import Object, ObjectContent from .gobject_property import Property from .gobject_signal import Signal diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py new file mode 100644 index 0000000..a684b39 --- /dev/null +++ b/blueprintcompiler/language/expression.py @@ -0,0 +1,63 @@ +# expressions.py +# +# Copyright 2022 James Westman +# +# This file is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This file is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program. If not, see . +# +# SPDX-License-Identifier: LGPL-3.0-or-later + + +from .common import * + + +expr = Pratt() + + +class Expr(AstNode): + grammar = expr + + def emit_xml(self, xml: XmlEmitter): + self.children[-1].emit_xml(xml) + + +class InfixExpr(AstNode): + @property + def lhs(self): + children = list(self.parent_by_type(Expr).children) + return children[children.index(self) - 1] + + +class IdentExpr(AstNode): + grammar = UseIdent("ident") + + def emit_xml(self, xml: XmlEmitter): + xml.start_tag("constant") + xml.put_text(self.tokens["ident"]) + xml.end_tag() + + +class LookupOp(InfixExpr): + grammar = [".", UseIdent("property")] + + def emit_xml(self, xml: XmlEmitter): + xml.start_tag("lookup", name=self.tokens["property"]) + self.lhs.emit_xml(xml) + xml.end_tag() + + +expr.children = [ + Prefix(IdentExpr), + Prefix(["(", Expr, ")"]), + Infix(10, LookupOp), +] diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index a542901..1c36dd1 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -18,6 +18,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +from .expression import Expr from .gobject_object import Object from .gtkbuilder_template import Template from .values import Value, TranslatedStringValue @@ -26,19 +27,27 @@ from .common import * class Property(AstNode): grammar = AnyOf( - Statement( + [ UseIdent("name"), ":", Keyword("bind"), - UseIdent("bind_source").expected("the ID of a source object to bind from"), + UseIdent("bind_source"), ".", - UseIdent("bind_property").expected("a property name to bind from"), + UseIdent("bind_property"), ZeroOrMore(AnyOf( ["no-sync-create", UseLiteral("no_sync_create", True)], ["inverted", UseLiteral("inverted", True)], ["bidirectional", UseLiteral("bidirectional", True)], Match("sync-create").warn("sync-create is deprecated in favor of no-sync-create"), )), + ";", + ], + Statement( + UseIdent("name"), + UseLiteral("binding", True), + ":", + "bind", + Expr, ), Statement( UseIdent("name"), @@ -146,7 +155,13 @@ class Property(AstNode): self.children[Object][0].emit_xml(xml) xml.end_tag() elif value is None: - xml.put_self_closing("property", **props) + if self.tokens["binding"]: + xml.start_tag("binding", **props) + for x in self.children: + x.emit_xml(xml) + xml.end_tag() + else: + xml.put_self_closing("property", **props); else: xml.start_tag("property", **props) value.emit_xml(xml) diff --git a/blueprintcompiler/parse_tree.py b/blueprintcompiler/parse_tree.py index e56a220..bc36415 100644 --- a/blueprintcompiler/parse_tree.py +++ b/blueprintcompiler/parse_tree.py @@ -98,6 +98,7 @@ class ParseContext: def __init__(self, tokens, index=0): self.tokens = list(tokens) + self.binding_power = 0 self.index = index self.start = index self.group = None @@ -118,6 +119,7 @@ class ParseContext: ctx = ParseContext(self.tokens, self.index) ctx.errors = self.errors ctx.warnings = self.warnings + ctx.binding_power = self.binding_power return ctx def apply_child(self, other): @@ -544,6 +546,64 @@ class Keyword(ParseNode): return str(token) == self.kw +class Prefix(ParseNode): + def __init__(self, child): + self.child = to_parse_node(child) + + def _parse(self, ctx: ParseContext): + return self.child.parse(ctx).succeeded() + + +class Infix(ParseNode): + def __init__(self, binding_power: int, child): + self.binding_power = binding_power + self.child = to_parse_node(child) + + def _parse(self, ctx: ParseContext): + ctx.binding_power = self.binding_power + return self.child.parse(ctx).succeeded() + + def __lt__(self, other): + return self.binding_power < other.binding_power + def __eq__(self, other): + return self.binding_power == other.binding_power + + +class Pratt(ParseNode): + """ Basic Pratt parser implementation. """ + + def __init__(self, *children): + self.children = children + + @property + def children(self): + return self._children + @children.setter + def children(self, children): + self._children = children + self.prefixes = [child for child in children if isinstance(child, Prefix)] + self.infixes = sorted([child for child in children if isinstance(child, Infix)], reverse=True) + + def _parse(self, ctx: ParseContext) -> bool: + for prefix in self.prefixes: + if prefix.parse(ctx).succeeded(): + break + else: + # none of the prefixes could be parsed + return False + + while True: + succeeded = False + for infix in self.infixes: + if infix.binding_power <= ctx.binding_power: + break + if infix.parse(ctx).succeeded(): + succeeded = True + break + if not succeeded: + return True + + def to_parse_node(value) -> ParseNode: if isinstance(value, str): return Match(value) diff --git a/tests/samples/expr_lookup.blp b/tests/samples/expr_lookup.blp new file mode 100644 index 0000000..d172f7e --- /dev/null +++ b/tests/samples/expr_lookup.blp @@ -0,0 +1,9 @@ +using Gtk 4.0; + +Overlay { + Label label {} +} + +Label { + label: bind (label.parent).child.label; +} diff --git a/tests/samples/expr_lookup.ui b/tests/samples/expr_lookup.ui new file mode 100644 index 0000000..2137e9b --- /dev/null +++ b/tests/samples/expr_lookup.ui @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + label + + + + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index c4a00a8..203b07f 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -136,6 +136,7 @@ class TestSamples(unittest.TestCase): self.assert_sample("combo_box_text") self.assert_sample("comments") self.assert_sample("enum") + self.assert_sample("expr_lookup") self.assert_sample("file_filter") self.assert_sample("flags") self.assert_sample("id_prop")