Add lambda expressions

This commit is contained in:
James Westman 2022-01-29 22:37:00 -06:00
parent 012bdf6843
commit fc497ac9e6
No known key found for this signature in database
GPG key ID: CE2DBA0ADB654EA6
18 changed files with 247 additions and 80 deletions

View file

@ -137,10 +137,10 @@ def prop_value_completer(ast_node, match_variables):
def signal_completer(ast_node, match_variables): def signal_completer(ast_node, match_variables):
if ast_node.gir_class: if ast_node.gir_class:
for signal in ast_node.gir_class.signals: for signal in ast_node.gir_class.signals:
if not isinstance(ast_node.parent, language.Object): if isinstance(ast_node.parent, language.Object):
name = "on" name = "on_" + (ast_node.parent.tokens["id"] or ast_node.parent.children[language.types.ClassName][0].tokens["class_name"].lower())
else: else:
name = "on_" + (ast_node.parent.tokens["id"] or ast_node.parent.tokens["class_name"].lower()) name = "on"
yield Completion(signal, CompletionItemKind.Property, snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;") yield Completion(signal, CompletionItemKind.Property, snippet=f"{signal} => ${{1:{name}_{signal.replace('-', '_')}}}()$0;")

View file

@ -487,7 +487,7 @@ class GirContext:
) )
def validate_class(self, name: str, ns: str): def validate_type(self, name: str, ns: str):
""" Raises an exception if there is a problem looking up the given """ Raises an exception if there is a problem looking up the given
class (it doesn't exist, it isn't a class, etc.) """ class (it doesn't exist, it isn't a class, etc.) """
@ -498,12 +498,7 @@ class GirContext:
if type is None: if type is None:
raise CompileError( raise CompileError(
f"Namespace {ns} does not contain a class called {name}", f"Namespace {ns} does not contain a type called {name}",
did_you_mean=(name, self.namespaces[ns].classes.keys()),
)
elif not isinstance(type, Class):
raise CompileError(
f"{ns}.{name} is not a class",
did_you_mean=(name, self.namespaces[ns].classes.keys()), did_you_mean=(name, self.namespaces[ns].classes.keys()),
) )

View file

@ -16,6 +16,7 @@ from .gtk_string_list import Strings
from .gtk_styles import Styles from .gtk_styles import Styles
from .gtkbuilder_child import Child from .gtkbuilder_child import Child
from .gtkbuilder_template import Template from .gtkbuilder_template import Template
from .lambdas import Lambda
from .imports import GtkDirective, Import from .imports import GtkDirective, Import
from .ui import UI from .ui import UI
from .values import IdentValue, TranslatedStringValue, FlagsValue, LiteralValue from .values import IdentValue, TranslatedStringValue, FlagsValue, LiteralValue
@ -47,4 +48,5 @@ VALUE_HOOKS.children = [
FlagsValue, FlagsValue,
IdentValue, IdentValue,
LiteralValue, LiteralValue,
Lambda,
] ]

View file

@ -17,7 +17,6 @@
# #
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .. import gir from .. import gir
from ..ast_utils import AstNode, validate, docs from ..ast_utils import AstNode, validate, docs
from ..errors import CompileError, MultipleErrors from ..errors import CompileError, MultipleErrors
@ -27,10 +26,29 @@ from ..decompiler import DecompileCtx, decompiler
from ..gir import StringType, BoolType, IntType, FloatType, GirType, Enumeration from ..gir import StringType, BoolType, IntType, FloatType, GirType, Enumeration
from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType from ..lsp_utils import Completion, CompletionItemKind, SemanticToken, SemanticTokenType
from ..parse_tree import * from ..parse_tree import *
from ..parser_utils import *
from ..xml_emitter import XmlEmitter from ..xml_emitter import XmlEmitter
OBJECT_HOOKS = AnyOf() OBJECT_HOOKS = AnyOf()
OBJECT_CONTENT_HOOKS = AnyOf() OBJECT_CONTENT_HOOKS = AnyOf()
VALUE_HOOKS = AnyOf() VALUE_HOOKS = AnyOf()
class Scope:
def get_variables(self) -> T.Iterator[str]:
yield from self.get_objects().keys()
def get_objects(self) -> T.Dict[str, T.Any]:
raise NotImplementedError()
@property
def this_name(self) -> T.Optional[str]:
return None
@property
def this_type(self) -> T.Optional[str]:
return None
@property
def this_type_glib_name(self) -> T.Optional[str]:
return None

