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 .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]

View file

@ -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")

View file

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

View file

@ -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)),
]

View file

@ -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

View file

@ -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(

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("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")