Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python code capable of parsing matter IDL files (including some unit tests) #13725

Merged
merged 23 commits into from
Jan 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0005833
A IDL parser:
andy31415 Jan 19, 2022
894f0ae
one more test
andy31415 Jan 19, 2022
4a9bfb0
minor comment
andy31415 Jan 19, 2022
71eb8b6
make the structs a bit more compact: easier to read
andy31415 Jan 19, 2022
ddf6c2b
one more tests
andy31415 Jan 19, 2022
b2b6f9e
more tests, fixed one bug
andy31415 Jan 19, 2022
fd5dc48
Add unit test for cluster commands
andy31415 Jan 19, 2022
a9f9f0d
Unit test for cluster enums
andy31415 Jan 19, 2022
b0eeb03
Unit test for cluster events
andy31415 Jan 19, 2022
c70a486
Rename "structure_member" to field since that seems easier to type an…
andy31415 Jan 19, 2022
23835e4
Merge branch 'master' into parse_matter_idl
andy31415 Jan 19, 2022
79594c0
Match the newest attribute format for IDLs
andy31415 Jan 19, 2022
31253cd
Merge branch 'master' into parse_matter_idl
andy31415 Jan 20, 2022
4181a79
Allow test_parsing to be run stand alone and hope that this fix also …
andy31415 Jan 20, 2022
1994684
Rename "parser" to a more specific name: the name parser is used in p…
andy31415 Jan 20, 2022
d8945ef
Restyle fixes
andy31415 Jan 20, 2022
64564f5
Merge branch 'master' into parse_matter_idl
andy31415 Jan 20, 2022
b4408df
Add support for global tag parsing after idl updated in master
andy31415 Jan 20, 2022
33e0440
Merge branch 'master' into parse_matter_idl
andy31415 Jan 20, 2022
af2e1ef
Add support for datatype sizes and unit tests
andy31415 Jan 21, 2022
313d4ee
Add test for sized strings in structs as well
andy31415 Jan 21, 2022
3795a0d
Ran restyler
andy31415 Jan 21, 2022
2336fc9
Merge branch 'master' into parse_matter_idl
andy31415 Jan 21, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
}
Expand Down
35 changes: 35 additions & 0 deletions scripts/idl/BUILD.gn
Original file line number Diff line number Diff line change
@@ -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_parser.py",
"matter_idl_types.py",
]

tests = [ "test_matter_idl_parser.py" ]
}
Empty file added scripts/idl/__init__.py
Empty file.
59 changes: 59 additions & 0 deletions scripts/idl/matter_grammar.lark
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
struct: "struct"i id "{" struct_field* "}"
enum: "enum"i id ":" data_type "{" enum_entry* "}"

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_tag* "attribute"i field
attribute_tag: "readonly"i -> attr_readonly
| "global"i -> attr_global

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_field: member_attribute* field

member_attribute: "optional"i -> optional
| "nullable"i -> nullable

field: data_type id list_marker? "=" number ";"
list_marker: "[" "]"

data_type: type ("<" number ">")?

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
240 changes: 240 additions & 0 deletions scripts/idl/matter_idl_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
#!/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

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)

@v_args(inline=True)
def enum(self, id, type, *entries):
return Enum(name=id, base_type=type, entries=list(entries))

def field(self, args):
data_type, name = args[0], args[1]
is_list = (len(args) == 4)
code = args[-1]

return Field(data_type=data_type, name=name, code=code, is_list=is_list)

def optional(self, _):
return FieldAttribute.OPTIONAL

def nullable(self, _):
return FieldAttribute.NULLABLE

def attr_readonly(self, _):
return AttributeTag.READABLE

def attr_global(self, _):
return AttributeTag.GLOBAL

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_field(self, args):
# Last argument is the named_member, the rest
# are attributes
field = args[-1]
field.attributes = set(args[:-1])
return field

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], fields=args[3:], )

def attribute(self, args):
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):
return Struct(name=id, fields=list(fields))

@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(name)
elif t == EndpointContentType.SERVER_CLUSTER:
endpoint.server_clusters.append(name)
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()
Loading