View file

@ -41,7 +41,14 @@ class InfixExpr(AstNode):
class IdentExpr(AstNode): class IdentExpr(AstNode):
grammar = UseIdent("ident") grammar = UseIdent("ident")
@property
def is_this(self):
return self.parent_by_type(Scope).this_name == self.tokens["ident"]
def emit_xml(self, xml: XmlEmitter): def emit_xml(self, xml: XmlEmitter):
if self.is_this:
raise CompilerBugError()
xml.start_tag("constant") xml.start_tag("constant")
xml.put_text(self.tokens["ident"]) xml.put_text(self.tokens["ident"])
xml.end_tag() xml.end_tag()
@ -51,11 +58,15 @@ class LookupOp(InfixExpr):
grammar = [".", UseIdent("property")] grammar = [".", UseIdent("property")]
def emit_xml(self, xml: XmlEmitter): def emit_xml(self, xml: XmlEmitter):
if isinstance(self.lhs, IdentExpr) and self.lhs.is_this:
xml.put_self_closing("lookup", name=self.tokens["property"], type=self.parent_by_type(Scope).this_type)
else:
xml.start_tag("lookup", name=self.tokens["property"]) xml.start_tag("lookup", name=self.tokens["property"])
self.lhs.emit_xml(xml) self.lhs.emit_xml(xml)
xml.end_tag() xml.end_tag()
expr.children = [ expr.children = [
Prefix(IdentExpr), Prefix(IdentExpr),
Prefix(["(", Expr, ")"]), Prefix(["(", Expr, ")"]),

View file

@ -21,6 +21,7 @@
import typing as T import typing as T
from functools import cached_property from functools import cached_property
from .types import ConcreteClassName, ClassName
from .common import * from .common import *
from .response_id import ResponseId from .response_id import ResponseId
@ -46,29 +47,11 @@ class ObjectContent(AstNode):
class Object(AstNode): class Object(AstNode):
grammar: T.Any = [ grammar: T.Any = [
class_name, ConcreteClassName,
Optional(UseIdent("id")), Optional(UseIdent("id")),
ObjectContent, ObjectContent,
] ]
@validate("namespace")
def gir_ns_exists(self):
if not self.tokens["ignore_gir"]:
self.root.gir.validate_ns(self.tokens["namespace"])
@validate("class_name")
def gir_class_exists(self):
if self.tokens["class_name"] and not self.tokens["ignore_gir"] and self.gir_ns is not None:
self.root.gir.validate_class(self.tokens["class_name"], self.tokens["namespace"])
@validate("namespace", "class_name")
def not_abstract(self):
if self.gir_class is not None and self.gir_class.abstract:
raise CompileError(
f"{self.gir_class.full_name} can't be instantiated because it's abstract",
hints=[f"did you mean to use a subclass of {self.gir_class.full_name}?"]
)
@property @property
def gir_ns(self): def gir_ns(self):
if not self.tokens["ignore_gir"]: if not self.tokens["ignore_gir"]:
@ -76,9 +59,11 @@ class Object(AstNode):
@property @property
def gir_class(self): def gir_class(self):
if self.tokens["class_name"] and not self.tokens["ignore_gir"]: class_names = self.children[ClassName]
return self.root.gir.get_class(self.tokens["class_name"], self.tokens["namespace"]) if len(class_names) == 0:
return None
else:
return class_names[0].gir_type
@docs("namespace") @docs("namespace")
def namespace_docs(self): def namespace_docs(self):
@ -109,7 +94,7 @@ class Object(AstNode):
from .gtkbuilder_child import Child from .gtkbuilder_child import Child
xml.start_tag("object", **{ xml.start_tag("object", **{
"class": self.gir_class or self.tokens["class_name"], "class": self.children[ClassName][0].glib_type_name,
"id": self.tokens["id"], "id": self.tokens["id"],
}) })
for child in self.children: for child in self.children:

View file

@ -18,6 +18,7 @@
# SPDX-License-Identifier: LGPL-3.0-or-later # SPDX-License-Identifier: LGPL-3.0-or-later
from .types import ClassName
from .gobject_object import Object, ObjectContent from .gobject_object import Object, ObjectContent
from .common import * from .common import *
@ -27,8 +28,8 @@ class Template(Object):
"template", "template",
UseIdent("name").expected("template class name"), UseIdent("name").expected("template class name"),
Optional([ Optional([
Match(":"), ":",
class_name.expected("parent class"), to_parse_node(ClassName).expected("parent class"),
]), ]),
ObjectContent, ObjectContent,
] ]
@ -38,10 +39,14 @@ class Template(Object):
pass # does not apply to templates pass # does not apply to templates
def emit_xml(self, xml: XmlEmitter): def emit_xml(self, xml: XmlEmitter):
parent = None
if len(self.children[ClassName]):
parent = self.children[ClassName][0].glib_type_name
xml.start_tag( xml.start_tag(
"template", "template",
**{"class": self.tokens["name"]}, **{"class": self.tokens["name"]},
parent=self.gir_class or self.tokens["class_name"] parent=parent,
) )
for child in self.children: for child in self.children:
child.emit_xml(xml) child.emit_xml(xml)

