vppapigen:  add parser support for enumflags

Type: improvement

Change-Id: I0f15862cc8399a4f7c8a81fe44ba8b27d8772278
Signed-off-by: Paul Vinciguerra <pvinci@vinciconsulting.com>
Signed-off-by: Ole Troan <ot@cisco.com>
(cherry picked from commit e15523297bb3905f2e0eef4272fc69a8a92463cc)
diff --git a/src/tools/vppapigen/test_vppapigen.py b/src/tools/vppapigen/test_vppapigen.py
index 999addf..c454ffc 100755
--- a/src/tools/vppapigen/test_vppapigen.py
+++ b/src/tools/vppapigen/test_vppapigen.py
@@ -1,9 +1,11 @@
 #!/usr/bin/env python3
 
 import unittest
-from vppapigen import VPPAPI, Option, ParseError, Union, foldup_crcs, global_types
+from vppapigen import VPPAPI, Option, ParseError, Union, foldup_crcs, \
+    global_types
 import vppapigen
 
+
 # TODO
 # - test parsing of options, typedefs, enums, defines
 # - test JSON, C output
@@ -19,6 +21,7 @@
         r = self.parser.parse_string(version_string)
         self.assertTrue(isinstance(r[0], Option))
 
+
 class TestUnion(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
@@ -49,7 +52,6 @@
         self.assertTrue(r[0].vla)
         s = self.parser.process(r)
 
-
         test_string2 = '''
         union foo_union_vla2 {
         u32 a;
@@ -74,6 +76,7 @@
         '''
         self.assertRaises(ValueError, self.parser.parse_string, test_string3)
 
+
 class TestTypedef(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
@@ -169,7 +172,7 @@
         '''
         crc = get_crc(test_string, 'foo')
 
-        # modify underlaying type
+        # modify underlying type
         test_string = '''
          typedef list { u8 foo2; };
          autoreply define foo { u8 foo;  vl_api_list_t l;};
@@ -255,5 +258,97 @@
 
         self.assertNotEqual(crc, crc2)
 
+
+class TestEnum(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.parser = VPPAPI()
+
+    def test_enum_as_enum(self):
+        test_string = """\
+enum tunnel_mode : u8
+{
+  /** point-to-point */
+  TUNNEL_API_MODE_P2P = 0,
+  /** multi-point */
+  TUNNEL_API_MODE_MP,
+};
+"""
+        r = self.parser.parse_string(test_string)
+        self.assertIsNotNone(r)
+        s = self.parser.process(r)
+        for o in s['types']:
+            if o.type == 'Enum':
+                self.assertEqual(o.name, "tunnel_mode")
+                break
+        else:
+            self.fail()
+
+    def test_enumflag_as_enum(self):
+        test_string = """\
+enum virtio_flags {
+        VIRTIO_API_FLAG_GSO = 1, /* enable gso on the interface */
+        VIRTIO_API_FLAG_CSUM_OFFLOAD = 2, /* enable checksum offload without gso on the interface */
+        VIRTIO_API_FLAG_GRO_COALESCE = 4, /* enable packet coalescing on tx side, provided gso enabled */
+        VIRTIO_API_FLAG_PACKED = 8, /* enable packed ring support, provided it is available from backend */
+        VIRTIO_API_FLAG_IN_ORDER = 16, /* enable in order support, provided it is available from backend */
+        VIRTIO_API_FLAG_BUFFERING = 32 [backwards_compatible], /* enable buffering to handle backend jitter/delays */
+};"""
+        r = self.parser.parse_string(test_string)
+        self.assertIsNotNone(r)
+        s = self.parser.process(r)
+        for o in s['types']:
+            if o.type == 'Enum':
+                self.assertEqual(o.name, "virtio_flags")
+                break
+        else:
+            self.fail()
+
+
+class TestEnumFlag(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.parser = VPPAPI()
+
+    def test_enum_as_enumflag(self):
+        test_string = """\
+enumflag tunnel_mode_ef : u8
+{
+  /** point-to-point */
+  TUNNEL_API_MODE_P2P = 0,
+  /** multi-point */
+  TUNNEL_API_MODE_MP,
+  TUNNEL_API_MODE_FOO,
+  TUNNEL_API_MODE_BAR,
+};"""
+        with self.assertRaises(TypeError) as ctx:
+            r = self.parser.parse_string(test_string)
+
+        self.assertTrue(str(ctx.exception).startswith(
+            'tunnel_mode_ef is not a flag enum.'))
+
+    def test_enumflag_as_enumflag(self):
+        test_string = """\
+enumflag virtio_flags_ef {
+        VIRTIO_API_FLAG_GSO = 1, /* enable gso on the interface */
+        VIRTIO_API_FLAG_CSUM_OFFLOAD = 2, /* enable checksum offload without gso on the interface */
+        VIRTIO_API_FLAG_GRO_COALESCE = 4, /* enable packet coalescing on tx side, provided gso enabled */
+        VIRTIO_API_FLAG_PACKED = 8, /* enable packed ring support, provided it is available from backend */
+        VIRTIO_API_FLAG_IN_ORDER = 16, /* enable in order support, provided it is available from backend */
+        VIRTIO_API_FLAG_BUFFERING = 32 [backwards_compatible], /* enable buffering to handle backend jitter/delays */
+};"""
+        r = self.parser.parse_string(test_string)
+        self.assertIsNotNone(r)
+        s = self.parser.process(r)
+        for o in s['types']:
+            if o.type == 'EnumFlag':
+                self.assertEqual(o.name, "virtio_flags_ef")
+                break
+        else:
+            self.fail()
+
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/src/tools/vppapigen/vppapigen.py b/src/tools/vppapigen/vppapigen.py
index da00823..b80dd4d 100755
--- a/src/tools/vppapigen/vppapigen.py
+++ b/src/tools/vppapigen/vppapigen.py
@@ -58,6 +58,7 @@
         'define': 'DEFINE',
         'typedef': 'TYPEDEF',
         'enum': 'ENUM',
+        'enumflag': 'ENUMFLAG',
         'typeonly': 'TYPEONLY',
         'manual_print': 'MANUAL_PRINT',
         'manual_endian': 'MANUAL_ENDIAN',
@@ -182,7 +183,16 @@
     return vla
 
 
-class Service():
+class Processable:
+    type = "<Invalid>"
+
+    def process(self, result):  # -> Dict
+        result[self.type].append(self)
+
+
+class Service(Processable):
+    type = 'Service'
+
     def __init__(self, caller, reply, events=None, stream_message=None,
                  stream=False):
         self.caller = caller
@@ -192,10 +202,11 @@
         self.events = [] if events is None else events
 
 
-class Typedef():
+class Typedef(Processable):
+    type = 'Typedef'
+
     def __init__(self, name, flags, block):
         self.name = name
-        self.type = 'Typedef'
         self.flags = flags
         self.block = block
         self.crc = str(block).encode()
@@ -211,14 +222,18 @@
         self.vla = vla_is_last_check(name, block)
         vla_mark_length_field(self.block)
 
+    def process(self, result):
+        result['types'].append(self)
+
     def __repr__(self):
         return self.name + str(self.flags) + str(self.block)
 
 
-class Using():
+class Using(Processable):
+    type = 'Using'
+
     def __init__(self, name, flags, alias):
         self.name = name
-        self.type = 'Using'
         self.vla = False
         self.block = []
         self.manual_print = True
@@ -248,13 +263,17 @@
         self.crc = str(self.block).encode()
         global_type_add(name, self)
 
+    def process(self, result):  # -> Dict
+        result['types'].append(self)
+
     def __repr__(self):
         return self.name + str(self.alias)
 
 
-class Union():
+class Union(Processable):
+    type = 'Union'
+
     def __init__(self, name, flags, block):
-        self.type = 'Union'
         self.manual_print = False
         self.manual_endian = False
         self.name = name
@@ -271,14 +290,18 @@
 
         global_type_add(name, self)
 
+    def process(self, result):
+        result['types'].append(self)
+
     def __repr__(self):
         return str(self.block)
 
 
-class Define():
+class Define(Processable):
+    type = 'Define'
+
     def __init__(self, name, flags, block):
         self.name = name
-        self.type = 'Define'
         self.flags = flags
         self.block = block
         self.dont_trace = False
@@ -313,16 +336,31 @@
 
         self.crc = str(block).encode()
 
+    def autoreply_block(self, name, parent):
+        block = [Field('u32', 'context'),
+                 Field('i32', 'retval')]
+        # inherit the parent's options
+        for k, v in parent.options.items():
+            block.append(Option(k, v))
+        return Define(name + '_reply', [], block)
+
+    def process(self, result):  # -> Dict
+        tname = self.__class__.__name__
+        result[tname].append(self)
+        if self.autoreply:
+            result[tname].append(self.autoreply_block(self.name, self))
+
     def __repr__(self):
         return self.name + str(self.flags) + str(self.block)
 
 
-class Enum():
+class Enum(Processable):
+    type = 'Enum'
+
     def __init__(self, name, block, enumtype='u32'):
         self.name = name
         self.enumtype = enumtype
         self.vla = False
-        self.type = 'Enum'
         self.manual_print = False
 
         count = 0
@@ -350,12 +388,30 @@
         self.crc = str(block3).encode()
         global_type_add(name, self)
 
+    def process(self, result):
+        result['types'].append(self)
+
     def __repr__(self):
         return self.name + str(self.block)
 
 
-class Import():
+class EnumFlag(Enum):
+    type = 'EnumFlag'
+
+    def __init__(self, name, block, enumtype='u32'):
+        super(EnumFlag, self).__init__(name, block, enumtype)
+
+        for b in self.block:
+            if bin(b[1])[2:].count("1") > 1:
+                raise TypeError("%s is not a flag enum.  No element in a "
+                                "flag enum may have more than a "
+                                "single bit set." % self.name)
+
+
+class Import(Processable):
+    type = 'Import'
     _initialized = False
+
     def __new__(cls, *args, **kwargs):
         if args[0] not in seen_imports:
             instance = super().__new__(cls)
@@ -383,13 +439,17 @@
         return self.filename
 
 
-class Option():
+class Option(Processable):
+    type = 'Option'
+
     def __init__(self, option, value=None):
-        self.type = 'Option'
         self.option = option
         self.value = value
         self.crc = str(option).encode()
 
+    def process(self, result):  # -> Dict
+        result[self.type][self.option] = self.value
+
     def __repr__(self):
         return str(self.option)
 
@@ -397,9 +457,10 @@
         return self.option[index]
 
 
-class Array():
+class Array(Processable):
+    type = 'Array'
+
     def __init__(self, fieldtype, name, length, modern_vla=False):
-        self.type = 'Array'
         self.fieldtype = fieldtype
         self.fieldname = name
         self.modern_vla = modern_vla
@@ -417,9 +478,12 @@
                     self.lengthfield])
 
 
-class Field():
+class Field(Processable):
+    type = 'Field'
+
     def __init__(self, fieldtype, name, limit=None):
-        self.type = 'Field'
+        # limit field has been expanded to an options dict.
+
         self.fieldtype = fieldtype
         self.is_lengthfield = False
 
@@ -437,18 +501,28 @@
         return str([self.fieldtype, self.fieldname])
 
 
-class Counter():
+class Counter(Processable):
+    type = 'Counter'
+
     def __init__(self, path, counter):
-        self.type = 'Counter'
         self.name = path
         self.block = counter
 
+    def process(self, result):  # -> Dict
+        result['Counters'].append(self)
 
-class Paths():
+
+class Paths(Processable):
+    type = 'Paths'
+
     def __init__(self, pathset):
-        self.type = 'Paths'
         self.paths = pathset
 
+    def __repr__(self):
+        return "%s(paths=%s)" % (
+            self.__class__.__name__, self.paths
+        )
+
 
 class Coord(object):
     """ Coordinates of a syntactic element. Consists of:
@@ -523,6 +597,7 @@
                 | option
                 | import
                 | enum
+                | enumflag
                 | union
                 | service
                 | paths
@@ -643,6 +718,17 @@
         else:
             p[0] = Enum(p[2], p[4])
 
+    def p_enumflag(self, p):
+        '''enumflag : ENUMFLAG ID '{' enum_statements '}' ';' '''
+        p[0] = EnumFlag(p[2], p[4])
+
+    def p_enumflag_type(self, p):
+        ''' enumflag : ENUMFLAG ID ':' enum_size '{' enum_statements '}' ';' '''  # noqa : E502
+        if len(p) == 9:
+            p[0] = EnumFlag(p[2], p[6], enumtype=p[4])
+        else:
+            p[0] = EnumFlag(p[2], p[4])
+
     def p_enum_size(self, p):
         ''' enum_size : U8
                       | U16
@@ -902,14 +988,6 @@
                 print('File not found: {}'.format(filename), file=sys.stderr)
                 sys.exit(2)
 
-    def autoreply_block(self, name, parent):
-        block = [Field('u32', 'context'),
-                 Field('i32', 'retval')]
-        # inherhit the parent's options
-        for k, v in parent.options.items():
-            block.append(Option(k, v))
-        return Define(name + '_reply', [], block)
-
     def process(self, objs):
         s = {}
         s['Option'] = {}
@@ -921,32 +999,17 @@
         s['Paths'] = []
         crc = 0
         for o in objs:
-            tname = o.__class__.__name__
             try:
                 crc = binascii.crc32(o.crc, crc) & 0xffffffff
             except AttributeError:
                 pass
-            if isinstance(o, Define):
-                s[tname].append(o)
-                if o.autoreply:
-                    s[tname].append(self.autoreply_block(o.name, o))
-            elif isinstance(o, Option):
-                s[tname][o.option] = o.value
-            elif type(o) is list:
+
+            if type(o) is list:
                 for o2 in o:
                     if isinstance(o2, Service):
-                        s['Service'].append(o2)
-            elif isinstance(o, (Enum, Typedef, Union, Using)):
-                s['types'].append(o)
-            elif isinstance(o, Counter):
-                s['Counters'].append(o)
-            elif isinstance(o, Paths):
-                s['Paths'].append(o)
+                        o2.process(s)
             else:
-                if tname not in s:
-                    raise ValueError('Unknown class type: {} {}'
-                                     .format(tname, o))
-                s[tname].append(o)
+                o.process(s)
 
         msgs = {d.name: d for d in s['Define']}
         svcs = {s.caller: s for s in s['Service']}
@@ -1021,7 +1084,7 @@
 
         return s
 
-    def process_imports(self, objs, in_import, result):
+    def process_imports(self, objs, in_import, result):  # -> List
         for o in objs:
             # Only allow the following object types from imported file
             if in_import and not isinstance(o, (Enum, Import, Typedef,
diff --git a/src/tools/vppapigen/vppapigen_c.py b/src/tools/vppapigen/vppapigen_c.py
index 4369dd8..44f86be 100644
--- a/src/tools/vppapigen/vppapigen_c.py
+++ b/src/tools/vppapigen/vppapigen_c.py
@@ -32,6 +32,7 @@
 
 process_imports = False
 
+
 ###############################################################################
 class ToJSON():
     '''Class to generate functions converting from VPP binary API to JSON.'''
@@ -89,7 +90,7 @@
             return 'cJSON_AddBoolToObject', '', False
 
         # Lookup type name check if it's enum
-        if vt.type == 'Enum':
+        if vt.type == 'Enum' or vt.type == 'EnumFlag':
             return '{t}_tojson'.format(t=t), '', True
         return '{t}_tojson'.format(t=t), '&', True
 
@@ -186,6 +187,7 @@
         write('}\n')
 
     _dispatch['Enum'] = print_enum
+    _dispatch['EnumFlag'] = print_enum
 
     def print_typedef(self, o):
         '''Create cJSON (dictionary) object from VPP API typedef'''
@@ -454,6 +456,7 @@
         write('}\n')
 
     _dispatch['Enum'] = print_enum
+    _dispatch['EnumFlag'] = print_enum
 
     def print_typedef(self, o):
         '''Convert from JSON object to VPP API binary representation'''
@@ -845,6 +848,7 @@
         write('    }\n')
 
     _dispatch['Enum'] = print_enum
+    _dispatch['EnumFlag'] = print_enum
 
     def print_obj(self, o, stream):
         '''Entry point'''
@@ -935,7 +939,7 @@
 '''
 
     for t in objs:
-        if t.__class__.__name__ == 'Enum':
+        if t.__class__.__name__ == 'Enum' or t.__class__.__name__ == 'EnumFlag':
             write(signature.format(name=t.name))
             pp.print_enum(t.block, stream)
             write('    return s;\n')
@@ -1071,7 +1075,7 @@
 '''
 
     for t in objs:
-        if t.__class__.__name__ == 'Enum':
+        if t.__class__.__name__ == 'Enum' or t.__class__.__name__ == 'EnumFlag' :
             output += signature.format(name=t.name)
             if t.enumtype in ENDIAN_STRINGS:
                 output += ('    *a = {}(*a);\n'
@@ -1191,7 +1195,7 @@
                       (o.alias['type'], o.name, o.alias['length']))
             else:
                 write('typedef %s vl_api_%s_t;\n' % (o.alias['type'], o.name))
-        elif tname == 'Enum':
+        elif tname == 'Enum' or tname == 'EnumFlag':
             if o.enumtype == 'u32':
                 write("typedef enum {\n")
             else:
diff --git a/src/tools/vppapigen/vppapigen_json.py b/src/tools/vppapigen/vppapigen_json.py
index 19f7d65..93ec21f 100644
--- a/src/tools/vppapigen/vppapigen_json.py
+++ b/src/tools/vppapigen/vppapigen_json.py
@@ -96,6 +96,8 @@
                               if o.__class__.__name__ == 'Union']))
     j['enums'] = (walk_enums([o for o in s['types']
                               if o.__class__.__name__ == 'Enum']))
+    j['enumflags'] = (walk_enums([o for o in s['types']
+                                  if o.__class__.__name__ == 'EnumFlag']))
     j['services'] = walk_services(s['Service'])
     j['options'] = s['Option']
     j['aliases'] = {o.name:o.alias for o in s['types'] if o.__class__.__name__ == 'Using'}