Improve expression type checking

This commit is contained in:
James Westman 2023-03-12 15:35:05 -05:00
parent b636d9ed71
commit 98ba7d467a
11 changed files with 160 additions and 55 deletions

View file

@ -1,7 +1,15 @@
from .attributes import BaseAttribute, BaseTypedAttribute from .attributes import BaseAttribute, BaseTypedAttribute
from .binding import Binding from .binding import Binding
from .contexts import ValueTypeCtx 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_object import Object, ObjectContent
from .gobject_property import Property from .gobject_property import Property
from .gobject_signal import Signal from .gobject_signal import Signal
@ -50,3 +58,5 @@ OBJECT_CONTENT_HOOKS.children = [
Strings, Strings,
Child, Child,
] ]
LITERAL.children = [Literal]

View file

@ -20,7 +20,7 @@
from dataclasses import dataclass from dataclasses import dataclass
from .common import * from .common import *
from .expression import ExprChain, LookupOp, IdentExpr from .expression import ExprChain, LookupOp, LiteralExpr
from .contexts import ValueTypeCtx from .contexts import ValueTypeCtx
@ -37,10 +37,14 @@ class Binding(AstNode):
@property @property
def simple_binding(self) -> T.Optional["SimpleBinding"]: def simple_binding(self) -> T.Optional["SimpleBinding"]:
if isinstance(self.expression.last, LookupOp): if isinstance(self.expression.last, LookupOp):
if isinstance(self.expression.last.lhs, IdentExpr): if isinstance(self.expression.last.lhs, LiteralExpr):
return SimpleBinding( from .values import IdentLiteral
self.expression.last.lhs.ident, self.expression.last.property_name
) 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 return None
@validate("bind") @validate("bind")

View file

@ -44,3 +44,4 @@ from ..parse_tree import *
OBJECT_CONTENT_HOOKS = AnyOf() OBJECT_CONTENT_HOOKS = AnyOf()
LITERAL = AnyOf()

View file