View file

@ -0,0 +1,53 @@
# lambdas.py
#
# Copyright 2022 James Westman <james@jwestman.net>
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .types import TypeName
from .expression import Expr
from .values import Value
from .common import *
class Lambda(Value, Scope):
grammar = [
"(",
TypeName,
UseIdent("argument"),
")",
"=>",
Expr,
]
def emit_xml(self, xml: XmlEmitter):
for child in self.children:
child.emit_xml(xml)
def get_objects(self):
return {
**self.parent.parent_by_type(Scope).get_objects(),
self.tokens["argument"]: None,
}
@property
def this_name(self) -> str:
return self.tokens["argument"]
@property
def this_type(self) -> str:
return self.children[TypeName][0].gir_type

View file

@ -0,0 +1,96 @@
# types.py
#
# Copyright 2022 James Westman <james@jwestman.net>
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
import typing as T
from .common import *
class TypeName(AstNode):
grammar = AnyOf(
[
UseIdent("namespace"),
".",
UseIdent("class_name"),
],
[
".",
UseIdent("class_name"),
UseLiteral("ignore_gir", True),
],
UseIdent("class_name"),
)
@validate("class_name")
def type_exists(self):
if not self.tokens["ignore_gir"] and self.gir_ns is not None:
self.root.gir.validate_type(self.tokens["class_name"], self.tokens["namespace"])
@validate("namespace")
def gir_ns_exists(self):
if not self.tokens["ignore_gir"]:
self.root.gir.validate_ns(self.tokens["namespace"])
@property
def gir_ns(self):
if not self.tokens["ignore_gir"]:
return self.root.gir.namespaces.get(self.tokens["namespace"] or "Gtk")
@property
def gir_type(self) -> T.Optional[gir.Class]:
if self.tokens["class_name"] and not self.tokens["ignore_gir"]:
return self.root.gir.get_type(self.tokens["class_name"], self.tokens["namespace"])
return None
@property
def glib_type_name(self) -> str:
if gir_type := self.gir_type:
return gir_type.glib_type_name
else:
return self.tokens["class_name"]
@docs("namespace")
def namespace_docs(self):
if ns := self.root.gir.namespaces.get(self.tokens["namespace"]):
return ns.doc
@docs("class_name")
def class_docs(self):
if self.gir_type:
return self.gir_type.doc
def emit_xml(self, xml: XmlEmitter):
pass
class ClassName(TypeName):
@validate("class_name")
def gir_type_is_class(self):
if self.gir_type is not None and not isinstance(self.gir_type, gir.Class):
raise CompileError(f"{self.gir_type.full_name} is not a class")
class ConcreteClassName(ClassName):
@validate("namespace", "class_name")
def not_abstract(self):
if self.gir_type is not None and self.gir_type.abstract:
raise CompileError(
f"{self.gir_type.full_name} can't be instantiated because it's abstract",
hints=[f"did you mean to use a subclass of {self.gir_type.full_name}?"]
)

