From 02344139c22deec3bbce18010b97105f72dd525c Mon Sep 17 00:00:00 2001 From: James Westman Date: Tue, 24 Dec 2024 12:54:23 -0600 Subject: [PATCH 1/6] language: Add expression literals Add expression literals, so you can set properties of type Gtk.Expression. --- blueprintcompiler/completions.py | 9 +++++ blueprintcompiler/language/__init__.py | 1 + blueprintcompiler/language/contexts.py | 6 +++ blueprintcompiler/language/expression.py | 30 ++++++++++++++ blueprintcompiler/language/gobject_object.py | 13 ++++++- .../language/gobject_property.py | 7 ++-- blueprintcompiler/language/values.py | 39 ++++++++++++++++++- blueprintcompiler/outputs/xml/__init__.py | 14 ++++--- docs/reference/expressions.rst | 39 +++++++++++++++---- docs/reference/objects.rst | 2 +- tests/sample_errors/expr_item_not_cast.blp | 5 +++ tests/sample_errors/expr_item_not_cast.err | 1 + tests/sample_errors/expr_value_assignment.blp | 5 +++ tests/sample_errors/expr_value_assignment.err | 1 + .../sample_errors/expr_value_closure_arg.blp | 5 +++ .../sample_errors/expr_value_closure_arg.err | 1 + tests/sample_errors/expr_value_item.blp | 5 +++ tests/sample_errors/expr_value_item.err | 1 + tests/samples/bind_expr_prop.blp | 9 +++++ tests/samples/bind_expr_prop.ui | 17 ++++++++ tests/samples/bind_literal.blp | 5 +++ tests/samples/bind_literal.ui | 14 +++++++ tests/samples/expr_value.blp | 5 +++ tests/samples/expr_value.ui | 14 +++++++ tests/samples/expr_value_closure.blp | 5 +++ tests/samples/expr_value_closure.ui | 16 ++++++++ tests/samples/expr_value_literal.blp | 5 +++ tests/samples/expr_value_literal.ui | 14 +++++++ tests/test_samples.py | 1 + 29 files changed, 268 insertions(+), 21 deletions(-) create mode 100644 tests/sample_errors/expr_item_not_cast.blp create mode 100644 tests/sample_errors/expr_item_not_cast.err create mode 100644 tests/sample_errors/expr_value_assignment.blp create mode 100644 tests/sample_errors/expr_value_assignment.err create mode 100644 tests/sample_errors/expr_value_closure_arg.blp create mode 100644 tests/sample_errors/expr_value_closure_arg.err create mode 100644 tests/sample_errors/expr_value_item.blp create mode 100644 tests/sample_errors/expr_value_item.err create mode 100644 tests/samples/bind_expr_prop.blp create mode 100644 tests/samples/bind_expr_prop.ui create mode 100644 tests/samples/bind_literal.blp create mode 100644 tests/samples/bind_literal.ui create mode 100644 tests/samples/expr_value.blp create mode 100644 tests/samples/expr_value.ui create mode 100644 tests/samples/expr_value_closure.blp create mode 100644 tests/samples/expr_value_closure.ui create mode 100644 tests/samples/expr_value_literal.blp create mode 100644 tests/samples/expr_value_literal.ui diff --git a/blueprintcompiler/completions.py b/blueprintcompiler/completions.py index 5d36739..b10ec3e 100644 --- a/blueprintcompiler/completions.py +++ b/blueprintcompiler/completions.py @@ -177,6 +177,15 @@ def property_completer(lsp, ast_node, match_variables): docs=prop.doc, detail=prop.detail, ) + elif prop.type.full_name == "Gtk.Expression": + yield Completion( + prop_name, + CompletionItemKind.Property, + sort_text=f"0 {prop_name}", + snippet=f"{prop_name}: expr $0;", + docs=prop.doc, + detail=prop.detail, + ) else: yield Completion( prop_name, diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index e797eaa..5eb2b60 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -41,6 +41,7 @@ from .types import ClassName from .ui import UI from .values import ( ArrayValue, + ExprValue, Flag, Flags, IdentLiteral, diff --git a/blueprintcompiler/language/contexts.py b/blueprintcompiler/language/contexts.py index c5e97b3..6e26048 100644 --- a/blueprintcompiler/language/contexts.py +++ b/blueprintcompiler/language/contexts.py @@ -79,3 +79,9 @@ class ScopeCtx: for child in node.children: if child.context[ScopeCtx] is self: yield from self._iter_recursive(child) + + +@dataclass +class ExprValueCtx: + """Indicates that the context is an expression literal, where the + "item" keyword may be used.""" diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index f305035..e0b4246 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -81,6 +81,16 @@ class LiteralExpr(ExprBase): or self.root.is_legacy_template(self.literal.value.ident) ) + @property + def is_this(self) -> bool: + from .values import IdentLiteral + + return ( + not self.is_object + and isinstance(self.literal.value, IdentLiteral) + and self.literal.value.ident == "item" + ) + @property def literal(self): from .values import Literal @@ -91,6 +101,15 @@ class LiteralExpr(ExprBase): def type(self) -> T.Optional[GirType]: return self.literal.value.type + @validate() + def item_validations(self): + if self.is_this: + if not isinstance(self.rhs, CastExpr): + raise CompileError('"item" must be cast to its object type') + + if not isinstance(self.rhs.rhs, LookupOp): + raise CompileError('"item" can only be used for looking up properties') + class LookupOp(InfixExpr): grammar = [".", UseIdent("property")] @@ -285,6 +304,9 @@ expr.children = [ def decompile_lookup( ctx: DecompileCtx, gir: gir.GirContext, cdata: str, name: str, type: str ): + if ctx.parent_node is not None and ctx.parent_node.tag == "property": + ctx.print("expr ") + if t := ctx.type_by_cname(type): type = decompile.full_name(t) else: @@ -304,6 +326,8 @@ def decompile_lookup( if constant is not None: if constant == ctx.template_class: ctx.print("template." + name) + elif constant == "": + ctx.print("item as <" + type + ">." + name) else: ctx.print(constant + "." + name) return @@ -318,6 +342,9 @@ def decompile_lookup( def decompile_constant( ctx: DecompileCtx, gir: gir.GirContext, cdata: str, type: T.Optional[str] = None ): + if ctx.parent_node is not None and ctx.parent_node.tag == "property": + ctx.print("expr ") + if type is None: if cdata == ctx.template_class: ctx.print("template") @@ -330,6 +357,9 @@ def decompile_constant( @decompiler("closure", skip_children=True) def decompile_closure(ctx: DecompileCtx, gir: gir.GirContext, function: str, type: str): + if ctx.parent_node is not None and ctx.parent_node.tag == "property": + ctx.print("expr ") + if t := ctx.type_by_cname(type): type = decompile.full_name(t) else: diff --git a/blueprintcompiler/language/gobject_object.py b/blueprintcompiler/language/gobject_object.py index 54cb297..1def15b 100644 --- a/blueprintcompiler/language/gobject_object.py +++ b/blueprintcompiler/language/gobject_object.py @@ -28,7 +28,18 @@ from .common import * from .response_id import ExtResponse from .types import ClassName, ConcreteClassName -RESERVED_IDS = {"this", "self", "template", "true", "false", "null", "none"} +RESERVED_IDS = { + "this", + "self", + "template", + "true", + "false", + "null", + "none", + "item", + "expr", + "typeof", +} class ObjectContent(AstNode): diff --git a/blueprintcompiler/language/gobject_property.py b/blueprintcompiler/language/gobject_property.py index b553909..50a7512 100644 --- a/blueprintcompiler/language/gobject_property.py +++ b/blueprintcompiler/language/gobject_property.py @@ -21,13 +21,12 @@ from .binding import Binding from .common import * from .contexts import ValueTypeCtx -from .gtkbuilder_template import Template -from .values import ArrayValue, ObjectValue, Value +from .values import ArrayValue, ExprValue, ObjectValue, Value class Property(AstNode): grammar = Statement( - UseIdent("name"), ":", AnyOf(Binding, ObjectValue, Value, ArrayValue) + UseIdent("name"), ":", AnyOf(Binding, ExprValue, ObjectValue, Value, ArrayValue) ) @property @@ -35,7 +34,7 @@ class Property(AstNode): return self.tokens["name"] @property - def value(self) -> T.Union[Binding, ObjectValue, Value, ArrayValue]: + def value(self) -> T.Union[Binding, ExprValue, ObjectValue, Value, ArrayValue]: return self.children[0] @property diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index 63cf4fc..6d60724 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -23,7 +23,8 @@ from blueprintcompiler.gir import ArrayType from blueprintcompiler.lsp_utils import SemanticToken from .common import * -from .contexts import ScopeCtx, ValueTypeCtx +from .contexts import ExprValueCtx, ScopeCtx, ValueTypeCtx +from .expression import Expression from .gobject_object import Object from .types import TypeName @@ -319,7 +320,12 @@ class IdentLiteral(AstNode): if self.ident == "null": if not self.context[ValueTypeCtx].allow_null: raise CompileError("null is not permitted here") - else: + elif self.ident == "item": + if not self.context[ExprValueCtx]: + raise CompileError( + '"item" can only be used in an expression literal' + ) + elif self.ident not in ["true", "false"]: raise CompileError( f"Could not find object with ID {self.ident}", did_you_mean=( @@ -407,6 +413,35 @@ class ObjectValue(AstNode): ) +class ExprValue(AstNode): + grammar = [Keyword("expr"), Expression] + + @property + def expression(self) -> Expression: + return self.children[Expression][0] + + @validate("expr") + def validate_for_type(self) -> None: + expected_type = self.parent.context[ValueTypeCtx].value_type + expr_type = self.root.gir.get_type("Expression", "Gtk") + if expected_type is not None and not expected_type.assignable_to(expr_type): + raise CompileError( + f"Cannot convert Gtk.Expression to {expected_type.full_name}" + ) + + @docs("expr") + def ref_docs(self): + return get_docs_section("Syntax ExprValue") + + @context(ExprValueCtx) + def expr_literal(self): + return ExprValueCtx() + + @context(ValueTypeCtx) + def value_type(self): + return ValueTypeCtx(None, must_infer_type=True) + + class Value(AstNode): grammar = AnyOf(Translated, Flags, Literal) diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index a21b6fb..5c03761 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -134,6 +134,11 @@ class XmlOutput(OutputFormat): self._emit_expression(value.expression, xml) xml.end_tag() + elif isinstance(value, ExprValue): + xml.start_tag("property", **props) + self._emit_expression(value.expression, xml) + xml.end_tag() + elif isinstance(value, ObjectValue): xml.start_tag("property", **props) self._emit_object(value.object, xml) @@ -218,12 +223,6 @@ class XmlOutput(OutputFormat): xml.put_text( "|".join([str(flag.value or flag.name) for flag in value.child.flags]) ) - elif isinstance(value.child, Translated): - raise CompilerBugError("translated values must be handled in the parent") - elif isinstance(value.child, TypeLiteral): - xml.put_text(value.child.type_name.glib_type_name) - elif isinstance(value.child, ObjectValue): - self._emit_object(value.child.object, xml) else: raise CompilerBugError() @@ -245,6 +244,9 @@ class XmlOutput(OutputFormat): raise CompilerBugError() def _emit_literal_expr(self, expr: LiteralExpr, xml: XmlEmitter): + if expr.is_this: + return + if expr.is_object: xml.start_tag("constant") else: diff --git a/docs/reference/expressions.rst b/docs/reference/expressions.rst index 8688ff0..3d523d1 100644 --- a/docs/reference/expressions.rst +++ b/docs/reference/expressions.rst @@ -42,8 +42,8 @@ Expressions are composed of property lookups and/or closures. Property lookups a .. _Syntax LookupExpression: -Lookup Expressions ------------------- +Lookups +------- .. rst-class:: grammar-block @@ -56,8 +56,8 @@ The type of a property expression is the type of the property it refers to. .. _Syntax ClosureExpression: -Closure Expressions -------------------- +Closures +-------- .. rst-class:: grammar-block @@ -72,8 +72,8 @@ Blueprint doesn't know the closure's return type, so closure expressions must be .. _Syntax CastExpression: -Cast Expressions ----------------- +Casts +----- .. rst-class:: grammar-block @@ -81,7 +81,32 @@ Cast Expressions Cast expressions allow Blueprint to know the type of an expression when it can't otherwise determine it. This is necessary for closures and for properties of application-defined types. +Example +~~~~~~~ + .. code-block:: blueprint // Cast the result of the closure so blueprint knows it's a string - label: bind $my_closure() as \ No newline at end of file + label: bind $format_bytes(template.file-size) as + +.. _Syntax ExprValue: + +Expression Values +----------------- + +.. rst-class:: grammar-block + + ExprValue = 'expr' :ref:`Expression` + +Some APIs take *an expression itself*--not its result--as a property value. For example, `Gtk.BoolFilter `_ has an ``expression`` property of type `Gtk.Expression `_. This expression is evaluated for every item in a list model to determine whether the item should be filtered. + +To define an expression for such a property, use ``expr`` instead of ``bind``. Inside the expression, you can use the ``item`` keyword to refer to the item being evaluated. You must cast the item to the correct type using the ``as`` keyword, and you can only use ``item`` in a property lookup--you may not pass it to a closure. + +Example +~~~~~~~ + +.. code-block:: blueprint + + BoolFilter { + expression: expr item as <$UserAccount>.active; + } diff --git a/docs/reference/objects.rst b/docs/reference/objects.rst index 09f5af8..6f76da6 100644 --- a/docs/reference/objects.rst +++ b/docs/reference/objects.rst @@ -58,7 +58,7 @@ Properties .. rst-class:: grammar-block - Property = `> ':' ( :ref:`Binding` | :ref:`ObjectValue` | :ref:`Value` ) ';' + Property = `> ':' ( :ref:`Binding` | :ref:`ExprValue` | :ref:`ObjectValue` | :ref:`Value` ) ';' Properties specify the details of each object, like a label's text, an image's icon name, or the margins on a container. diff --git a/tests/sample_errors/expr_item_not_cast.blp b/tests/sample_errors/expr_item_not_cast.blp new file mode 100644 index 0000000..76a1d89 --- /dev/null +++ b/tests/sample_errors/expr_item_not_cast.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +BoolFilter { + expression: expr item.visible; +} diff --git a/tests/sample_errors/expr_item_not_cast.err b/tests/sample_errors/expr_item_not_cast.err new file mode 100644 index 0000000..f6cf7d4 --- /dev/null +++ b/tests/sample_errors/expr_item_not_cast.err @@ -0,0 +1 @@ +4,20,4,"item" must be cast to its object type \ No newline at end of file diff --git a/tests/sample_errors/expr_value_assignment.blp b/tests/sample_errors/expr_value_assignment.blp new file mode 100644 index 0000000..51d778f --- /dev/null +++ b/tests/sample_errors/expr_value_assignment.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label { + label: expr 1; +} diff --git a/tests/sample_errors/expr_value_assignment.err b/tests/sample_errors/expr_value_assignment.err new file mode 100644 index 0000000..1c7092a --- /dev/null +++ b/tests/sample_errors/expr_value_assignment.err @@ -0,0 +1 @@ +4,10,4,Cannot convert Gtk.Expression to string \ No newline at end of file diff --git a/tests/sample_errors/expr_value_closure_arg.blp b/tests/sample_errors/expr_value_closure_arg.blp new file mode 100644 index 0000000..7f828c4 --- /dev/null +++ b/tests/sample_errors/expr_value_closure_arg.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +BoolFilter { + expression: expr $closure(item as ) as ; +} diff --git a/tests/sample_errors/expr_value_closure_arg.err b/tests/sample_errors/expr_value_closure_arg.err new file mode 100644 index 0000000..b9e19f8 --- /dev/null +++ b/tests/sample_errors/expr_value_closure_arg.err @@ -0,0 +1 @@ +4,29,4,"item" can only be used for looking up properties \ No newline at end of file diff --git a/tests/sample_errors/expr_value_item.blp b/tests/sample_errors/expr_value_item.blp new file mode 100644 index 0000000..141c806 --- /dev/null +++ b/tests/sample_errors/expr_value_item.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +BoolFilter { + expression: expr item as