From 5093f23b650a5c159c0569a71a989a6cdaee6e89 Mon Sep 17 00:00:00 2001 From: Justin Wood Date: Mon, 18 Jul 2022 16:50:36 -0700 Subject: [PATCH] Fabric-scoping support in IDL parsing (#20819) (#20878) * Add fabric scope support in IDL for commands * Add support for fabric scoped attributes. Due to ambiguity of fabric_scoped, moved to earley parser instead of lalr. Potentially somewhat slower, but likely still fast enough * Switch back to lalr parser because it is much faster * Doc updates and one more test * Fix command id in examples * Use common keyword of fabric after updating grammar a bit * Fix extra rule * Restyle Co-authored-by: Andrei Litvin --- scripts/idl/README.md | 12 +++++- scripts/idl/matter_grammar.lark | 9 +++-- scripts/idl/matter_idl_parser.py | 55 ++++++++++++++++++++++----- scripts/idl/matter_idl_types.py | 2 + scripts/idl/test_matter_idl_parser.py | 17 +++++++++ 5 files changed, 82 insertions(+), 13 deletions(-) diff --git a/scripts/idl/README.md b/scripts/idl/README.md index 3e67ba20d90724..42a1a62ae54c5a 100644 --- a/scripts/idl/README.md +++ b/scripts/idl/README.md @@ -94,6 +94,10 @@ server cluster AccessControl = 31 { // These defaults can be modified to any of view/operate/manage/administer roles. attribute access(read: manage, write: administer) int32u customAcl = 3; + // Attributes may be fabric-scoped as well by tagging them as `fabric`. + fabric readonly attribute int16u myFabricAttr = 22; + fabric attribute(read: view, write: administer) int16u someFabricRWAttribute = 33; + // attributes may be read-only as well readonly attribute int16u clusterRevision = 65533; @@ -118,7 +122,13 @@ server cluster AccessControl = 31 { command access(invoke: administer) Off(): DefaultSuccess = 4; // command invocation can require timed invoke usage - timed command RequiresTimedInvok(): DefaultSuccess = 4; + timed command RequiresTimedInvok(): DefaultSuccess = 5; + + // commands may be fabric scoped + fabric command RequiresTimedInvok(): DefaultSuccess = 6; + + // commands may have multiple attributes + fabric timed command RequiresTimedInvok(): DefaultSuccess = 7; } // A client cluster represents something that is used by an app diff --git a/scripts/idl/matter_grammar.lark b/scripts/idl/matter_grammar.lark index 3a31d6df0ece60..eb7afe993e9e61 100644 --- a/scripts/idl/matter_grammar.lark +++ b/scripts/idl/matter_grammar.lark @@ -26,9 +26,13 @@ attribute_access: "access"i "(" (attribute_access_entry ("," attribute_access_en attribute_with_access: attribute_access? struct_field -attribute: attribute_tag* "attribute"i attribute_with_access ";" +shared_tag: "fabric"i -> shared_tag_fabric +shared_tags: shared_tag* -> shared_tags + +attribute: shared_tags attribute_tags "attribute"i attribute_with_access ";" attribute_tag: "readonly"i -> attr_readonly | "nosubscribe"i -> attr_nosubscribe +attribute_tags: attribute_tag* -> attribute_tags request_struct: "request"i struct @@ -38,12 +42,11 @@ response_struct: "response"i "struct"i id "=" positive_integer "{" (struct_field command_attribute: "timed"i -> timed_command command_attributes: command_attribute* - command_access: "access"i "(" ("invoke"i ":" access_privilege)? ")" command_with_access: "command"i command_access? id -command: command_attributes command_with_access "(" id? ")" ":" id "=" positive_integer ";" +command: shared_tags command_attributes command_with_access "(" id? ")" ":" id "=" positive_integer ";" cluster: cluster_side "cluster"i id "=" positive_integer "{" (enum|bitmap|event|attribute|struct|request_struct|response_struct|command)* "}" ?cluster_side: "server"i -> server_cluster diff --git a/scripts/idl/matter_idl_parser.py b/scripts/idl/matter_idl_parser.py index 89c73d76966dd8..643505c059bad5 100755 --- a/scripts/idl/matter_idl_parser.py +++ b/scripts/idl/matter_idl_parser.py @@ -1,5 +1,6 @@ #!/usr/bin/env python +import enum import logging from lark import Lark @@ -15,6 +16,10 @@ from matter_idl_types import * +class SharedTag(enum.Enum): + FABRIC_SCOPED = enum.auto() + + class AddServerClusterToEndpointTransform: """Provides an 'apply' method that can be run on endpoints to add a server cluster to the given endpoint. @@ -128,6 +133,12 @@ def data_type(self, tokens): else: raise Error("Unexpected size for data type") + def shared_tag_fabric(self, _): + return SharedTag.FABRIC_SCOPED + + def shared_tags(self, entries): + return entries + @v_args(inline=True) def constant_entry(self, id, number): return ConstantEntry(name=id, code=number) @@ -159,6 +170,9 @@ def attr_readonly(self, _): def attr_nosubscribe(self, _): return AttributeTag.NOSUBSCRIBE + def attribute_tags(self, tags): + return tags + def critical_priority(self, _): return EventPriority.CRITICAL @@ -204,14 +218,23 @@ def command_with_access(self, args): return init_args def command(self, args): - # A command has 4 arguments if no input or - # 5 arguments if input parameter is available - param_in = None - if len(args) > 4: - param_in = args[2] + # The command takes 5 arguments if no input argument, 6 if input + # argument is provided + if len(args) != 6: + args.insert(3, None) + + attr = args[1] # direct command attributes + for shared_attr in args[0]: + if shared_attr == SharedTag.FABRIC_SCOPED: + attr.add(CommandAttribute.FABRIC_SCOPED) + else: + raise Exception("Unknown shared tag: %r" % shared_attr) return Command( - attributes=args[0], input_param=param_in, output_param=args[-2], code=args[-1], **args[1]) + attributes=attr, + input_param=args[3], output_param=args[4], code=args[5], + **args[2] + ) def event_access(self, privilege): return privilege[0] @@ -291,9 +314,17 @@ def ESCAPED_STRING(self, s): # handle escapes, skip the start and end quotes return s.value[1:-1].encode('utf-8').decode('unicode-escape') - def attribute(self, args): - tags = set(args[:-1]) - (definition, acl) = args[-1] + @v_args(inline=True) + def attribute(self, shared_tags, tags, definition_tuple): + + tags = set(tags) + (definition, acl) = definition_tuple + + for shared_attr in shared_tags: + if shared_attr == SharedTag.FABRIC_SCOPED: + tags.add(AttributeTag.FABRIC_SCOPED) + else: + raise Exception("Unknown shared tag: %r" % shared_attr) # 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 @@ -396,6 +427,12 @@ def CreateParser(skip_meta: bool = False): """ Generates a parser that will process a ".matter" file into a IDL """ + + # NOTE: LALR parser is fast. While Earley could parse more ambigous grammars, + # earley is much slower: + # - 0.39s LALR parsing of all-clusters-app.matter + # - 2.26s Earley parsing of the same thing. + # For this reason, every attempt should be made to make the grammar context free return ParserWithLines(Lark.open('matter_grammar.lark', rel_to=__file__, start='idl', parser='lalr', propagate_positions=True), skip_meta) diff --git a/scripts/idl/matter_idl_types.py b/scripts/idl/matter_idl_types.py index b2d0e1cfda71d1..ca2b4d955c0be4 100644 --- a/scripts/idl/matter_idl_types.py +++ b/scripts/idl/matter_idl_types.py @@ -28,12 +28,14 @@ class FieldAttribute(enum.Enum): class CommandAttribute(enum.Enum): TIMED_INVOKE = enum.auto() + FABRIC_SCOPED = enum.auto() class AttributeTag(enum.Enum): READABLE = enum.auto() WRITABLE = enum.auto() NOSUBSCRIBE = enum.auto() + FABRIC_SCOPED = enum.auto() class AttributeStorage(enum.Enum): diff --git a/scripts/idl/test_matter_idl_parser.py b/scripts/idl/test_matter_idl_parser.py index 90cfa4da46c6ee..6f4e9faa2fb41b 100755 --- a/scripts/idl/test_matter_idl_parser.py +++ b/scripts/idl/test_matter_idl_parser.py @@ -96,6 +96,7 @@ def test_cluster_attribute(self): attribute int32u rwAttr[] = 123; readonly nosubscribe attribute int8s nosub[] = 0xaa; readonly attribute nullable int8s isNullable = 0xab; + fabric readonly attribute int8s fabric_attr = 0x1234; } """) @@ -112,6 +113,8 @@ def test_cluster_attribute(self): data_type=DataType(name="int8s"), code=0xAA, name="nosub", is_list=True)), Attribute(tags=set([AttributeTag.READABLE]), definition=Field( data_type=DataType(name="int8s"), code=0xAB, name="isNullable", attributes=set([FieldAttribute.NULLABLE]))), + Attribute(tags=set([AttributeTag.READABLE, AttributeTag.FABRIC_SCOPED]), definition=Field( + data_type=DataType(name="int8s"), code=0x1234, name="fabric_attr")) ] )]) self.assertEqual(actual, expected) @@ -145,6 +148,7 @@ def test_attribute_access(self): attribute access(read: manage) int8s attr3 = 3; attribute access(write: administer) int8s attr4 = 4; attribute access(read: operate, write: manage) int8s attr5 = 5; + fabric attribute access(read: view, write: administer) int16u attr6 = 6; } """) @@ -176,6 +180,11 @@ def test_attribute_access(self): readacl=AccessPrivilege.OPERATE, writeacl=AccessPrivilege.MANAGE ), + Attribute(tags=set([AttributeTag.READABLE, AttributeTag.WRITABLE, AttributeTag.FABRIC_SCOPED]), definition=Field( + data_type=DataType(name="int16u"), code=6, name="attr6"), + readacl=AccessPrivilege.VIEW, + writeacl=AccessPrivilege.ADMINISTER + ), ] )]) self.assertEqual(actual, expected) @@ -190,6 +199,8 @@ def test_cluster_commands(self): command WithoutArg(): DefaultSuccess = 123; command InOutStuff(InParam): OutParam = 222; timed command TimedCommand(InParam): DefaultSuccess = 0xab; + fabric command FabricScopedCommand(InParam): DefaultSuccess = 0xac; + fabric Timed command FabricScopedTimedCommand(InParam): DefaultSuccess = 0xad; } """) expected = Idl(clusters=[ @@ -210,6 +221,12 @@ def test_cluster_commands(self): Command(name="TimedCommand", code=0xab, input_param="InParam", output_param="DefaultSuccess", attributes=set([CommandAttribute.TIMED_INVOKE])), + Command(name="FabricScopedCommand", code=0xac, + input_param="InParam", output_param="DefaultSuccess", + attributes=set([CommandAttribute.FABRIC_SCOPED])), + Command(name="FabricScopedTimedCommand", code=0xad, + input_param="InParam", output_param="DefaultSuccess", + attributes=set([CommandAttribute.TIMED_INVOKE, CommandAttribute.FABRIC_SCOPED])), ], )]) self.assertEqual(actual, expected)