@ -19,6 +19,7 @@
from .common import * from .common import *
from .contexts import ValueTypeCtx
from .types import TypeName from .types import TypeName
from .gtkbuilder_template import Template from .gtkbuilder_template import Template
@ -27,6 +28,13 @@ expr = Sequence()
class Expr(AstNode): 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 @property
def type(self) -> T.Optional[GirType]: def type(self) -> T.Optional[GirType]:
raise NotImplementedError() raise NotImplementedError()
@ -70,39 +78,45 @@ class InfixExpr(Expr):
return children[children.index(self) - 1] return children[children.index(self) - 1]
class IdentExpr(Expr): class LiteralExpr(Expr):
grammar = UseIdent("ident") grammar = LITERAL
@property @property
def ident(self) -> str: def is_object(self) -> bool:
return self.tokens["ident"] from .values import IdentLiteral
@validate() return (
def exists(self): isinstance(self.literal.value, IdentLiteral)
if self.root.objects_by_id.get(self.ident) is None: and self.literal.value.ident in self.root.objects_by_id
raise CompileError( )
f"Could not find object with ID {self.ident}",
did_you_mean=(self.ident, self.root.objects_by_id.keys()), @property
) def literal(self):
from .values import Literal
return self.children[Literal][0]
@property @property
def type(self) -> T.Optional[GirType]: def type(self) -> T.Optional[GirType]:
if object := self.root.objects_by_id.get(self.ident): return self.literal.value.type
return object.gir_class
else:
return None
@property @property
def type_complete(self) -> bool: def type_complete(self) -> bool:
if object := self.root.objects_by_id.get(self.ident): from .values import IdentLiteral
return not isinstance(object, Template)
else: if isinstance(self.literal, IdentLiteral):
return True if object := self.root.objects_by_id.get(self.ident):
return not isinstance(object, Template)
return True
class LookupOp(InfixExpr): class LookupOp(InfixExpr):
grammar = [".", UseIdent("property")] grammar = [".", UseIdent("property")]
@context(ValueTypeCtx)
def value_type(self) -> ValueTypeCtx:
return ValueTypeCtx(None)
@property @property
def property_name(self) -> str: def property_name(self) -> str:
return self.tokens["property"] return self.tokens["property"]
@ -119,11 +133,15 @@ class LookupOp(InfixExpr):
@validate("property") @validate("property")
def property_exists(self): def property_exists(self):
if ( if self.lhs.type is None:
self.lhs.type is None raise CompileError(
or not self.lhs.type_complete f"Could not determine the type of the preceding expression",
or isinstance(self.lhs.type, UncheckedType) 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 return
elif not isinstance(self.lhs.type, gir.Class) and not isinstance( elif not isinstance(self.lhs.type, gir.Class) and not isinstance(
@ -143,6 +161,10 @@ class LookupOp(InfixExpr):
class CastExpr(InfixExpr): class CastExpr(InfixExpr):
grammar = ["as", "(", TypeName, ")"] grammar = ["as", "(", TypeName, ")"]
@context(ValueTypeCtx)
def value_type(self):
return ValueTypeCtx(self.type)
@property @property
def type(self) -> T.Optional[GirType]: def type(self) -> T.Optional[GirType]:
return self.children[TypeName][0].gir_type 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): class ClosureExpr(Expr):
grammar = [ grammar = [
Optional(["$", UseLiteral("extern", True)]), Optional(["$", UseLiteral("extern", True)]),
UseIdent("name"), UseIdent("name"),
"(", "(",
Delimited(ExprChain, ","), Delimited(ClosureArg, ","),
")", ")",
] ]
@ -183,8 +217,8 @@ class ClosureExpr(Expr):
return self.tokens["name"] return self.tokens["name"]
@property @property
def args(self) -> T.List[ExprChain]: def args(self) -> T.List[ClosureArg]:
return self.children[ExprChain] return self.children[ClosureArg]
@validate() @validate()
def cast_to_return_type(self): def cast_to_return_type(self):
@ -200,6 +234,6 @@ class ClosureExpr(Expr):
expr.children = [ expr.children = [
AnyOf(ClosureExpr, IdentExpr, ["(", ExprChain, ")"]), AnyOf(ClosureExpr, LiteralExpr, ["(", ExprChain, ")"]),
ZeroOrMore(AnyOf(LookupOp, CastExpr)), ZeroOrMore(AnyOf(LookupOp, CastExpr)),
] ]

View file

@ -79,6 +79,10 @@ class TypeLiteral(AstNode):
Match(")").expected(), Match(")").expected(),
] ]
@property
def type(self):
return gir.TypeType()
@property @property
def type_name(self) -> TypeName: def type_name(self) -> TypeName:
return self.children[TypeName][0] return self.children[TypeName][0]
@ -97,6 +101,10 @@ class QuotedLiteral(AstNode):
def value(self) -> str: def value(self) -> str:
return self.tokens["value"] return self.tokens["value"]
@property
def type(self):
return gir.StringType()
@validate() @validate()
def validate_for_type(self) -> None: def validate_for_type(self) -> None:
expected_type = self.context[ValueTypeCtx].value_type expected_type = self.context[ValueTypeCtx].value_type
@ -171,10 +179,10 @@ class NumberLiteral(AstNode):
elif isinstance(expected_type, gir.UIntType): elif isinstance(expected_type, gir.UIntType):
if self.value < 0: if self.value < 0:
raise CompileError( 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}") raise CompileError(f"Cannot convert number to {expected_type.full_name}")
@ -237,6 +245,18 @@ class IdentLiteral(AstNode):
def ident(self) -> str: def ident(self) -> str:
return self.tokens["value"] 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() @validate()
def validate_for_type(self) -> None: def validate_for_type(self) -> None:
expected_type = self.context[ValueTypeCtx].value_type expected_type = self.context[ValueTypeCtx].value_type

View file

@ -159,21 +159,24 @@ class XmlOutput(OutputFormat):
self._emit_object(child.object, xml) self._emit_object(child.object, xml)
xml.end_tag() 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): def _emit_value(self, value: Value, xml: XmlEmitter):
if isinstance(value.child, Literal): if isinstance(value.child, Literal):
literal = value.child.value self._emit_literal(value.child, xml)
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)
elif isinstance(value.child, Flags): elif isinstance(value.child, Flags):
xml.put_text( xml.put_text(
"|".join([str(flag.value or flag.name) for flag in value.child.flags]) "|".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) self._emit_expression_part(expression.last, xml)
def _emit_expression_part(self, expression: Expr, xml: XmlEmitter): def _emit_expression_part(self, expression: Expr, xml: XmlEmitter):
if isinstance(expression, IdentExpr): if isinstance(expression, LiteralExpr):
self._emit_ident_expr(expression, xml) self._emit_literal_expr(expression, xml)
elif isinstance(expression, LookupOp): elif isinstance(expression, LookupOp):
self._emit_lookup_op(expression, xml) self._emit_lookup_op(expression, xml)
elif isinstance(expression, ExprChain): elif isinstance(expression, ExprChain):
@ -204,9 +207,12 @@ class XmlOutput(OutputFormat):
else: else:
raise CompilerBugError() raise CompilerBugError()
def _emit_ident_expr(self, expr: IdentExpr, xml: XmlEmitter): def _emit_literal_expr(self, expr: LiteralExpr, xml: XmlEmitter):
xml.start_tag("constant") if expr.is_object:
xml.put_text(expr.ident) xml.start_tag("constant")
else:
xml.start_tag("constant", type=expr.type)
self._emit_literal(expr.literal, xml)
xml.end_tag() xml.end_tag()
def _emit_lookup_op(self, expr: LookupOp, xml: XmlEmitter): 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): def _emit_closure_expr(self, expr: ClosureExpr, xml: XmlEmitter):
xml.start_tag("closure", function=expr.closure_name, type=expr.type) xml.start_tag("closure", function=expr.closure_name, type=expr.type)
for arg in expr.args: for arg in expr.args:
self._emit_expression_part(arg, xml) self._emit_expression_part(arg.expr, xml)
xml.end_tag() xml.end_tag()
def _emit_attribute( def _emit_attribute(

View file

@ -0,0 +1,7 @@
using Gtk 4.0;
template GtkListItem {
Label {
label: bind GtkListItem.item.label;
}
}

View file

@ -0,0 +1 @@
5,34,5,Could not determine the type of the preceding expression

View file

@ -0,0 +1,5 @@
using Gtk 4.0;
Label {
label: bind $my-closure (true, 10, "Hello") as (string);
}

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<object class="GtkLabel">
<binding name="label">
<closure function="my-closure" type="gchararray">
<constant type="gboolean">true</constant>
<constant type="gint">10</constant>
<constant type="gchararray">Hello</constant>
</closure>
</binding>
</object>
</interface>

View file

@ -150,6 +150,9 @@ class TestSamples(unittest.TestCase):
self.assert_sample("comments") self.assert_sample("comments")
self.assert_sample("enum") self.assert_sample("enum")
self.assert_sample("expr_closure", skip_run=True) # The closure doesn't exist 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("expr_lookup")
self.assert_sample("file_filter") self.assert_sample("file_filter")
self.assert_sample("flags") self.assert_sample("flags")
@ -208,6 +211,7 @@ class TestSamples(unittest.TestCase):
self.assert_sample_error("empty") self.assert_sample_error("empty")
self.assert_sample_error("enum_member_dne") self.assert_sample_error("enum_member_dne")
self.assert_sample_error("expr_cast_conversion") 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_closure_not_cast")
self.assert_sample_error("expr_lookup_dne") self.assert_sample_error("expr_lookup_dne")
self.assert_sample_error("expr_lookup_no_properties") self.assert_sample_error("expr_lookup_no_properties")