From 00058334a58fc58f00eefa4a2be854b27bd497ec Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Wed, 19 Jan 2022 07:55:12 -0500 Subject: [PATCH 01/18] A IDL parser: Can parse current IDL format (but that may change). Has working unit tests. --- BUILD.gn | 1 + scripts/idl/BUILD.gn | 35 +++++ scripts/idl/__init__.py | 0 scripts/idl/matter_grammar.lark | 57 ++++++++ scripts/idl/matter_idl_types.py | 114 ++++++++++++++++ scripts/idl/parser.py | 224 ++++++++++++++++++++++++++++++++ scripts/idl/setup.py | 28 ++++ scripts/idl/test_parsing.py | 76 +++++++++++ scripts/requirements.txt | 3 + 9 files changed, 538 insertions(+) create mode 100644 scripts/idl/BUILD.gn create mode 100644 scripts/idl/__init__.py create mode 100644 scripts/idl/matter_grammar.lark create mode 100644 scripts/idl/matter_idl_types.py create mode 100755 scripts/idl/parser.py create mode 100644 scripts/idl/setup.py create mode 100644 scripts/idl/test_parsing.py diff --git a/BUILD.gn b/BUILD.gn index 258ea4852ff27f..0fe2b8cba7a38f 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -160,6 +160,7 @@ if (current_toolchain != "${dir_pw_toolchain}/default:default") { deps = [ "//:fake_platform_tests", "//scripts/build:build_examples.tests", + "//scripts/idl:idl.tests", "//src:tests_run", ] } diff --git a/scripts/idl/BUILD.gn b/scripts/idl/BUILD.gn new file mode 100644 index 00000000000000..00166c136659e7 --- /dev/null +++ b/scripts/idl/BUILD.gn @@ -0,0 +1,35 @@ +# Copyright (c) 2022 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import("//build_overrides/build.gni") +import("//build_overrides/chip.gni") + +import("//build_overrides/pigweed.gni") +import("$dir_pw_build/python.gni") + +pw_python_package("idl") { + setup = [ "setup.py" ] + inputs = [ + # Dependency grammar + "matter_grammar.lark", + ] + + sources = [ + "__init__.py", + "matter_idl_types.py", + "parser.py", + ] + + tests = [ "test_parsing.py" ] +} diff --git a/scripts/idl/__init__.py b/scripts/idl/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/scripts/idl/matter_grammar.lark b/scripts/idl/matter_grammar.lark new file mode 100644 index 00000000000000..73ec7be44549cd --- /dev/null +++ b/scripts/idl/matter_grammar.lark @@ -0,0 +1,57 @@ +struct: "struct"i id "{" struct_member* "}" +enum: "enum"i id ":" type "{" enum_entry* "}" + +event: event_priority "event"i id "=" number "{" struct_member* "}" + +?event_priority: "critical"i -> critical_priority + | "info"i -> info_priority + | "debug"i -> debug_priority + +attribute: "attribute"i "(" attribute_access ")" named_member +attribute_access: "readonly"i -> readonly + | "writable"i -> writable + +request_struct: "request"i struct +response_struct: "response"i struct + +command: "command"i id "(" id? ")" ":" id "=" number ";" + +cluster: cluster_side "cluster"i id "=" number "{" (enum|event|attribute|struct|request_struct|response_struct|command)* "}" +?cluster_side: "server"i -> server_cluster + | "client"i -> client_cluster + +endpoint: "endpoint"i number "{" endpoint_cluster* "}" +endpoint_cluster: endpoint_cluster_type "cluster"i id ";" + +?endpoint_cluster_type: "server"i -> endpoint_server_cluster + | "binding"i -> endpoint_binding_to_cluster + +enum_entry: id "=" number ";" +number: POSITIVE_INTEGER | HEX_INTEGER + +struct_member: member_attribute* named_member + +member_attribute: "optional"i -> optional + | "nullable"i -> nullable + +named_member: type id list_marker? "=" number ";" +list_marker: "[" "]" + +id: ID +type: ID + +COMMENT: "{" /(.|\n)+/ "}" + | "//" /.*/ + +POSITIVE_INTEGER: /\d+/ +HEX_INTEGER: /0x[A-Fa-f0-9]+/ +ID: /[a-zA-Z_][a-zA-Z0-9_]*/ + +idl: (struct|enum|cluster|endpoint)* + +%import common.WS +%import common.C_COMMENT +%import common.CPP_COMMENT +%ignore WS +%ignore C_COMMENT +%ignore CPP_COMMENT diff --git a/scripts/idl/matter_idl_types.py b/scripts/idl/matter_idl_types.py new file mode 100644 index 00000000000000..62d094e684e5c6 --- /dev/null +++ b/scripts/idl/matter_idl_types.py @@ -0,0 +1,114 @@ +import enum + +from dataclasses import dataclass, field +from typing import List, Set + + +class MemberAttribute(enum.Enum): + OPTIONAL = enum.auto() + NULLABLE = enum.auto() + + +class AttributeAccess(enum.Enum): + READONLY = enum.auto() + WRITABLE = enum.auto() + + +class EventPriority(enum.Enum): + DEBUG = enum.auto() + INFO = enum.auto() + CRITICAL = enum.auto() + + +class ClusterSide(enum.Enum): + CLIENT = enum.auto() + SERVER = enum.auto() + + +class StructTag(enum.Enum): + REQUEST = enum.auto() + RESPONSE = enum.auto() + + +class EndpointContentType(enum.Enum): + SERVER_CLUSTER = enum.auto() + CLIENT_BINDING = enum.auto() + + +@dataclass +class StructureMember: + data_type: str + code: int + name: str + is_list: bool = False + attributes: Set[MemberAttribute] = field(default_factory=set) + + +@dataclass +class Attribute: + access: AttributeAccess + definition: StructureMember + + +@dataclass +class Struct: + name: str + members: List[StructureMember] + tag: StructTag = None + + +@dataclass +class Event: + priority: EventPriority + name: str + code: int + members: List[StructureMember] + + +@dataclass +class EnumEntry: + name: str + code: int + + +@dataclass +class Enum: + name: str + base_type: str + entries: List[EnumEntry] + + +@dataclass +class Command: + name: str + code: int + input_param: str + output_param: str + + +@dataclass +class Cluster: + side: ClusterSide + name: str + code: int + enums: List[Enum] = field(default_factory=list) + events: List[Event] = field(default_factory=list) + attributes: List[Attribute] = field(default_factory=list) + structs: List[Struct] = field(default_factory=list) + commands: List[Command] = field(default_factory=list) + + +@dataclass +class Endpoint: + number: int + server_clusters: List[str] = field(default_factory=list) + client_bindings: List[str] = field(default_factory=list) + + +@dataclass +class Idl: + # Enums and structs represent globally used items + enums: List[Enum] = field(default_factory=list) + structs: List[Struct] = field(default_factory=list) + clusters: List[Cluster] = field(default_factory=list) + endpoints: List[Endpoint] = field(default_factory=list) diff --git a/scripts/idl/parser.py b/scripts/idl/parser.py new file mode 100755 index 00000000000000..f519a0710cf2cc --- /dev/null +++ b/scripts/idl/parser.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python + +import logging + +from lark import Lark +from lark.visitors import Transformer, v_args + +try: + from .matter_idl_types import * +except: + import os + import sys + sys.path.append(os.path.abspath(os.path.dirname(__file__))) + + from matter_idl_types import * + + +class MatterIdlTransformer(Transformer): + """A transformer capable to transform data + parsed by Lark according to matter_grammar.lark + """ + + def number(self, tokens): + """Numbers in the grammar are integers or hex numbers. + """ + if len(tokens) != 1: + raise Error("Unexpected argument counts") + + n = tokens[0].value + if n.startswith('0x'): + return int(n[2:], 16) + else: + return int(n) + + def id(self, tokens): + """An id is a string containing an identifier + """ + if len(tokens) != 1: + raise Error("Unexpected argument counts") + return tokens[0].value + + def type(self, tokens): + """A type is just a string for the type + """ + if len(tokens) != 1: + raise Error("Unexpected argument counts") + return tokens[0].value + + @v_args(inline=True) + def enum_entry(self, id, number): + return EnumEntry(name=id, code=number) + + @v_args(inline=True) + def enum(self, id, type, *entries): + return Enum(name=id, base_type=type, entries=list(entries)) + + def named_member(self, args): + data_type, name = args[0], args[1] + is_list = (len(args) == 4) + code = args[-1] + + return StructureMember(data_type=data_type, name=name, code=code, is_list=is_list) + + def optional(self, _): + return MemberAttribute.OPTIONAL + + def nullable(self, _): + return MemberAttribute.NULLABLE + + def readonly(self, _): + return AttributeAccess.READONLY + + def writable(self, _): + return AttributeAccess.WRITABLE + + def critical_priority(self, _): + return EventPriority.CRITICAL + + def info_priority(self, _): + return EventPriority.INFO + + def debug_priority(self, _): + return EventPriority.DEBUG + + def endpoint_server_cluster(self, _): + return EndpointContentType.SERVER_CLUSTER + + def endpoint_binding_to_cluster(self, _): + return EndpointContentType.CLIENT_BINDING + + def struct_member(self, args): + # Last argument is the named_member, the rest + # are attributes + member = args[-1] + member.attributes = set(args[:-1]) + return member + + def server_cluster(self, _): + return ClusterSide.SERVER + + def client_cluster(self, _): + return ClusterSide.CLIENT + + def command(self, args): + # A command has 3 arguments if no input or + # 4 arguments if input parameter is available + param_in = None + if len(args) > 3: + param_in = args[1] + return Command(name=args[0], input_param=param_in, output_param=args[-2], code=args[-1]) + + def event(self, args): + return Event(priority=args[0], name=args[1], code=args[2], members=args[3:], ) + + @v_args(inline=True) + def attribute(self, attribute_access, named_member): + return Attribute(access=attribute_access, definition=named_member) + + @v_args(inline=True) + def struct(self, id, *members): + return Struct(name=id, members=list(members)) + + @v_args(inline=True) + def request_struct(self, value): + value.tag = StructTag.REQUEST + return value + + @v_args(inline=True) + def response_struct(self, value): + value.tag = StructTag.RESPONSE + return value + + @v_args(inline=True) + def endpoint(self, number, *clusters): + endpoint = Endpoint(number=number) + + for t, name in clusters: + if t == EndpointContentType.CLIENT_BINDING: + endpoint.client_bindings.append(t) + elif t == EndpointContentType.SERVER_CLUSTER: + endpoint.server_clusters.append(t) + else: + raise Error("Unknown endpoint content: %r" % t) + + return endpoint + + @v_args(inline=True) + def endpoint_cluster(self, t, id): + return (t, id) + + @v_args(inline=True) + def cluster(self, side, name, code, *content): + result = Cluster(side=side, name=name, code=code) + + for item in content: + if type(item) == Enum: + result.enums.append(item) + elif type(item) == Event: + result.events.append(item) + elif type(item) == Attribute: + result.attributes.append(item) + elif type(item) == Struct: + result.structs.append(item) + elif type(item) == Command: + result.commands.append(item) + else: + raise Error("UNKNOWN cluster content item: %r" % item) + + return result + + def idl(self, items): + idl = Idl() + + for item in items: + if type(item) == Enum: + idl.enums.append(item) + elif type(item) == Struct: + idl.structs.append(item) + elif type(item) == Cluster: + idl.clusters.append(item) + elif type(item) == Endpoint: + idl.endpoints.append(item) + else: + raise Error("UNKNOWN idl content item: %r" % item) + + return idl + + +def CreateParser(): + return Lark.open('matter_grammar.lark', rel_to=__file__, start='idl', parser='lalr', transformer=MatterIdlTransformer()) + + +if __name__ == '__main__': + import click + import coloredlogs + + # Supported log levels, mapping string values required for argument + # parsing into logging constants + __LOG_LEVELS__ = { + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warn': logging.WARN, + 'fatal': logging.FATAL, + } + + @click.command() + @click.option( + '--log-level', + default='INFO', + type=click.Choice(__LOG_LEVELS__.keys(), case_sensitive=False), + help='Determines the verbosity of script output.') + @click.argument('filename') + def main(log_level, filename=None): + coloredlogs.install(level=__LOG_LEVELS__[ + log_level], fmt='%(asctime)s %(levelname)-7s %(message)s') + + logging.info("Starting to parse ...") + data = CreateParser().parse(open(filename).read()) + logging.info("Parse completed") + + logging.info("Data:") + print(data) + + main() diff --git a/scripts/idl/setup.py b/scripts/idl/setup.py new file mode 100644 index 00000000000000..4566792c40074a --- /dev/null +++ b/scripts/idl/setup.py @@ -0,0 +1,28 @@ +# Copyright (c) 2022 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""The idl package.""" + +import setuptools # type: ignore + +setuptools.setup( + name='idl', + version='0.0.1', + author='Project CHIP Authors', + description='Parse matter idl files', + packages=setuptools.find_packages(), + package_data={'idl': ['py.typed']}, + zip_safe=False, +) diff --git a/scripts/idl/test_parsing.py b/scripts/idl/test_parsing.py new file mode 100644 index 00000000000000..50693e3d0584d6 --- /dev/null +++ b/scripts/idl/test_parsing.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2022 Project CHIP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from parser import CreateParser +from matter_idl_types import * + +import unittest + + +def parseText(txt): + return CreateParser().parse(txt) + + +class TestParser(unittest.TestCase): + + def test_skips_comments(self): + self.assertEqual(parseText(""" + // this is a single line comment + // repeated + + /* This is a C++ comment + and also whitespace should be ignored + */ + """), Idl()) + + def test_global_enum(self): + self.assertEqual(parseText(""" + enum GlobalEnum : ENUM8 { + kValue1 = 1; + kOther = 0x12; + } + """), + Idl(enums=[ + Enum(name='GlobalEnum', base_type='ENUM8', + entries=[ + EnumEntry(name="kValue1", code=1), + EnumEntry(name="kOther", code=0x12), + ])] + )) + + def test_global_struct(self): + self.assertEqual(parseText(""" + struct Something { + CHAR_STRING astring = 1; + optional CLUSTER_ID idlist[] = 2; + nullable int valueThatIsNullable = 0x123; + } + """), + Idl(structs=[ + Struct(name='Something', + members=[ + StructureMember( + data_type="CHAR_STRING", code=1, name="astring", ), + StructureMember(data_type="CLUSTER_ID", code=2, name="idlist", is_list=True, attributes=set( + [MemberAttribute.OPTIONAL])), + StructureMember(data_type="int", code=0x123, name="valueThatIsNullable", attributes=set( + [MemberAttribute.NULLABLE])), + ])] + )) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/requirements.txt b/scripts/requirements.txt index f68b124bb7d6f7..0be3b90c84f2e9 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -46,3 +46,6 @@ pandas ; platform_machine != 'aarch64' and platform_machine != 'arm64' # scripts/build click + +# scripts/idl +lark From 894f0ae573de6396261193ba822d1d263d5d5971 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Wed, 19 Jan 2022 14:42:04 -0500 Subject: [PATCH 02/18] one more test --- scripts/idl/matter_idl_types.py | 2 +- scripts/idl/parser.py | 2 +- scripts/idl/test_parsing.py | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/scripts/idl/matter_idl_types.py b/scripts/idl/matter_idl_types.py index 62d094e684e5c6..d3f5d676659e8e 100644 --- a/scripts/idl/matter_idl_types.py +++ b/scripts/idl/matter_idl_types.py @@ -11,7 +11,7 @@ class MemberAttribute(enum.Enum): class AttributeAccess(enum.Enum): READONLY = enum.auto() - WRITABLE = enum.auto() + READWRITE = enum.auto() class EventPriority(enum.Enum): diff --git a/scripts/idl/parser.py b/scripts/idl/parser.py index f519a0710cf2cc..b6eb6e2a761991 100755 --- a/scripts/idl/parser.py +++ b/scripts/idl/parser.py @@ -71,7 +71,7 @@ def readonly(self, _): return AttributeAccess.READONLY def writable(self, _): - return AttributeAccess.WRITABLE + return AttributeAccess.READWRITE def critical_priority(self, _): return EventPriority.CRITICAL diff --git a/scripts/idl/test_parsing.py b/scripts/idl/test_parsing.py index 50693e3d0584d6..d2d2a7f5394a8a 100644 --- a/scripts/idl/test_parsing.py +++ b/scripts/idl/test_parsing.py @@ -71,6 +71,25 @@ def test_global_struct(self): ])] )) + def test_cluster_attribute(self): + self.assertEqual(parseText(""" + server cluster MyCluster = 0x321 { + attribute(readonly) int8u roAttr = 1; + attribute(writable) int32u rwAttr[] = 123; + } + """), + Idl(clusters=[ + Cluster(side=ClusterSide.SERVER, + name="MyCluster", + code=0x321, + attributes=[ + Attribute(access=AttributeAccess.READONLY, definition=StructureMember( + data_type="int8u", code=1, name="roAttr")), + Attribute(access=AttributeAccess.READWRITE, definition=StructureMember( + data_type="int32u", code=123, name="rwAttr", is_list=True)), + ] + )])) + if __name__ == '__main__': unittest.main() From 4a9bfb076ce36eae4472a7a84775f90008a31289 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Wed, 19 Jan 2022 14:42:52 -0500 Subject: [PATCH 03/18] minor comment --- scripts/idl/test_parsing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/idl/test_parsing.py b/scripts/idl/test_parsing.py index d2d2a7f5394a8a..4c1a330e334511 100644 --- a/scripts/idl/test_parsing.py +++ b/scripts/idl/test_parsing.py @@ -40,7 +40,7 @@ def test_global_enum(self): self.assertEqual(parseText(""" enum GlobalEnum : ENUM8 { kValue1 = 1; - kOther = 0x12; + kOther = 0x12; /* hex numbers tested sporadically */ } """), Idl(enums=[ From 71eb8b6947a84c04dabc2f351bff22269a9a46c6 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Wed, 19 Jan 2022 14:46:36 -0500 Subject: [PATCH 04/18] make the structs a bit more compact: easier to read --- scripts/idl/test_parsing.py | 83 ++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/scripts/idl/test_parsing.py b/scripts/idl/test_parsing.py index 4c1a330e334511..245c98ba0e66fb 100644 --- a/scripts/idl/test_parsing.py +++ b/scripts/idl/test_parsing.py @@ -27,68 +27,77 @@ def parseText(txt): class TestParser(unittest.TestCase): def test_skips_comments(self): - self.assertEqual(parseText(""" + actual = parseText(""" // this is a single line comment // repeated /* This is a C++ comment and also whitespace should be ignored */ - """), Idl()) + """) + expected = Idl() + + self.assertEqual(actual, expected) def test_global_enum(self): - self.assertEqual(parseText(""" + actual = parseText(""" enum GlobalEnum : ENUM8 { kValue1 = 1; kOther = 0x12; /* hex numbers tested sporadically */ } - """), - Idl(enums=[ - Enum(name='GlobalEnum', base_type='ENUM8', - entries=[ - EnumEntry(name="kValue1", code=1), - EnumEntry(name="kOther", code=0x12), - ])] - )) + """) + + expected = Idl(enums=[ + Enum(name='GlobalEnum', base_type='ENUM8', + entries=[ + EnumEntry(name="kValue1", code=1), + EnumEntry(name="kOther", code=0x12), + ])] + ) + self.assertEqual(actual, expected) def test_global_struct(self): - self.assertEqual(parseText(""" + actual = parseText(""" struct Something { CHAR_STRING astring = 1; optional CLUSTER_ID idlist[] = 2; nullable int valueThatIsNullable = 0x123; } - """), - Idl(structs=[ - Struct(name='Something', - members=[ - StructureMember( - data_type="CHAR_STRING", code=1, name="astring", ), - StructureMember(data_type="CLUSTER_ID", code=2, name="idlist", is_list=True, attributes=set( - [MemberAttribute.OPTIONAL])), - StructureMember(data_type="int", code=0x123, name="valueThatIsNullable", attributes=set( - [MemberAttribute.NULLABLE])), - ])] - )) + """) + + expected = Idl(structs=[ + Struct(name='Something', + members=[ + StructureMember( + data_type="CHAR_STRING", code=1, name="astring", ), + StructureMember(data_type="CLUSTER_ID", code=2, name="idlist", is_list=True, attributes=set( + [MemberAttribute.OPTIONAL])), + StructureMember(data_type="int", code=0x123, name="valueThatIsNullable", attributes=set( + [MemberAttribute.NULLABLE])), + ])] + ) + self.assertEqual(actual, expected) def test_cluster_attribute(self): - self.assertEqual(parseText(""" + actual = parseText(""" server cluster MyCluster = 0x321 { attribute(readonly) int8u roAttr = 1; attribute(writable) int32u rwAttr[] = 123; } - """), - Idl(clusters=[ - Cluster(side=ClusterSide.SERVER, - name="MyCluster", - code=0x321, - attributes=[ - Attribute(access=AttributeAccess.READONLY, definition=StructureMember( - data_type="int8u", code=1, name="roAttr")), - Attribute(access=AttributeAccess.READWRITE, definition=StructureMember( - data_type="int32u", code=123, name="rwAttr", is_list=True)), - ] - )])) + """) + + expected = Idl(clusters=[ + Cluster(side=ClusterSide.SERVER, + name="MyCluster", + code=0x321, + attributes=[ + Attribute(access=AttributeAccess.READONLY, definition=StructureMember( + data_type="int8u", code=1, name="roAttr")), + Attribute(access=AttributeAccess.READWRITE, definition=StructureMember( + data_type="int32u", code=123, name="rwAttr", is_list=True)), + ] + )]) + self.assertEqual(actual, expected) if __name__ == '__main__': From ddf6c2b9815fc3d6dbd0e27e06b868209d7f4ebf Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Wed, 19 Jan 2022 14:48:26 -0500 Subject: [PATCH 05/18] one more tests --- scripts/idl/test_parsing.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/idl/test_parsing.py b/scripts/idl/test_parsing.py index 245c98ba0e66fb..a8c9d1ea7cbf55 100644 --- a/scripts/idl/test_parsing.py +++ b/scripts/idl/test_parsing.py @@ -99,6 +99,20 @@ def test_cluster_attribute(self): )]) self.assertEqual(actual, expected) + def test_multiple_clusters(self): + actual = parseText(""" + server cluster A = 1 {} + client cluster B = 2 {} + client cluster C = 3 {} + """) + + expected = Idl(clusters=[ + Cluster(side=ClusterSide.SERVER, name="A", code=1), + Cluster(side=ClusterSide.CLIENT, name="B", code=2), + Cluster(side=ClusterSide.CLIENT, name="C", code=3), + ]) + self.assertEqual(actual, expected) + if __name__ == '__main__': unittest.main() From b2b6f9e6e3dea5b4108ac921570ee0a6925da138 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Wed, 19 Jan 2022 14:54:43 -0500 Subject: [PATCH 06/18] more tests, fixed one bug --- scripts/idl/parser.py | 4 ++-- scripts/idl/test_parsing.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/scripts/idl/parser.py b/scripts/idl/parser.py index b6eb6e2a761991..6d4861cc4f55a8 100755 --- a/scripts/idl/parser.py +++ b/scripts/idl/parser.py @@ -136,9 +136,9 @@ def endpoint(self, number, *clusters): for t, name in clusters: if t == EndpointContentType.CLIENT_BINDING: - endpoint.client_bindings.append(t) + endpoint.client_bindings.append(name) elif t == EndpointContentType.SERVER_CLUSTER: - endpoint.server_clusters.append(t) + endpoint.server_clusters.append(name) else: raise Error("Unknown endpoint content: %r" % t) diff --git a/scripts/idl/test_parsing.py b/scripts/idl/test_parsing.py index a8c9d1ea7cbf55..c2c449b2634006 100644 --- a/scripts/idl/test_parsing.py +++ b/scripts/idl/test_parsing.py @@ -113,6 +113,38 @@ def test_multiple_clusters(self): ]) self.assertEqual(actual, expected) + def test_endpoints(self): + actual = parseText(""" + endpoint 12 { + server cluster Foo; + server cluster Bar; + binding cluster Bar; + binding cluster Test; + } + """) + + expected = Idl(endpoints=[Endpoint(number=12, + server_clusters=["Foo", "Bar"], + client_bindings=["Bar", "Test"],) + ]) + self.assertEqual(actual, expected) + + def test_multi_endpoints(self): + actual = parseText(""" + endpoint 1 {} + endpoint 2 {} + endpoint 0xa {} + endpoint 100 {} + """) + + expected = Idl(endpoints=[ + Endpoint(number=1), + Endpoint(number=2), + Endpoint(number=10), + Endpoint(number=100), + ]) + self.assertEqual(actual, expected) + if __name__ == '__main__': unittest.main() From fd5dc48161b1524f97b7e9f123c0be9779cf7b5a Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Wed, 19 Jan 2022 14:59:36 -0500 Subject: [PATCH 07/18] Add unit test for cluster commands --- scripts/idl/test_parsing.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/scripts/idl/test_parsing.py b/scripts/idl/test_parsing.py index c2c449b2634006..628467f06399e0 100644 --- a/scripts/idl/test_parsing.py +++ b/scripts/idl/test_parsing.py @@ -99,6 +99,37 @@ def test_cluster_attribute(self): )]) self.assertEqual(actual, expected) + def test_cluster_commands(self): + actual = parseText(""" + server cluster WithCommands = 1 { + struct FreeStruct {} + request struct InParam {} + response struct OutParam {} + + command WithoutArg(): DefaultSuccess = 123; + command InOutStuff(InParam): OutParam = 222; + } + """) + expected = Idl(clusters=[ + Cluster(side=ClusterSide.SERVER, + name="WithCommands", + code=1, + structs=[ + Struct(name="FreeStruct", members=[]), + Struct(name="InParam", members=[], + tag=StructTag.REQUEST), + Struct(name="OutParam", members=[], + tag=StructTag.RESPONSE), + ], + commands=[ + Command(name="WithoutArg", code=123, + input_param=None, output_param="DefaultSuccess"), + Command(name="InOutStuff", code=222, + input_param="InParam", output_param="OutParam"), + ], + )]) + self.assertEqual(actual, expected) + def test_multiple_clusters(self): actual = parseText(""" server cluster A = 1 {} From a9f9f0df1b61b40c353b633f8bd4bddc8a874d15 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Wed, 19 Jan 2022 15:02:13 -0500 Subject: [PATCH 08/18] Unit test for cluster enums --- scripts/idl/test_parsing.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/scripts/idl/test_parsing.py b/scripts/idl/test_parsing.py index 628467f06399e0..ae2e52d888eef3 100644 --- a/scripts/idl/test_parsing.py +++ b/scripts/idl/test_parsing.py @@ -130,6 +130,28 @@ def test_cluster_commands(self): )]) self.assertEqual(actual, expected) + def test_cluster_enum(self): + actual = parseText(""" + client cluster WithEnums = 0xab { + enum TestEnum : ENUM16 { + A = 0x123; + B = 0x234; + } + } + """) + expected = Idl(clusters=[ + Cluster(side=ClusterSide.CLIENT, + name="WithEnums", + code=0xab, + enums=[ + Enum(name="TestEnum", base_type="ENUM16", + entries=[ + EnumEntry(name="A", code=0x123), + EnumEntry(name="B", code=0x234), + ])], + )]) + self.assertEqual(actual, expected) + def test_multiple_clusters(self): actual = parseText(""" server cluster A = 1 {} From b0eeb036ca985b559be16b271ddae701e7718e70 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Wed, 19 Jan 2022 15:05:56 -0500 Subject: [PATCH 09/18] Unit test for cluster events --- scripts/idl/test_parsing.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/scripts/idl/test_parsing.py b/scripts/idl/test_parsing.py index ae2e52d888eef3..fb9cd522c21b0f 100644 --- a/scripts/idl/test_parsing.py +++ b/scripts/idl/test_parsing.py @@ -152,6 +152,32 @@ def test_cluster_enum(self): )]) self.assertEqual(actual, expected) + def test_cluster_events(self): + actual = parseText(""" + client cluster EventTester = 0x123 { + critical event StartUp = 0 { + INT32U softwareVersion = 0; + } + info event Hello = 1 {} + debug event GoodBye = 2 {} + } + """) + expected = Idl(clusters=[ + Cluster(side=ClusterSide.CLIENT, + name="EventTester", + code=0x123, + events=[ + Event(priority=EventPriority.CRITICAL, name="StartUp", code=0, members=[ + StructureMember(data_type="INT32U", + code=0, name="softwareVersion"), + ]), + Event(priority=EventPriority.INFO, + name="Hello", code=1, members=[]), + Event(priority=EventPriority.DEBUG, + name="GoodBye", code=2, members=[]), + ])]) + self.assertEqual(actual, expected) + def test_multiple_clusters(self): actual = parseText(""" server cluster A = 1 {} From c70a4867f0a5ecc62295c82bcc9a36819abac289 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Wed, 19 Jan 2022 16:05:32 -0500 Subject: [PATCH 10/18] Rename "structure_member" to field since that seems easier to type and is a good term --- scripts/idl/matter_grammar.lark | 10 +++++----- scripts/idl/matter_idl_types.py | 12 ++++++------ scripts/idl/parser.py | 22 +++++++++++----------- scripts/idl/test_parsing.py | 30 +++++++++++++++--------------- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/scripts/idl/matter_grammar.lark b/scripts/idl/matter_grammar.lark index 73ec7be44549cd..baa87a21b62990 100644 --- a/scripts/idl/matter_grammar.lark +++ b/scripts/idl/matter_grammar.lark @@ -1,13 +1,13 @@ -struct: "struct"i id "{" struct_member* "}" +struct: "struct"i id "{" struct_field* "}" enum: "enum"i id ":" type "{" enum_entry* "}" -event: event_priority "event"i id "=" number "{" struct_member* "}" +event: event_priority "event"i id "=" number "{" struct_field* "}" ?event_priority: "critical"i -> critical_priority | "info"i -> info_priority | "debug"i -> debug_priority -attribute: "attribute"i "(" attribute_access ")" named_member +attribute: "attribute"i "(" attribute_access ")" field attribute_access: "readonly"i -> readonly | "writable"i -> writable @@ -29,12 +29,12 @@ endpoint_cluster: endpoint_cluster_type "cluster"i id ";" enum_entry: id "=" number ";" number: POSITIVE_INTEGER | HEX_INTEGER -struct_member: member_attribute* named_member +struct_field: member_attribute* field member_attribute: "optional"i -> optional | "nullable"i -> nullable -named_member: type id list_marker? "=" number ";" +field: type id list_marker? "=" number ";" list_marker: "[" "]" id: ID diff --git a/scripts/idl/matter_idl_types.py b/scripts/idl/matter_idl_types.py index d3f5d676659e8e..89dd63b0ad614c 100644 --- a/scripts/idl/matter_idl_types.py +++ b/scripts/idl/matter_idl_types.py @@ -4,7 +4,7 @@ from typing import List, Set -class MemberAttribute(enum.Enum): +class FieldAttribute(enum.Enum): OPTIONAL = enum.auto() NULLABLE = enum.auto() @@ -36,24 +36,24 @@ class EndpointContentType(enum.Enum): @dataclass -class StructureMember: +class Field: data_type: str code: int name: str is_list: bool = False - attributes: Set[MemberAttribute] = field(default_factory=set) + attributes: Set[FieldAttribute] = field(default_factory=set) @dataclass class Attribute: access: AttributeAccess - definition: StructureMember + definition: Field @dataclass class Struct: name: str - members: List[StructureMember] + fields: List[Field] tag: StructTag = None @@ -62,7 +62,7 @@ class Event: priority: EventPriority name: str code: int - members: List[StructureMember] + fields: List[Field] @dataclass diff --git a/scripts/idl/parser.py b/scripts/idl/parser.py index 6d4861cc4f55a8..a688bc3a02e80e 100755 --- a/scripts/idl/parser.py +++ b/scripts/idl/parser.py @@ -54,18 +54,18 @@ def enum_entry(self, id, number): def enum(self, id, type, *entries): return Enum(name=id, base_type=type, entries=list(entries)) - def named_member(self, args): + def field(self, args): data_type, name = args[0], args[1] is_list = (len(args) == 4) code = args[-1] - return StructureMember(data_type=data_type, name=name, code=code, is_list=is_list) + return Field(data_type=data_type, name=name, code=code, is_list=is_list) def optional(self, _): - return MemberAttribute.OPTIONAL + return FieldAttribute.OPTIONAL def nullable(self, _): - return MemberAttribute.NULLABLE + return FieldAttribute.NULLABLE def readonly(self, _): return AttributeAccess.READONLY @@ -88,12 +88,12 @@ def endpoint_server_cluster(self, _): def endpoint_binding_to_cluster(self, _): return EndpointContentType.CLIENT_BINDING - def struct_member(self, args): + def struct_field(self, args): # Last argument is the named_member, the rest # are attributes - member = args[-1] - member.attributes = set(args[:-1]) - return member + field = args[-1] + field.attributes = set(args[:-1]) + return field def server_cluster(self, _): return ClusterSide.SERVER @@ -110,15 +110,15 @@ def command(self, args): return Command(name=args[0], input_param=param_in, output_param=args[-2], code=args[-1]) def event(self, args): - return Event(priority=args[0], name=args[1], code=args[2], members=args[3:], ) + return Event(priority=args[0], name=args[1], code=args[2], fields=args[3:], ) @v_args(inline=True) def attribute(self, attribute_access, named_member): return Attribute(access=attribute_access, definition=named_member) @v_args(inline=True) - def struct(self, id, *members): - return Struct(name=id, members=list(members)) + def struct(self, id, *fields): + return Struct(name=id, fields=list(fields)) @v_args(inline=True) def request_struct(self, value): diff --git a/scripts/idl/test_parsing.py b/scripts/idl/test_parsing.py index fb9cd522c21b0f..59fd7440b480dc 100644 --- a/scripts/idl/test_parsing.py +++ b/scripts/idl/test_parsing.py @@ -67,13 +67,13 @@ def test_global_struct(self): expected = Idl(structs=[ Struct(name='Something', - members=[ - StructureMember( + fields=[ + Field( data_type="CHAR_STRING", code=1, name="astring", ), - StructureMember(data_type="CLUSTER_ID", code=2, name="idlist", is_list=True, attributes=set( - [MemberAttribute.OPTIONAL])), - StructureMember(data_type="int", code=0x123, name="valueThatIsNullable", attributes=set( - [MemberAttribute.NULLABLE])), + Field(data_type="CLUSTER_ID", code=2, name="idlist", is_list=True, attributes=set( + [FieldAttribute.OPTIONAL])), + Field(data_type="int", code=0x123, name="valueThatIsNullable", attributes=set( + [FieldAttribute.NULLABLE])), ])] ) self.assertEqual(actual, expected) @@ -91,9 +91,9 @@ def test_cluster_attribute(self): name="MyCluster", code=0x321, attributes=[ - Attribute(access=AttributeAccess.READONLY, definition=StructureMember( + Attribute(access=AttributeAccess.READONLY, definition=Field( data_type="int8u", code=1, name="roAttr")), - Attribute(access=AttributeAccess.READWRITE, definition=StructureMember( + Attribute(access=AttributeAccess.READWRITE, definition=Field( data_type="int32u", code=123, name="rwAttr", is_list=True)), ] )]) @@ -115,10 +115,10 @@ def test_cluster_commands(self): name="WithCommands", code=1, structs=[ - Struct(name="FreeStruct", members=[]), - Struct(name="InParam", members=[], + Struct(name="FreeStruct", fields=[]), + Struct(name="InParam", fields=[], tag=StructTag.REQUEST), - Struct(name="OutParam", members=[], + Struct(name="OutParam", fields=[], tag=StructTag.RESPONSE), ], commands=[ @@ -167,14 +167,14 @@ def test_cluster_events(self): name="EventTester", code=0x123, events=[ - Event(priority=EventPriority.CRITICAL, name="StartUp", code=0, members=[ - StructureMember(data_type="INT32U", + Event(priority=EventPriority.CRITICAL, name="StartUp", code=0, fields=[ + Field(data_type="INT32U", code=0, name="softwareVersion"), ]), Event(priority=EventPriority.INFO, - name="Hello", code=1, members=[]), + name="Hello", code=1, fields=[]), Event(priority=EventPriority.DEBUG, - name="GoodBye", code=2, members=[]), + name="GoodBye", code=2, fields=[]), ])]) self.assertEqual(actual, expected) From 79594c0b9502c9f022f46cfb391fbc0cd29eb140 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Wed, 19 Jan 2022 17:10:26 -0500 Subject: [PATCH 11/18] Match the newest attribute format for IDLs --- scripts/idl/matter_grammar.lark | 3 +-- scripts/idl/parser.py | 9 ++++++--- scripts/idl/test_parsing.py | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/scripts/idl/matter_grammar.lark b/scripts/idl/matter_grammar.lark index baa87a21b62990..cbaf0c0249d4f8 100644 --- a/scripts/idl/matter_grammar.lark +++ b/scripts/idl/matter_grammar.lark @@ -7,9 +7,8 @@ event: event_priority "event"i id "=" number "{" struct_field* "}" | "info"i -> info_priority | "debug"i -> debug_priority -attribute: "attribute"i "(" attribute_access ")" field +attribute: attribute_access? "attribute"i field attribute_access: "readonly"i -> readonly - | "writable"i -> writable request_struct: "request"i struct response_struct: "response"i struct diff --git a/scripts/idl/parser.py b/scripts/idl/parser.py index a688bc3a02e80e..e8acaee9cff020 100755 --- a/scripts/idl/parser.py +++ b/scripts/idl/parser.py @@ -112,9 +112,12 @@ def command(self, args): def event(self, args): return Event(priority=args[0], name=args[1], code=args[2], fields=args[3:], ) - @v_args(inline=True) - def attribute(self, attribute_access, named_member): - return Attribute(access=attribute_access, definition=named_member) + def attribute(self, args): + access = AttributeAccess.READWRITE # default + if len(args) > 1: + access = args[0] + + return Attribute(access=access, definition=args[-1]) @v_args(inline=True) def struct(self, id, *fields): diff --git a/scripts/idl/test_parsing.py b/scripts/idl/test_parsing.py index 59fd7440b480dc..116a766e15f94f 100644 --- a/scripts/idl/test_parsing.py +++ b/scripts/idl/test_parsing.py @@ -81,8 +81,8 @@ def test_global_struct(self): def test_cluster_attribute(self): actual = parseText(""" server cluster MyCluster = 0x321 { - attribute(readonly) int8u roAttr = 1; - attribute(writable) int32u rwAttr[] = 123; + readonly attribute int8u roAttr = 1; + attribute int32u rwAttr[] = 123; } """) From 4181a795f861f6ff3bb800e955ea06a7734ab42d Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Thu, 20 Jan 2022 08:50:47 -0500 Subject: [PATCH 12/18] Allow test_parsing to be run stand alone and hope that this fix also fixes mac --- scripts/idl/test_parsing.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) mode change 100644 => 100755 scripts/idl/test_parsing.py diff --git a/scripts/idl/test_parsing.py b/scripts/idl/test_parsing.py old mode 100644 new mode 100755 index 116a766e15f94f..016684b19fcfd7 --- a/scripts/idl/test_parsing.py +++ b/scripts/idl/test_parsing.py @@ -14,8 +14,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from parser import CreateParser -from matter_idl_types import * +try: + from .parser import CreateParser + from .matter_idl_types import * +except: + import os + import sys + sys.path.append(os.path.abspath(os.path.dirname(__file__))) + + from parser import CreateParser + from matter_idl_types import * import unittest From 199468489f8ad04df470bdafecc1af70b17f9494 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Thu, 20 Jan 2022 10:47:39 -0500 Subject: [PATCH 13/18] Rename "parser" to a more specific name: the name parser is used in python and is too generic to attempt a top level import on it --- scripts/idl/BUILD.gn | 4 ++-- scripts/idl/{parser.py => matter_idl_parser.py} | 0 scripts/idl/{test_parsing.py => test_matter_idl_parser.py} | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename scripts/idl/{parser.py => matter_idl_parser.py} (100%) rename scripts/idl/{test_parsing.py => test_matter_idl_parser.py} (98%) diff --git a/scripts/idl/BUILD.gn b/scripts/idl/BUILD.gn index 00166c136659e7..8e83489c058ca9 100644 --- a/scripts/idl/BUILD.gn +++ b/scripts/idl/BUILD.gn @@ -28,8 +28,8 @@ pw_python_package("idl") { sources = [ "__init__.py", "matter_idl_types.py", - "parser.py", + "matter_idl_parser.py", ] - tests = [ "test_parsing.py" ] + tests = [ "test_matter_idl_parser.py" ] } diff --git a/scripts/idl/parser.py b/scripts/idl/matter_idl_parser.py similarity index 100% rename from scripts/idl/parser.py rename to scripts/idl/matter_idl_parser.py diff --git a/scripts/idl/test_parsing.py b/scripts/idl/test_matter_idl_parser.py similarity index 98% rename from scripts/idl/test_parsing.py rename to scripts/idl/test_matter_idl_parser.py index 016684b19fcfd7..23e1d691f9d81b 100755 --- a/scripts/idl/test_parsing.py +++ b/scripts/idl/test_matter_idl_parser.py @@ -15,14 +15,14 @@ # limitations under the License. try: - from .parser import CreateParser + from .matter_idl_parser import CreateParser from .matter_idl_types import * except: import os import sys sys.path.append(os.path.abspath(os.path.dirname(__file__))) - from parser import CreateParser + from matter_idl_parser import CreateParser from matter_idl_types import * import unittest From d8945ef998db75ec4966c00a53d1dbed04094da6 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Thu, 20 Jan 2022 16:36:00 -0500 Subject: [PATCH 14/18] Restyle fixes --- scripts/idl/BUILD.gn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/idl/BUILD.gn b/scripts/idl/BUILD.gn index 8e83489c058ca9..c771fa4cf6bfb1 100644 --- a/scripts/idl/BUILD.gn +++ b/scripts/idl/BUILD.gn @@ -27,8 +27,8 @@ pw_python_package("idl") { sources = [ "__init__.py", - "matter_idl_types.py", "matter_idl_parser.py", + "matter_idl_types.py", ] tests = [ "test_matter_idl_parser.py" ] From b4408df74696184e4227ca4a115206d969466f24 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Thu, 20 Jan 2022 17:42:15 -0500 Subject: [PATCH 15/18] Add support for global tag parsing after idl updated in master --- scripts/idl/matter_grammar.lark | 5 +++-- scripts/idl/matter_idl_parser.py | 22 +++++++++++++--------- scripts/idl/matter_idl_types.py | 21 +++++++++++++++++---- scripts/idl/test_matter_idl_parser.py | 10 ++++++++-- 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/scripts/idl/matter_grammar.lark b/scripts/idl/matter_grammar.lark index cbaf0c0249d4f8..335a853b0ce8a4 100644 --- a/scripts/idl/matter_grammar.lark +++ b/scripts/idl/matter_grammar.lark @@ -7,8 +7,9 @@ event: event_priority "event"i id "=" number "{" struct_field* "}" | "info"i -> info_priority | "debug"i -> debug_priority -attribute: attribute_access? "attribute"i field -attribute_access: "readonly"i -> readonly +attribute: attribute_tag* "attribute"i field +attribute_tag: "readonly"i -> attr_readonly + | "global"i -> attr_global request_struct: "request"i struct response_struct: "response"i struct diff --git a/scripts/idl/matter_idl_parser.py b/scripts/idl/matter_idl_parser.py index e8acaee9cff020..1ade381f5b2dc7 100755 --- a/scripts/idl/matter_idl_parser.py +++ b/scripts/idl/matter_idl_parser.py @@ -67,11 +67,11 @@ def optional(self, _): def nullable(self, _): return FieldAttribute.NULLABLE - def readonly(self, _): - return AttributeAccess.READONLY + def attr_readonly(self, _): + return AttributeTag.READABLE - def writable(self, _): - return AttributeAccess.READWRITE + def attr_global(self, _): + return AttributeTag.GLOBAL def critical_priority(self, _): return EventPriority.CRITICAL @@ -113,11 +113,15 @@ def event(self, args): return Event(priority=args[0], name=args[1], code=args[2], fields=args[3:], ) def attribute(self, args): - access = AttributeAccess.READWRITE # default - if len(args) > 1: - access = args[0] - - return Attribute(access=access, definition=args[-1]) + tags = set(args[:-1]) + # until we support write only (and need a bit of a reshuffle) + # if the 'attr_readonly == READABLE' is not in the list, we make things + # read/write + if AttributeTag.READABLE not in tags: + tags.add(AttributeTag.READABLE) + tags.add(AttributeTag.WRITABLE) + + return Attribute(definition=args[-1], tags=tags) @v_args(inline=True) def struct(self, id, *fields): diff --git a/scripts/idl/matter_idl_types.py b/scripts/idl/matter_idl_types.py index 89dd63b0ad614c..7ebc5b7cadb25e 100644 --- a/scripts/idl/matter_idl_types.py +++ b/scripts/idl/matter_idl_types.py @@ -9,9 +9,10 @@ class FieldAttribute(enum.Enum): NULLABLE = enum.auto() -class AttributeAccess(enum.Enum): - READONLY = enum.auto() - READWRITE = enum.auto() +class AttributeTag(enum.Enum): + READABLE = enum.auto() + WRITABLE = enum.auto() + GLOBAL = enum.auto() class EventPriority(enum.Enum): @@ -46,8 +47,20 @@ class Field: @dataclass class Attribute: - access: AttributeAccess definition: Field + tags: Set[AttributeTag] = field(default_factory=set()) + + @property + def is_readable(self): + return AttributeTag.READABLE in self.tags + + @property + def is_writable(self): + return AttributeTag.WRITABLE in self.tags + + @property + def is_global(self): + return AttributeTag.GLOBAL in self.tags @dataclass diff --git a/scripts/idl/test_matter_idl_parser.py b/scripts/idl/test_matter_idl_parser.py index 23e1d691f9d81b..19c6d676364750 100755 --- a/scripts/idl/test_matter_idl_parser.py +++ b/scripts/idl/test_matter_idl_parser.py @@ -91,6 +91,8 @@ def test_cluster_attribute(self): server cluster MyCluster = 0x321 { readonly attribute int8u roAttr = 1; attribute int32u rwAttr[] = 123; + global attribute int32u grwAttr[] = 124; + readonly global attribute int32u groAttr[] = 125; } """) @@ -99,10 +101,14 @@ def test_cluster_attribute(self): name="MyCluster", code=0x321, attributes=[ - Attribute(access=AttributeAccess.READONLY, definition=Field( + Attribute(tags=set([AttributeTag.READABLE]), definition=Field( data_type="int8u", code=1, name="roAttr")), - Attribute(access=AttributeAccess.READWRITE, definition=Field( + Attribute(tags=set([AttributeTag.READABLE, AttributeTag.WRITABLE]), definition=Field( data_type="int32u", code=123, name="rwAttr", is_list=True)), + Attribute(tags=set([AttributeTag.GLOBAL, AttributeTag.READABLE, AttributeTag.WRITABLE]), definition=Field( + data_type="int32u", code=124, name="grwAttr", is_list=True)), + Attribute(tags=set([AttributeTag.GLOBAL, AttributeTag.READABLE]), definition=Field( + data_type="int32u", code=125, name="groAttr", is_list=True)), ] )]) self.assertEqual(actual, expected) From af2e1ef31749984dc7c054850f5dfb17666d021b Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Thu, 20 Jan 2022 22:29:25 -0500 Subject: [PATCH 16/18] Add support for datatype sizes and unit tests --- scripts/idl/matter_grammar.lark | 6 ++-- scripts/idl/matter_idl_parser.py | 9 ++++++ scripts/idl/matter_idl_types.py | 12 ++++++-- scripts/idl/test_matter_idl_parser.py | 41 ++++++++++++++++++++------- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/scripts/idl/matter_grammar.lark b/scripts/idl/matter_grammar.lark index 335a853b0ce8a4..b7c388bcb9c061 100644 --- a/scripts/idl/matter_grammar.lark +++ b/scripts/idl/matter_grammar.lark @@ -1,5 +1,5 @@ struct: "struct"i id "{" struct_field* "}" -enum: "enum"i id ":" type "{" enum_entry* "}" +enum: "enum"i id ":" data_type "{" enum_entry* "}" event: event_priority "event"i id "=" number "{" struct_field* "}" @@ -34,9 +34,11 @@ struct_field: member_attribute* field member_attribute: "optional"i -> optional | "nullable"i -> nullable -field: type id list_marker? "=" number ";" +field: data_type id list_marker? "=" number ";" list_marker: "[" "]" +data_type: type ("<" number ">")? + id: ID type: ID diff --git a/scripts/idl/matter_idl_parser.py b/scripts/idl/matter_idl_parser.py index 1ade381f5b2dc7..29b71d0caa62fb 100755 --- a/scripts/idl/matter_idl_parser.py +++ b/scripts/idl/matter_idl_parser.py @@ -46,6 +46,15 @@ def type(self, tokens): raise Error("Unexpected argument counts") return tokens[0].value + def data_type(self, tokens): + if len(tokens) == 1: + return DataType(name=tokens[0]) + # Just a string for data type + elif len(tokens) == 2: + return DataType(name=tokens[0], max_length=tokens[1]) + else: + raise Error("Unexpected size for data type") + @v_args(inline=True) def enum_entry(self, id, number): return EnumEntry(name=id, code=number) diff --git a/scripts/idl/matter_idl_types.py b/scripts/idl/matter_idl_types.py index 7ebc5b7cadb25e..587a45232068ba 100644 --- a/scripts/idl/matter_idl_types.py +++ b/scripts/idl/matter_idl_types.py @@ -1,7 +1,7 @@ import enum from dataclasses import dataclass, field -from typing import List, Set +from typing import List, Set, Union class FieldAttribute(enum.Enum): @@ -36,6 +36,14 @@ class EndpointContentType(enum.Enum): CLIENT_BINDING = enum.auto() +@dataclass +class DataType: + name: str + + # Applies for strings (char or binary) + max_length: Union[int, None] = None + + @dataclass class Field: data_type: str @@ -67,7 +75,7 @@ def is_global(self): class Struct: name: str fields: List[Field] - tag: StructTag = None + tag: Union[StructTag, None] = None @dataclass diff --git a/scripts/idl/test_matter_idl_parser.py b/scripts/idl/test_matter_idl_parser.py index 19c6d676364750..6acabea02956fd 100755 --- a/scripts/idl/test_matter_idl_parser.py +++ b/scripts/idl/test_matter_idl_parser.py @@ -56,7 +56,7 @@ def test_global_enum(self): """) expected = Idl(enums=[ - Enum(name='GlobalEnum', base_type='ENUM8', + Enum(name='GlobalEnum', base_type=DataType(name='ENUM8'), entries=[ EnumEntry(name="kValue1", code=1), EnumEntry(name="kOther", code=0x12), @@ -77,10 +77,10 @@ def test_global_struct(self): Struct(name='Something', fields=[ Field( - data_type="CHAR_STRING", code=1, name="astring", ), - Field(data_type="CLUSTER_ID", code=2, name="idlist", is_list=True, attributes=set( + data_type=DataType(name="CHAR_STRING"), code=1, name="astring", ), + Field(data_type=DataType(name="CLUSTER_ID"), code=2, name="idlist", is_list=True, attributes=set( [FieldAttribute.OPTIONAL])), - Field(data_type="int", code=0x123, name="valueThatIsNullable", attributes=set( + Field(data_type=DataType(name="int"), code=0x123, name="valueThatIsNullable", attributes=set( [FieldAttribute.NULLABLE])), ])] ) @@ -102,13 +102,34 @@ def test_cluster_attribute(self): code=0x321, attributes=[ Attribute(tags=set([AttributeTag.READABLE]), definition=Field( - data_type="int8u", code=1, name="roAttr")), + data_type=DataType(name="int8u"), code=1, name="roAttr")), Attribute(tags=set([AttributeTag.READABLE, AttributeTag.WRITABLE]), definition=Field( - data_type="int32u", code=123, name="rwAttr", is_list=True)), + data_type=DataType(name="int32u"), code=123, name="rwAttr", is_list=True)), Attribute(tags=set([AttributeTag.GLOBAL, AttributeTag.READABLE, AttributeTag.WRITABLE]), definition=Field( - data_type="int32u", code=124, name="grwAttr", is_list=True)), + data_type=DataType(name="int32u"), code=124, name="grwAttr", is_list=True)), Attribute(tags=set([AttributeTag.GLOBAL, AttributeTag.READABLE]), definition=Field( - data_type="int32u", code=125, name="groAttr", is_list=True)), + data_type=DataType(name="int32u"), code=125, name="groAttr", is_list=True)), + ] + )]) + self.assertEqual(actual, expected) + + def test_sized_attribute(self): + actual = parseText(""" + server cluster MyCluster = 1 { + attribute char_string<11> attr1 = 1; + attribute octet_string<33> attr2[] = 2; + } + """) + + expected = Idl(clusters=[ + Cluster(side=ClusterSide.SERVER, + name="MyCluster", + code=1, + attributes=[ + Attribute(tags=set([AttributeTag.READABLE, AttributeTag.WRITABLE]), definition=Field( + data_type=DataType(name="char_string", max_length=11), code=1, name="attr1")), + Attribute(tags=set([AttributeTag.READABLE, AttributeTag.WRITABLE]), definition=Field( + data_type=DataType(name="octet_string", max_length=33), code=2, name="attr2", is_list=True)), ] )]) self.assertEqual(actual, expected) @@ -158,7 +179,7 @@ def test_cluster_enum(self): name="WithEnums", code=0xab, enums=[ - Enum(name="TestEnum", base_type="ENUM16", + Enum(name="TestEnum", base_type=DataType(name="ENUM16"), entries=[ EnumEntry(name="A", code=0x123), EnumEntry(name="B", code=0x234), @@ -182,7 +203,7 @@ def test_cluster_events(self): code=0x123, events=[ Event(priority=EventPriority.CRITICAL, name="StartUp", code=0, fields=[ - Field(data_type="INT32U", + Field(data_type=DataType(name="INT32U"), code=0, name="softwareVersion"), ]), Event(priority=EventPriority.INFO, From 313d4eefaeebde3b9147257ad510c199085f626a Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Thu, 20 Jan 2022 22:30:48 -0500 Subject: [PATCH 17/18] Add test for sized strings in structs as well --- scripts/idl/test_matter_idl_parser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/idl/test_matter_idl_parser.py b/scripts/idl/test_matter_idl_parser.py index 6acabea02956fd..77afb091775583 100755 --- a/scripts/idl/test_matter_idl_parser.py +++ b/scripts/idl/test_matter_idl_parser.py @@ -70,6 +70,7 @@ def test_global_struct(self): CHAR_STRING astring = 1; optional CLUSTER_ID idlist[] = 2; nullable int valueThatIsNullable = 0x123; + char_string<123> sized_string = 222; } """) @@ -82,6 +83,7 @@ def test_global_struct(self): [FieldAttribute.OPTIONAL])), Field(data_type=DataType(name="int"), code=0x123, name="valueThatIsNullable", attributes=set( [FieldAttribute.NULLABLE])), + Field(data_type=DataType(name="char_string", max_length=123), code=222, name="sized_string", attributes=set()), ])] ) self.assertEqual(actual, expected) From 3795a0dc4db0881f556c7792d57f696a1c225e40 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Thu, 20 Jan 2022 22:31:58 -0500 Subject: [PATCH 18/18] Ran restyler --- scripts/idl/test_matter_idl_parser.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/idl/test_matter_idl_parser.py b/scripts/idl/test_matter_idl_parser.py index 77afb091775583..7d841f09c51bb1 100755 --- a/scripts/idl/test_matter_idl_parser.py +++ b/scripts/idl/test_matter_idl_parser.py @@ -60,7 +60,7 @@ def test_global_enum(self): entries=[ EnumEntry(name="kValue1", code=1), EnumEntry(name="kOther", code=0x12), - ])] + ])] ) self.assertEqual(actual, expected) @@ -83,7 +83,8 @@ def test_global_struct(self): [FieldAttribute.OPTIONAL])), Field(data_type=DataType(name="int"), code=0x123, name="valueThatIsNullable", attributes=set( [FieldAttribute.NULLABLE])), - Field(data_type=DataType(name="char_string", max_length=123), code=222, name="sized_string", attributes=set()), + Field(data_type=DataType(name="char_string", max_length=123), + code=222, name="sized_string", attributes=set()), ])] ) self.assertEqual(actual, expected) @@ -185,7 +186,7 @@ def test_cluster_enum(self): entries=[ EnumEntry(name="A", code=0x123), EnumEntry(name="B", code=0x234), - ])], + ])], )]) self.assertEqual(actual, expected) @@ -206,7 +207,7 @@ def test_cluster_events(self): events=[ Event(priority=EventPriority.CRITICAL, name="StartUp", code=0, fields=[ Field(data_type=DataType(name="INT32U"), - code=0, name="softwareVersion"), + code=0, name="softwareVersion"), ]), Event(priority=EventPriority.INFO, name="Hello", code=1, fields=[]),