mirror of
https://gitlab.gnome.org/jwestman/blueprint-compiler.git
synced 2025-05-04 15:59:08 -04:00
Improve expression type checking
This commit is contained in:
parent
b636d9ed71
commit
98ba7d467a
11 changed files with 160 additions and 55 deletions
|
@ -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]
|
||||||
|
|
|
@ -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,9 +37,13 @@ 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):
|
||||||
|
from .values import IdentLiteral
|
||||||
|
|
||||||
|
if isinstance(self.expression.last.lhs.literal.value, IdentLiteral):
|
||||||
return SimpleBinding(
|
return SimpleBinding(
|
||||||
self.expression.last.lhs.ident, self.expression.last.property_name
|
self.expression.last.lhs.literal.value.ident,
|
||||||
|
self.expression.last.property_name,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -44,3 +44,4 @@ from ..parse_tree import *
|
||||||
|
|
||||||
|
|
||||||
OBJECT_CONTENT_HOOKS = AnyOf()
|
OBJECT_CONTENT_HOOKS = AnyOf()
|
||||||
|
LITERAL = AnyOf()
|
||||||
|
|
|
@ -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:
|
||||||
|
from .values import IdentLiteral
|
||||||
|
|
||||||
|
if isinstance(self.literal, IdentLiteral):
|
||||||
if object := self.root.objects_by_id.get(self.ident):
|
if object := self.root.objects_by_id.get(self.ident):
|
||||||
return not isinstance(object, Template)
|
return not isinstance(object, Template)
|
||||||
else:
|
|
||||||
return True
|
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)),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -159,11 +159,10 @@ class XmlOutput(OutputFormat):
|
||||||
self._emit_object(child.object, xml)
|
self._emit_object(child.object, xml)
|
||||||
xml.end_tag()
|
xml.end_tag()
|
||||||
|
|
||||||
def _emit_value(self, value: Value, xml: XmlEmitter):
|
def _emit_literal(self, literal: Literal, xml: XmlEmitter):
|
||||||
if isinstance(value.child, Literal):
|
literal = literal.value
|
||||||
literal = value.child.value
|
|
||||||
if isinstance(literal, IdentLiteral):
|
if isinstance(literal, IdentLiteral):
|
||||||
value_type = value.context[ValueTypeCtx].value_type
|
value_type = literal.context[ValueTypeCtx].value_type
|
||||||
if isinstance(value_type, gir.BoolType):
|
if isinstance(value_type, gir.BoolType):
|
||||||
xml.put_text(literal.ident)
|
xml.put_text(literal.ident)
|
||||||
elif isinstance(value_type, gir.Enumeration):
|
elif isinstance(value_type, gir.Enumeration):
|
||||||
|
@ -174,6 +173,10 @@ class XmlOutput(OutputFormat):
|
||||||
xml.put_text(literal.type_name.glib_type_name)
|
xml.put_text(literal.type_name.glib_type_name)
|
||||||
else:
|
else:
|
||||||
xml.put_text(literal.value)
|
xml.put_text(literal.value)
|
||||||
|
|
||||||
|
def _emit_value(self, value: Value, xml: XmlEmitter):
|
||||||
|
if isinstance(value.child, Literal):
|
||||||
|
self._emit_literal(value.child, xml)
|
||||||
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):
|
||||||
|
if expr.is_object:
|
||||||
xml.start_tag("constant")
|
xml.start_tag("constant")
|
||||||
xml.put_text(expr.ident)
|
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(
|
||||||
|
|
7
tests/sample_errors/expr_cast_needed.blp
Normal file
7
tests/sample_errors/expr_cast_needed.blp
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
using Gtk 4.0;
|
||||||
|
|
||||||
|
template GtkListItem {
|
||||||
|
Label {
|
||||||
|
label: bind GtkListItem.item.label;
|
||||||
|
}
|
||||||
|
}
|
1
tests/sample_errors/expr_cast_needed.err
Normal file
1
tests/sample_errors/expr_cast_needed.err
Normal file
|
@ -0,0 +1 @@
|
||||||
|
5,34,5,Could not determine the type of the preceding expression
|
5
tests/samples/expr_closure_args.blp
Normal file
5
tests/samples/expr_closure_args.blp
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
using Gtk 4.0;
|
||||||
|
|
||||||
|
Label {
|
||||||
|
label: bind $my-closure (true, 10, "Hello") as (string);
|
||||||
|
}
|
13
tests/samples/expr_closure_args.ui
Normal file
13
tests/samples/expr_closure_args.ui
Normal 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>
|
|
@ -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")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue