diff --git a/blueprintcompiler/language/__init__.py b/blueprintcompiler/language/__init__.py index 822a91a..8f68647 100644 --- a/blueprintcompiler/language/__init__.py +++ b/blueprintcompiler/language/__init__.py @@ -1,7 +1,15 @@ from .attributes import BaseAttribute, BaseTypedAttribute from .binding import Binding from .contexts import ValueTypeCtx -from .expression import CastExpr, ClosureExpr, Expr, ExprChain, IdentExpr, LookupOp +from .expression import ( + CastExpr, + ClosureArg, + ClosureExpr, + Expr, + ExprChain, + LiteralExpr, + LookupOp, +) from .gobject_object import Object, ObjectContent from .gobject_property import Property from .gobject_signal import Signal @@ -50,3 +58,5 @@ OBJECT_CONTENT_HOOKS.children = [ Strings, Child, ] + +LITERAL.children = [Literal] diff --git a/blueprintcompiler/language/binding.py b/blueprintcompiler/language/binding.py index 91d5a6b..ab544ca 100644 --- a/blueprintcompiler/language/binding.py +++ b/blueprintcompiler/language/binding.py @@ -20,7 +20,7 @@ from dataclasses import dataclass from .common import * -from .expression import ExprChain, LookupOp, IdentExpr +from .expression import ExprChain, LookupOp, LiteralExpr from .contexts import ValueTypeCtx @@ -37,10 +37,14 @@ class Binding(AstNode): @property def simple_binding(self) -> T.Optional["SimpleBinding"]: if isinstance(self.expression.last, LookupOp): - if isinstance(self.expression.last.lhs, IdentExpr): - return SimpleBinding( - self.expression.last.lhs.ident, self.expression.last.property_name - ) + if isinstance(self.expression.last.lhs, LiteralExpr): + from .values import IdentLiteral + + if isinstance(self.expression.last.lhs.literal.value, IdentLiteral): + return SimpleBinding( + self.expression.last.lhs.literal.value.ident, + self.expression.last.property_name, + ) return None @validate("bind") diff --git a/blueprintcompiler/language/common.py b/blueprintcompiler/language/common.py index 636f15d..734e59b 100644 --- a/blueprintcompiler/language/common.py +++ b/blueprintcompiler/language/common.py @@ -44,3 +44,4 @@ from ..parse_tree import * OBJECT_CONTENT_HOOKS = AnyOf() +LITERAL = AnyOf() diff --git a/blueprintcompiler/language/expression.py b/blueprintcompiler/language/expression.py index 82787ef..5347a88 100644 --- a/blueprintcompiler/language/expression.py +++ b/blueprintcompiler/language/expression.py @@ -19,6 +19,7 @@ from .common import * +from .contexts import ValueTypeCtx from .types import TypeName from .gtkbuilder_template import Template @@ -27,6 +28,13 @@ expr = Sequence() class Expr(AstNode): + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + if rhs := self.rhs: + return rhs.context[ValueTypeCtx] + else: + return self.parent.context[ValueTypeCtx] + @property def type(self) -> T.Optional[GirType]: raise NotImplementedError() @@ -70,39 +78,45 @@ class InfixExpr(Expr): return children[children.index(self) - 1] -class IdentExpr(Expr): - grammar = UseIdent("ident") +class LiteralExpr(Expr): + grammar = LITERAL @property - def ident(self) -> str: - return self.tokens["ident"] + def is_object(self) -> bool: + from .values import IdentLiteral - @validate() - def exists(self): - if self.root.objects_by_id.get(self.ident) is None: - raise CompileError( - f"Could not find object with ID {self.ident}", - did_you_mean=(self.ident, self.root.objects_by_id.keys()), - ) + return ( + isinstance(self.literal.value, IdentLiteral) + and self.literal.value.ident in self.root.objects_by_id + ) + + @property + def literal(self): + from .values import Literal + + return self.children[Literal][0] @property def type(self) -> T.Optional[GirType]: - if object := self.root.objects_by_id.get(self.ident): - return object.gir_class - else: - return None + return self.literal.value.type @property def type_complete(self) -> bool: - if object := self.root.objects_by_id.get(self.ident): - return not isinstance(object, Template) - else: - return True + from .values import IdentLiteral + + if isinstance(self.literal, IdentLiteral): + if object := self.root.objects_by_id.get(self.ident): + return not isinstance(object, Template) + return True class LookupOp(InfixExpr): grammar = [".", UseIdent("property")] + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(None) + @property def property_name(self) -> str: return self.tokens["property"] @@ -119,11 +133,15 @@ class LookupOp(InfixExpr): @validate("property") def property_exists(self): - if ( - self.lhs.type is None - or not self.lhs.type_complete - or isinstance(self.lhs.type, UncheckedType) - ): + if self.lhs.type is None: + raise CompileError( + f"Could not determine the type of the preceding expression", + hints=[ + f"add a type cast so blueprint knows which type the property {self.property_name} belongs to" + ], + ) + + if isinstance(self.lhs.type, UncheckedType): return elif not isinstance(self.lhs.type, gir.Class) and not isinstance( @@ -143,6 +161,10 @@ class LookupOp(InfixExpr): class CastExpr(InfixExpr): grammar = ["as", "(", TypeName, ")"] + @context(ValueTypeCtx) + def value_type(self): + return ValueTypeCtx(self.type) + @property def type(self) -> T.Optional[GirType]: return self.children[TypeName][0].gir_type @@ -162,12 +184,24 @@ class CastExpr(InfixExpr): ) +class ClosureArg(AstNode): + grammar = ExprChain + + @property + def expr(self) -> ExprChain: + return self.children[ExprChain][0] + + @context(ValueTypeCtx) + def value_type(self) -> ValueTypeCtx: + return ValueTypeCtx(None) + + class ClosureExpr(Expr): grammar = [ Optional(["$", UseLiteral("extern", True)]), UseIdent("name"), "(", - Delimited(ExprChain, ","), + Delimited(ClosureArg, ","), ")", ] @@ -183,8 +217,8 @@ class ClosureExpr(Expr): return self.tokens["name"] @property - def args(self) -> T.List[ExprChain]: - return self.children[ExprChain] + def args(self) -> T.List[ClosureArg]: + return self.children[ClosureArg] @validate() def cast_to_return_type(self): @@ -200,6 +234,6 @@ class ClosureExpr(Expr): expr.children = [ - AnyOf(ClosureExpr, IdentExpr, ["(", ExprChain, ")"]), + AnyOf(ClosureExpr, LiteralExpr, ["(", ExprChain, ")"]), ZeroOrMore(AnyOf(LookupOp, CastExpr)), ] diff --git a/blueprintcompiler/language/values.py b/blueprintcompiler/language/values.py index ca10f50..83de28e 100644 --- a/blueprintcompiler/language/values.py +++ b/blueprintcompiler/language/values.py @@ -79,6 +79,10 @@ class TypeLiteral(AstNode): Match(")").expected(), ] + @property + def type(self): + return gir.TypeType() + @property def type_name(self) -> TypeName: return self.children[TypeName][0] @@ -97,6 +101,10 @@ class QuotedLiteral(AstNode): def value(self) -> str: return self.tokens["value"] + @property + def type(self): + return gir.StringType() + @validate() def validate_for_type(self) -> None: expected_type = self.context[ValueTypeCtx].value_type @@ -171,10 +179,10 @@ class NumberLiteral(AstNode): elif isinstance(expected_type, gir.UIntType): if self.value < 0: raise CompileError( - f"Cannot convert {self.group.tokens['value']} to unsigned integer" + f"Cannot convert -{self.group.tokens['value']} to unsigned integer" ) - elif expected_type is not None: + elif not isinstance(expected_type, gir.FloatType) and expected_type is not None: raise CompileError(f"Cannot convert number to {expected_type.full_name}") @@ -237,6 +245,18 @@ class IdentLiteral(AstNode): def ident(self) -> str: return self.tokens["value"] + @property + def type(self) -> T.Optional[gir.GirType]: + # If the expected type is known, then use that. Otherwise, guess. + if expected_type := self.context[ValueTypeCtx].value_type: + return expected_type + elif self.ident in ["true", "false"]: + return gir.BoolType() + elif object := self.root.objects_by_id.get(self.ident): + return object.gir_class + else: + return None + @validate() def validate_for_type(self) -> None: expected_type = self.context[ValueTypeCtx].value_type diff --git a/blueprintcompiler/outputs/xml/__init__.py b/blueprintcompiler/outputs/xml/__init__.py index c38c070..f379710 100644 --- a/blueprintcompiler/outputs/xml/__init__.py +++ b/blueprintcompiler/outputs/xml/__init__.py @@ -159,21 +159,24 @@ class XmlOutput(OutputFormat): self._emit_object(child.object, xml) xml.end_tag() + def _emit_literal(self, literal: Literal, xml: XmlEmitter): + literal = literal.value + if isinstance(literal, IdentLiteral): + value_type = literal.context[ValueTypeCtx].value_type + if isinstance(value_type, gir.BoolType): + xml.put_text(literal.ident) + elif isinstance(value_type, gir.Enumeration): + xml.put_text(str(value_type.members[literal.ident].value)) + else: + xml.put_text(literal.ident) + elif isinstance(literal, TypeLiteral): + xml.put_text(literal.type_name.glib_type_name) + else: + xml.put_text(literal.value) + def _emit_value(self, value: Value, xml: XmlEmitter): if isinstance(value.child, Literal): - literal = value.child.value - if isinstance(literal, IdentLiteral): - value_type = value.context[ValueTypeCtx].value_type - if isinstance(value_type, gir.BoolType): - xml.put_text(literal.ident) - elif isinstance(value_type, gir.Enumeration): - xml.put_text(str(value_type.members[literal.ident].value)) - else: - xml.put_text(literal.ident) - elif isinstance(literal, TypeLiteral): - xml.put_text(literal.type_name.glib_type_name) - else: - xml.put_text(literal.value) + self._emit_literal(value.child, xml) elif isinstance(value.child, Flags): xml.put_text( "|".join([str(flag.value or flag.name) for flag in value.child.flags]) @@ -191,8 +194,8 @@ class XmlOutput(OutputFormat): self._emit_expression_part(expression.last, xml) def _emit_expression_part(self, expression: Expr, xml: XmlEmitter): - if isinstance(expression, IdentExpr): - self._emit_ident_expr(expression, xml) + if isinstance(expression, LiteralExpr): + self._emit_literal_expr(expression, xml) elif isinstance(expression, LookupOp): self._emit_lookup_op(expression, xml) elif isinstance(expression, ExprChain): @@ -204,9 +207,12 @@ class XmlOutput(OutputFormat): else: raise CompilerBugError() - def _emit_ident_expr(self, expr: IdentExpr, xml: XmlEmitter): - xml.start_tag("constant") - xml.put_text(expr.ident) + def _emit_literal_expr(self, expr: LiteralExpr, xml: XmlEmitter): + if expr.is_object: + xml.start_tag("constant") + else: + xml.start_tag("constant", type=expr.type) + self._emit_literal(expr.literal, xml) xml.end_tag() def _emit_lookup_op(self, expr: LookupOp, xml: XmlEmitter): @@ -220,7 +226,7 @@ class XmlOutput(OutputFormat): 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) + self._emit_expression_part(arg.expr, xml) xml.end_tag() def _emit_attribute( diff --git a/tests/sample_errors/expr_cast_needed.blp b/tests/sample_errors/expr_cast_needed.blp new file mode 100644 index 0000000..f647cb5 --- /dev/null +++ b/tests/sample_errors/expr_cast_needed.blp @@ -0,0 +1,7 @@ +using Gtk 4.0; + +template GtkListItem { + Label { + label: bind GtkListItem.item.label; + } +} \ No newline at end of file diff --git a/tests/sample_errors/expr_cast_needed.err b/tests/sample_errors/expr_cast_needed.err new file mode 100644 index 0000000..51269d2 --- /dev/null +++ b/tests/sample_errors/expr_cast_needed.err @@ -0,0 +1 @@ +5,34,5,Could not determine the type of the preceding expression \ No newline at end of file diff --git a/tests/samples/expr_closure_args.blp b/tests/samples/expr_closure_args.blp new file mode 100644 index 0000000..d09c881 --- /dev/null +++ b/tests/samples/expr_closure_args.blp @@ -0,0 +1,5 @@ +using Gtk 4.0; + +Label { + label: bind $my-closure (true, 10, "Hello") as (string); +} \ No newline at end of file diff --git a/tests/samples/expr_closure_args.ui b/tests/samples/expr_closure_args.ui new file mode 100644 index 0000000..1b539ac --- /dev/null +++ b/tests/samples/expr_closure_args.ui @@ -0,0 +1,13 @@ + + + + + + + true + 10 + Hello + + + + diff --git a/tests/test_samples.py b/tests/test_samples.py index 7bc7d28..29d2f14 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -150,6 +150,9 @@ class TestSamples(unittest.TestCase): 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_closure_args", skip_run=True + ) # The closure doesn't exist self.assert_sample("expr_lookup") self.assert_sample("file_filter") self.assert_sample("flags") @@ -208,6 +211,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_cast_needed") self.assert_sample_error("expr_closure_not_cast") self.assert_sample_error("expr_lookup_dne") self.assert_sample_error("expr_lookup_no_properties")