View file

@ -24,7 +24,7 @@ from .gtkbuilder_template import Template
from .common import * from .common import *
class UI(AstNode): class UI(AstNode, Scope):
""" The AST node for the entire file """ """ The AST node for the entire file """
grammar = [ grammar = [
@ -63,6 +63,10 @@ class UI(AstNode):
return { obj.tokens["id"]: obj for obj in self.iterate_children_recursive() if obj.tokens["id"] is not None } return { obj.tokens["id"]: obj for obj in self.iterate_children_recursive() if obj.tokens["id"] is not None }
def get_objects(self):
return self.objects_by_id
@validate() @validate()
def gir_errors(self): def gir_errors(self):
# make sure gir is loaded # make sure gir is loaded

View file

@ -20,7 +20,6 @@
from .errors import MultipleErrors, PrintableError from .errors import MultipleErrors, PrintableError
from .parse_tree import * from .parse_tree import *
from .parser_utils import *
from .tokenizer import TokenType from .tokenizer import TokenType
from .language import OBJECT_HOOKS, OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI from .language import OBJECT_HOOKS, OBJECT_CONTENT_HOOKS, VALUE_HOOKS, Template, UI

View file

@ -1,36 +0,0 @@
# parser_utils.py
#
# Copyright 2021 James Westman <james@jwestman.net>
#
# This file is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# This file is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: LGPL-3.0-or-later
from .parse_tree import *
class_name = AnyOf(
[
UseIdent("namespace"),
".",
UseIdent("class_name"),
],
[
".",
UseIdent("class_name"),
UseLiteral("ignore_gir", True),
],
UseIdent("class_name"),
)

View file

@ -1 +1 @@
3,29,13,Namespace Gtk does not contain a class called NotARealClass 3,29,13,Namespace Gtk does not contain a type called NotARealClass

9
tests/samples/lambda.blp Normal file
View file

@ -0,0 +1,9 @@
using Gtk 4.0;
Gtk.BoolFilter filter {
expression: (Gtk.Label object) => object.visible;
}
Gtk.BoolFilter {
expression: (Gtk.Label object) => filter.invert;
}

16
tests/samples/lambda.ui Normal file
View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<object class="GtkBoolFilter" id="filter">
<property name="expression">
<lookup name="visible" type="GtkLabel"/>
</property>
</object>
<object class="GtkBoolFilter">
<property name="expression">
<lookup name="invert">
<constant>filter</constant>
</lookup>
</property>
</object>
</interface>

View file

@ -0,0 +1,3 @@
using Gtk 4.0;
template MyWidget : .MyBaseWidget {}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk" version="4.0"/>
<template class="MyWidget" parent="MyBaseWidget"></template>
</interface>

View file

@ -141,6 +141,7 @@ class TestSamples(unittest.TestCase):
self.assert_sample("flags") self.assert_sample("flags")
self.assert_sample("id_prop") self.assert_sample("id_prop")
self.assert_sample("inline_menu") self.assert_sample("inline_menu")
self.assert_sample("lambda")
self.assert_sample("layout") self.assert_sample("layout")
self.assert_sample("menu") self.assert_sample("menu")
self.assert_sample("numbers") self.assert_sample("numbers")
@ -153,6 +154,7 @@ class TestSamples(unittest.TestCase):
self.assert_sample("strings") self.assert_sample("strings")
self.assert_sample("style") self.assert_sample("style")
self.assert_sample("template") self.assert_sample("template")
self.assert_sample("template_custom_parent")
self.assert_sample("template_no_parent") self.assert_sample("template_no_parent")
self.assert_sample("translated") self.assert_sample("translated")
self.assert_sample("uint") self.assert_sample("uint")