From 59aa054c4cd0e5a6ca609f98448c86a2fe09bb95 Mon Sep 17 00:00:00 2001 From: James Westman Date: Sat, 24 Dec 2022 21:46:03 -0600 Subject: [PATCH] language: Add closure expressions --- blueprintcompiler/gir.py | 6 ++ blueprintcompiler/language/__init__.py | 2 +- blueprintcompiler/language/expression.py | 57 +++++++++++++++++-- blueprintcompiler/outputs/xml/__init__.py | 10 +++- blueprintcompiler/tokenizer.py | 2 +- tests/sample_errors/expr_closure_not_cast.blp | 5 ++ tests/sample_errors/expr_closure_not_cast.err | 1 + tests/samples/expr_closure.blp | 5 ++ tests/samples/expr_closure.ui | 13 +++++ tests/test_samples.py | 2 + 10 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 tests/sample_errors/expr_closure_not_cast.blp create mode 100644 tests/sample_errors/expr_closure_not_cast.err create mode 100644 tests/samples/expr_closure.blp create mode 100644 tests/samples/expr_closure.ui diff --git a/blueprintcompiler/gir.py b/blueprintcompiler/gir.py index ab8af75..4ee2b1e 100644 --- a/blueprintcompiler/gir.py +++ b/blueprintcompiler/gir.py @@ -189,7 +189,10 @@ class TypeType(BasicType): _BASIC_TYPES = { + "bool": BoolType, "gboolean": BoolType, + "string": StringType, + "gchararray": StringType, "int": IntType, "gint": IntType, "gint64": IntType, @@ -730,6 +733,9 @@ class GirContext: return None def get_type(self, name: str, ns: str) -> T.Optional[GirNode]: + if ns is None and name in _BASIC_TYPES: + return _BASIC_TYPES[name]() + ns = ns or "Gtk" if ns not in self.namespaces: diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 0f1132b..4063943 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -1,5 +1,5 @@ from .attributes import BaseAttribute, BaseTypedAttribute -from .expression import CastExpr, IdentExpr, LookupOp, ExprChain +from .expression import CastExpr, ClosureExpr, Expr, ExprChain, IdentExpr, LookupOp 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 index 2d50984..f2b2ea1 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -25,13 +25,24 @@ from .types import TypeName expr = Pratt() -class Expr: +class Expr(AstNode): @property def type(self) -> T.Optional[GirType]: raise NotImplementedError() + @property + def rhs(self) -> T.Optional["Expr"]: + if isinstance(self.parent, ExprChain): + children = list(self.parent.children) + if children.index(self) + 1 < len(children): + return children[children.index(self) + 1] + else: + return self.parent.rhs + else: + return None -class ExprChain(Expr, AstNode): + +class ExprChain(Expr): grammar = expr @property @@ -43,14 +54,14 @@ class ExprChain(Expr, AstNode): return self.last.type -class InfixExpr(Expr, AstNode): +class InfixExpr(Expr): @property def lhs(self): children = list(self.parent_by_type(ExprChain).children) return children[children.index(self) - 1] -class IdentExpr(Expr, AstNode): +class IdentExpr(Expr): grammar = UseIdent("ident") @property @@ -124,7 +135,45 @@ class CastExpr(InfixExpr): ) +class ClosureExpr(Expr): + grammar = [ + Optional(["$", UseLiteral("extern", True)]), + UseIdent("name"), + "(", + Delimited(ExprChain, ","), + ")", + ] + + @property + def type(self) -> T.Optional[GirType]: + if isinstance(self.rhs, CastExpr): + return self.rhs.type + else: + return None + + @property + def closure_name(self) -> str: + return self.tokens["name"] + + @property + def args(self) -> T.List[ExprChain]: + return self.children[ExprChain] + + @validate() + def cast_to_return_type(self): + if not isinstance(self.rhs, CastExpr): + raise CompileError( + "Closure expression must be cast to the closure's return type" + ) + + @validate() + def builtin_exists(self): + if not self.tokens["extern"]: + raise CompileError(f"{self.closure_name} is not a builtin function") + + expr.children = [ + Prefix(ClosureExpr), Prefix(IdentExpr), Prefix(["(", ExprChain, ")"]), Infix(10, LookupOp), diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index 69ce12f..ac2aea8 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -179,7 +179,7 @@ class XmlOutput(OutputFormat): def _emit_expression(self, expression: ExprChain, xml: XmlEmitter): self._emit_expression_part(expression.children[-1], xml) - def _emit_expression_part(self, expression, xml: XmlEmitter): + def _emit_expression_part(self, expression: Expr, xml: XmlEmitter): if isinstance(expression, IdentExpr): self._emit_ident_expr(expression, xml) elif isinstance(expression, LookupOp): @@ -188,6 +188,8 @@ class XmlOutput(OutputFormat): self._emit_expression(expression, xml) elif isinstance(expression, CastExpr): self._emit_cast_expr(expression, xml) + elif isinstance(expression, ClosureExpr): + self._emit_closure_expr(expression, xml) else: raise CompilerBugError() @@ -204,6 +206,12 @@ class XmlOutput(OutputFormat): def _emit_cast_expr(self, expr: CastExpr, xml: XmlEmitter): self._emit_expression_part(expr.lhs, xml) + def _emit_closure_expr(self, expr: ClosureExpr, xml: XmlEmitter): + xml.start_tag("closure", function=expr.closure_name, type=expr.type) + for arg in expr.args: + self._emit_expression_part(arg, xml) + xml.end_tag() + def _emit_attribute( self, tag: str, attr: str, name: str, value: Value, xml: XmlEmitter ): diff --git a/blueprintcompiler/tokenizer.py b/blueprintcompiler/tokenizer.py index 4991967..516bc0b 100644 --- a/blueprintcompiler/tokenizer.py +++ b/blueprintcompiler/tokenizer.py @@ -46,7 +46,7 @@ _tokens = [ (TokenType.WHITESPACE, r"\s+"), (TokenType.COMMENT, r"\/\*[\s\S]*?\*\/"), (TokenType.COMMENT, r"\/\/[^\n]*"), - (TokenType.OP, r"<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"), + (TokenType.OP, r"\$|<<|>>|=>|::|<|>|:=|\.|\|\||\||\+|\-|\*|=|:|/"), (TokenType.PUNCTUATION, r"\(|\)|\{|\}|;|\[|\]|\,"), ] _TOKENS = [(type, re.compile(regex)) for (type, regex) in _tokens] diff --git a/tests/sample_errors/expr_closure_not_cast.blp b/tests/sample_errors/expr_closure_not_cast.blp new file mode 100644 index 0000000..7903bb6 --- /dev/null +++ b/tests/sample_errors/expr_closure_not_cast.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label { + label: bind $closure(); +} \ No newline at end of file diff --git a/tests/sample_errors/expr_closure_not_cast.err b/tests/sample_errors/expr_closure_not_cast.err new file mode 100644 index 0000000..fcc2dfb --- /dev/null +++ b/tests/sample_errors/expr_closure_not_cast.err @@ -0,0 +1 @@ +4,15,10,Closure expression must be cast to the closure's return type \ No newline at end of file diff --git a/tests/samples/expr_closure.blp b/tests/samples/expr_closure.blp new file mode 100644 index 0000000..99874e8 --- /dev/null +++ b/tests/samples/expr_closure.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label my-label { + label: bind ($my-closure(my-label.margin-bottom)) as (string); +} \ No newline at end of file diff --git a/tests/samples/expr_closure.ui b/tests/samples/expr_closure.ui new file mode 100644 index 0000000..1581d65 --- /dev/null +++ b/tests/samples/expr_closure.ui @@ -0,0 +1,13 @@ + + + + + + + + my-label + + + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index 1195cdf..6d809c5 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -151,6 +151,7 @@ class TestSamples(unittest.TestCase): self.assert_sample("combo_box_text") self.assert_sample("comments") self.assert_sample("enum") + self.assert_sample("expr_closure", skip_run=True) # The closure doesn't exist self.assert_sample("expr_lookup") self.assert_sample("file_filter") self.assert_sample("flags") @@ -208,6 +209,7 @@ class TestSamples(unittest.TestCase): self.assert_sample_error("empty") self.assert_sample_error("enum_member_dne") self.assert_sample_error("expr_cast_conversion") + self.assert_sample_error("expr_closure_not_cast") self.assert_sample_error("expr_lookup_dne") self.assert_sample_error("expr_lookup_no_properties") self.assert_sample_error("filters_in_non_file_filter")