ce05cbe9b3f86cb1a842ad40b473c1d832d97a90
[vpp.git] / src / tools / vppapigen / vppapigen.py
1 #!/usr/bin/env python3
2
3 import sys
4 import argparse
5 import keyword
6 import logging
7 import binascii
8 import os
9 from subprocess import Popen, PIPE
10 import ply.lex as lex
11 import ply.yacc as yacc
12
13 assert sys.version_info >= (3, 5), "Not supported Python version: {}".format(
14     sys.version
15 )
16 log = logging.getLogger("vppapigen")
17
18 # Ensure we don't leave temporary files around
19 sys.dont_write_bytecode = True
20
21 #
22 # VPP API language
23 #
24
25 # Global dictionary of new types (including enums)
26 global_types = {}
27
28 seen_imports = {}
29
30
31 def global_type_add(name, obj):
32     """Add new type to the dictionary of types"""
33     type_name = "vl_api_" + name + "_t"
34     if type_name in global_types:
35         raise KeyError("Attempted redefinition of {!r} with {!r}.".format(name, obj))
36     global_types[type_name] = obj
37
38
39 # All your trace are belong to us!
40 def exception_handler(exception_type, exception, traceback):
41     print("%s: %s" % (exception_type.__name__, exception))
42
43
44 #
45 # Lexer
46 #
47 class VPPAPILexer:
48     def __init__(self, filename):
49         self.filename = filename
50
51     reserved = {
52         "service": "SERVICE",
53         "rpc": "RPC",
54         "returns": "RETURNS",
55         "null": "NULL",
56         "stream": "STREAM",
57         "events": "EVENTS",
58         "define": "DEFINE",
59         "typedef": "TYPEDEF",
60         "enum": "ENUM",
61         "enumflag": "ENUMFLAG",
62         "typeonly": "TYPEONLY",
63         "manual_print": "MANUAL_PRINT",
64         "manual_endian": "MANUAL_ENDIAN",
65         "dont_trace": "DONT_TRACE",
66         "autoreply": "AUTOREPLY",
67         "autoendian": "AUTOENDIAN",
68         "option": "OPTION",
69         "u8": "U8",
70         "u16": "U16",
71         "u32": "U32",
72         "u64": "U64",
73         "i8": "I8",
74         "i16": "I16",
75         "i32": "I32",
76         "i64": "I64",
77         "f64": "F64",
78         "bool": "BOOL",
79         "string": "STRING",
80         "import": "IMPORT",
81         "true": "TRUE",
82         "false": "FALSE",
83         "union": "UNION",
84         "counters": "COUNTERS",
85         "paths": "PATHS",
86         "units": "UNITS",
87         "severity": "SEVERITY",
88         "type": "TYPE",
89         "description": "DESCRIPTION",
90     }
91
92     tokens = ["STRING_LITERAL", "COMMENT", "ID", "NUM"] + list(reserved.values())
93
94     t_ignore_LINE_COMMENT = "//.*"
95
96     def t_FALSE(self, t):
97         r"false"
98         t.value = False
99         return t
100
101     def t_TRUE(self, t):
102         r"false"
103         t.value = True
104         return t
105
106     def t_NUM(self, t):
107         r"0[xX][0-9a-fA-F]+|-?\d+\.?\d*"
108         base = 16 if t.value.startswith("0x") else 10
109         if "." in t.value:
110             t.value = float(t.value)
111         else:
112             t.value = int(t.value, base)
113         return t
114
115     def t_ID(self, t):
116         r"[a-zA-Z_][a-zA-Z_0-9]*"
117         # Check for reserved words
118         t.type = VPPAPILexer.reserved.get(t.value, "ID")
119         return t
120
121     # C string
122     def t_STRING_LITERAL(self, t):
123         r"\"([^\\\n]|(\\.))*?\" "
124         t.value = str(t.value).replace('"', "")
125         return t
126
127     # C or C++ comment (ignore)
128     def t_COMMENT(self, t):
129         r"(/\*(.|\n)*?\*/)|(//.*)"
130         t.lexer.lineno += t.value.count("\n")
131         return t
132
133     # Error handling rule
134     def t_error(self, t):
135         raise ParseError(
136             "Illegal character '{}' ({})"
137             "in {}: line {}".format(
138                 t.value[0], hex(ord(t.value[0])), self.filename, t.lexer.lineno
139             )
140         )
141
142     # Define a rule so we can track line numbers
143     def t_newline(self, t):
144         r"\n+"
145         t.lexer.lineno += len(t.value)
146
147     literals = ":{}[];=.,"
148
149     # A string containing ignored characters (spaces and tabs)
150     t_ignore = " \t"
151
152
153 def vla_mark_length_field(block):
154     if isinstance(block[-1], Array):
155         lengthfield = block[-1].lengthfield
156         for b in block:
157             if b.fieldname == lengthfield:
158                 b.is_lengthfield = True
159
160
161 def vla_is_last_check(name, block):
162     vla = False
163     for i, b in enumerate(block):
164         if isinstance(b, Array) and b.vla:
165             vla = True
166             if i + 1 < len(block):
167                 raise ValueError(
168                     'VLA field "{}" must be the last field in message "{}"'.format(
169                         b.fieldname, name
170                     )
171                 )
172         elif b.fieldtype.startswith("vl_api_"):
173             if global_types[b.fieldtype].vla:
174                 vla = True
175                 if i + 1 < len(block):
176                     raise ValueError(
177                         'VLA field "{}" must be the last '
178                         'field in message "{}"'.format(b.fieldname, name)
179                     )
180         elif b.fieldtype == "string" and b.length == 0:
181             vla = True
182             if i + 1 < len(block):
183                 raise ValueError(
184                     'VLA field "{}" must be the last '
185                     'field in message "{}"'.format(b.fieldname, name)
186                 )
187     return vla
188
189
190 class Processable:
191     type = "<Invalid>"
192
193     def process(self, result):  # -> Dict
194         result[self.type].append(self)
195
196
197 class Service(Processable):
198     type = "Service"
199
200     def __init__(self, caller, reply, events=None, stream_message=None, stream=False):
201         self.caller = caller
202         self.reply = reply
203         self.stream = stream
204         self.stream_message = stream_message
205         self.events = [] if events is None else events
206
207
208 class Typedef(Processable):
209     type = "Typedef"
210
211     def __init__(self, name, flags, block):
212         self.name = name
213         self.flags = flags
214         self.block = block
215         self.crc = str(block).encode()
216         self.manual_print = False
217         self.manual_endian = False
218         for f in flags:
219             if f == "manual_print":
220                 self.manual_print = True
221             elif f == "manual_endian":
222                 self.manual_endian = True
223         global_type_add(name, self)
224
225         self.vla = vla_is_last_check(name, block)
226         vla_mark_length_field(self.block)
227
228     def process(self, result):
229         result["types"].append(self)
230
231     def __repr__(self):
232         return self.name + str(self.flags) + str(self.block)
233
234
235 class Using(Processable):
236     type = "Using"
237
238     def __init__(self, name, flags, alias):
239         self.name = name
240         self.vla = False
241         self.block = []
242         self.manual_print = True
243         self.manual_endian = True
244
245         self.manual_print = False
246         self.manual_endian = False
247         for f in flags:
248             if f == "manual_print":
249                 self.manual_print = True
250             elif f == "manual_endian":
251                 self.manual_endian = True
252
253         if isinstance(alias, Array):
254             a = {"type": alias.fieldtype, "length": alias.length}
255         else:
256             a = {"type": alias.fieldtype}
257         self.alias = a
258         self.using = alias
259
260         #
261         # Should have been:
262         #  self.crc = str(alias).encode()
263         # but to be backwards compatible use the block ([])
264         #
265         self.crc = str(self.block).encode()
266         global_type_add(name, self)
267
268     def process(self, result):  # -> Dict
269         result["types"].append(self)
270
271     def __repr__(self):
272         return self.name + str(self.alias)
273
274
275 class Union(Processable):
276     type = "Union"
277
278     def __init__(self, name, flags, block):
279         self.manual_print = False
280         self.manual_endian = False
281         self.name = name
282
283         for f in flags:
284             if f == "manual_print":
285                 self.manual_print = True
286             elif f == "manual_endian":
287                 self.manual_endian = True
288
289         self.block = block
290         self.crc = str(block).encode()
291         self.vla = vla_is_last_check(name, block)
292
293         global_type_add(name, self)
294
295     def process(self, result):
296         result["types"].append(self)
297
298     def __repr__(self):
299         return str(self.block)
300
301
302 class Define(Processable):
303     type = "Define"
304
305     def __init__(self, name, flags, block, comment=None):
306         self.name = name
307         self.flags = flags
308         self.block = block
309         self.dont_trace = False
310         self.manual_print = False
311         self.manual_endian = False
312         self.autoreply = False
313         self.autoendian = 0
314         self.options = {}
315         self.comment = comment
316         for f in flags:
317             if f == "dont_trace":
318                 self.dont_trace = True
319             elif f == "manual_print":
320                 self.manual_print = True
321             elif f == "manual_endian":
322                 self.manual_endian = True
323             elif f == "autoreply":
324                 self.autoreply = True
325             elif f == "autoendian":
326                 self.autoendian = 1
327
328         remove = []
329         for b in block:
330             if isinstance(b, Option):
331                 self.options[b.option] = b.value
332                 remove.append(b)
333
334         block = [x for x in block if x not in remove]
335         self.block = block
336         self.vla = vla_is_last_check(name, block)
337         vla_mark_length_field(self.block)
338
339         self.crc = str(block).encode()
340
341     def autoreply_block(self, name, parent):
342         block = [Field("u32", "context"), Field("i32", "retval")]
343         # inherit the parent's options
344         for k, v in parent.options.items():
345             block.append(Option(k, v))
346         return Define(name + "_reply", [], block)
347
348     def process(self, result):  # -> Dict
349         tname = self.__class__.__name__
350         result[tname].append(self)
351         if self.autoreply:
352             result[tname].append(self.autoreply_block(self.name, self))
353
354     def __repr__(self):
355         return self.name + str(self.flags) + str(self.block)
356
357
358 class Enum(Processable):
359     type = "Enum"
360
361     def __init__(self, name, block, enumtype="u32"):
362         self.name = name
363         self.enumtype = enumtype
364         self.vla = False
365         self.manual_print = False
366
367         count = -1
368         block2 = []
369         block3 = []
370         bc_set = False
371
372         for b in block:
373             if "value" in b:
374                 count = b["value"]
375             else:
376                 count += 1
377             block2.append([b["id"], count])
378             try:
379                 if b["option"]["backwards_compatible"]:
380                     pass
381                 bc_set = True
382             except KeyError:
383                 block3.append([b["id"], count])
384                 if bc_set:
385                     raise ValueError(
386                         "Backward compatible enum must "
387                         "be last {!r} {!r}".format(name, b["id"])
388                     )
389         self.block = block2
390         self.crc = str(block3).encode()
391         global_type_add(name, self)
392
393     def process(self, result):
394         result["types"].append(self)
395
396     def __repr__(self):
397         return self.name + str(self.block)
398
399
400 class EnumFlag(Enum):
401     type = "EnumFlag"
402
403     def __init__(self, name, block, enumtype="u32"):
404         super(EnumFlag, self).__init__(name, block, enumtype)
405
406         for b in self.block:
407             if bin(b[1])[2:].count("1") > 1:
408                 raise TypeError(
409                     "%s is not a flag enum.  No element in a "
410                     "flag enum may have more than a "
411                     "single bit set." % self.name
412                 )
413
414
415 class Import(Processable):
416     type = "Import"
417     _initialized = False
418
419     def __new__(cls, *args, **kwargs):
420         if args[0] not in seen_imports:
421             instance = super().__new__(cls)
422             instance._initialized = False
423             seen_imports[args[0]] = instance
424
425         return seen_imports[args[0]]
426
427     def __init__(self, filename, revision):
428         if self._initialized:
429             return
430         self.filename = filename
431         # Deal with imports
432         parser = VPPAPI(filename=filename, revision=revision)
433         dirlist = dirlist_get()
434         f = filename
435         for dir in dirlist:
436             f = os.path.join(dir, filename)
437             if os.path.exists(f):
438                 break
439         self.result = parser.parse_filename(f, None)
440         self._initialized = True
441
442     def __repr__(self):
443         return self.filename
444
445
446 class Option(Processable):
447     type = "Option"
448
449     def __init__(self, option, value=None):
450         self.option = option
451         self.value = value
452         self.crc = str(option).encode()
453
454     def process(self, result):  # -> Dict
455         result[self.type][self.option] = self.value
456
457     def __repr__(self):
458         return str(self.option)
459
460     def __getitem__(self, index):
461         return self.option[index]
462
463
464 class Array(Processable):
465     type = "Array"
466
467     def __init__(self, fieldtype, name, length, modern_vla=False):
468         self.fieldtype = fieldtype
469         self.fieldname = name
470         self.modern_vla = modern_vla
471         if type(length) is str:
472             self.lengthfield = length
473             self.length = 0
474             self.vla = True
475         else:
476             self.length = length
477             self.lengthfield = None
478             self.vla = False
479
480     def __repr__(self):
481         return str([self.fieldtype, self.fieldname, self.length, self.lengthfield])
482
483
484 class Field(Processable):
485     type = "Field"
486
487     def __init__(self, fieldtype, name, limit=None):
488         # limit field has been expanded to an options dict.
489
490         self.fieldtype = fieldtype
491         self.is_lengthfield = False
492
493         if self.fieldtype == "string":
494             raise ValueError("The string type {!r} is an " "array type ".format(name))
495
496         if name in keyword.kwlist:
497             raise ValueError(
498                 "Fieldname {!r} is a python keyword and is not "
499                 "accessible via the python API. ".format(name)
500             )
501         self.fieldname = name
502         self.limit = limit
503
504     def __repr__(self):
505         return str([self.fieldtype, self.fieldname])
506
507
508 class Counter(Processable):
509     type = "Counter"
510
511     def __init__(self, path, counter):
512         self.name = path
513         self.block = counter
514
515     def process(self, result):  # -> Dict
516         result["Counters"].append(self)
517
518
519 class Paths(Processable):
520     type = "Paths"
521
522     def __init__(self, pathset):
523         self.paths = pathset
524
525     def __repr__(self):
526         return "%s(paths=%s)" % (self.__class__.__name__, self.paths)
527
528
529 class Coord:
530     """Coordinates of a syntactic element. Consists of:
531     - File name
532     - Line number
533     - (optional) column number, for the Lexer
534     """
535
536     __slots__ = ("file", "line", "column", "__weakref__")
537
538     def __init__(self, file, line, column=None):
539         self.file = file
540         self.line = line
541         self.column = column
542
543     def __str__(self):
544         str = "%s:%s" % (self.file, self.line)
545         if self.column:
546             str += ":%s" % self.column
547         return str
548
549
550 class ParseError(Exception):
551     pass
552
553
554 #
555 # Grammar rules
556 #
557 class VPPAPIParser:
558     tokens = VPPAPILexer.tokens
559
560     def __init__(self, filename, logger, revision=None):
561         self.filename = filename
562         self.logger = logger
563         self.fields = []
564         self.revision = revision
565         self.last_comment = None
566
567     def _parse_error(self, msg, coord):
568         raise ParseError("%s: %s" % (coord, msg))
569
570     def _parse_warning(self, msg, coord):
571         if self.logger:
572             self.logger.warning("%s: %s" % (coord, msg))
573
574     def _coord(self, lineno, column=None):
575         return Coord(file=self.filename, line=lineno, column=column)
576
577     def _token_coord(self, p, token_idx):
578         """Returns the coordinates for the YaccProduction object 'p' indexed
579         with 'token_idx'. The coordinate includes the 'lineno' and
580         'column'. Both follow the lex semantic, starting from 1.
581         """
582         last_cr = p.lexer.lexdata.rfind("\n", 0, p.lexpos(token_idx))
583         if last_cr < 0:
584             last_cr = -1
585         column = p.lexpos(token_idx) - (last_cr)
586         return self._coord(p.lineno(token_idx), column)
587
588     def p_slist(self, p):
589         """slist : stmt
590         | slist stmt"""
591         if len(p) == 2:
592             p[0] = [p[1]]
593         else:
594             p[0] = p[1] + [p[2]]
595
596     def p_stmt(self, p):
597         """stmt : define
598         | typedef
599         | option
600         | import
601         | enum
602         | enumflag
603         | union
604         | service
605         | paths
606         | comment
607         | counters"""
608         p[0] = p[1]
609
610     def p_import(self, p):
611         """import : IMPORT STRING_LITERAL ';'"""
612         p[0] = Import(p[2], revision=self.revision)
613
614     def p_path_elements(self, p):
615         """path_elements : path_element
616         | path_elements path_element"""
617         if len(p) == 2:
618             p[0] = p[1]
619         else:
620             if type(p[1]) is dict:
621                 p[0] = [p[1], p[2]]
622             else:
623                 p[0] = p[1] + [p[2]]
624
625     def p_path_element(self, p):
626         """path_element : STRING_LITERAL STRING_LITERAL ';'"""
627         p[0] = {"path": p[1], "counter": p[2]}
628
629     def p_paths(self, p):
630         """paths : PATHS '{' path_elements '}' ';'"""
631         p[0] = Paths(p[3])
632
633     def p_counters(self, p):
634         """counters : COUNTERS ID '{' counter_elements '}' ';'"""
635         p[0] = Counter(p[2], p[4])
636
637     def p_counter_elements(self, p):
638         """counter_elements : counter_element
639         | counter_elements counter_element"""
640         if len(p) == 2:
641             p[0] = [p[1]]
642         else:
643             if type(p[1]) is dict:
644                 p[0] = [p[1], p[2]]
645             else:
646                 p[0] = p[1] + [p[2]]
647
648     def p_counter_element(self, p):
649         """counter_element : ID '{' counter_statements '}' ';'"""
650         p[0] = {**{"name": p[1]}, **p[3]}
651
652     def p_counter_statements(self, p):
653         """counter_statements : counter_statement
654         | counter_statements counter_statement"""
655         if len(p) == 2:
656             p[0] = p[1]
657         else:
658             p[0] = {**p[1], **p[2]}
659
660     def p_counter_statement(self, p):
661         """counter_statement : SEVERITY ID ';'
662         | UNITS STRING_LITERAL ';'
663         | DESCRIPTION STRING_LITERAL ';'
664         | TYPE ID ';'"""
665         p[0] = {p[1]: p[2]}
666
667     def p_service(self, p):
668         """service : SERVICE '{' service_statements '}' ';'"""
669         p[0] = p[3]
670
671     def p_service_statements(self, p):
672         """service_statements : service_statement
673         | service_statements service_statement"""
674         if len(p) == 2:
675             p[0] = [p[1]]
676         else:
677             p[0] = p[1] + [p[2]]
678
679     def p_service_statement(self, p):
680         """service_statement : RPC ID RETURNS NULL ';'
681         | RPC ID RETURNS ID ';'
682         | RPC ID RETURNS STREAM ID ';'
683         | RPC ID RETURNS ID EVENTS event_list ';'"""
684         if p[2] == p[4]:
685             # Verify that caller and reply differ
686             self._parse_error(
687                 "Reply ID ({}) should not be equal to Caller ID".format(p[2]),
688                 self._token_coord(p, 1),
689             )
690         if len(p) == 8:
691             p[0] = Service(p[2], p[4], p[6])
692         elif len(p) == 7:
693             p[0] = Service(p[2], p[5], stream=True)
694         else:
695             p[0] = Service(p[2], p[4])
696
697     def p_service_statement2(self, p):
698         """service_statement : RPC ID RETURNS ID STREAM ID ';'"""
699         p[0] = Service(p[2], p[4], stream_message=p[6], stream=True)
700
701     def p_event_list(self, p):
702         """event_list : events
703         | event_list events"""
704         if len(p) == 2:
705             p[0] = [p[1]]
706         else:
707             p[0] = p[1] + [p[2]]
708
709     def p_event(self, p):
710         """events : ID
711         | ID ','"""
712         p[0] = p[1]
713
714     def p_enum(self, p):
715         """enum : ENUM ID '{' enum_statements '}' ';'"""
716         p[0] = Enum(p[2], p[4])
717
718     def p_enum_type(self, p):
719         """enum : ENUM ID ':' enum_size '{' enum_statements '}' ';'"""
720         if len(p) == 9:
721             p[0] = Enum(p[2], p[6], enumtype=p[4])
722         else:
723             p[0] = Enum(p[2], p[4])
724
725     def p_enumflag(self, p):
726         """enumflag : ENUMFLAG ID '{' enum_statements '}' ';'"""
727         p[0] = EnumFlag(p[2], p[4])
728
729     def p_enumflag_type(self, p):
730         """enumflag : ENUMFLAG ID ':' enumflag_size '{' enum_statements '}' ';'"""  # noqa : E502
731         if len(p) == 9:
732             p[0] = EnumFlag(p[2], p[6], enumtype=p[4])
733         else:
734             p[0] = EnumFlag(p[2], p[4])
735
736     def p_enum_size(self, p):
737         """enum_size : U8
738         | U16
739         | U32
740         | I8
741         | I16
742         | I32"""
743         p[0] = p[1]
744
745     def p_enumflag_size(self, p):
746         """enumflag_size : U8
747         | U16
748         | U32"""
749         p[0] = p[1]
750
751     def p_define(self, p):
752         """define : DEFINE ID '{' block_statements_opt '}' ';'"""
753         self.fields = []
754         p[0] = Define(p[2], [], p[4], self.last_comment)
755
756     def p_define_flist(self, p):
757         """define : flist DEFINE ID '{' block_statements_opt '}' ';'"""
758         # Legacy typedef
759         if "typeonly" in p[1]:
760             self._parse_error(
761                 "legacy typedef. use typedef: {} {}[{}];".format(p[1], p[2], p[4]),
762                 self._token_coord(p, 1),
763             )
764         else:
765             p[0] = Define(p[3], p[1], p[5], self.last_comment)
766
767     def p_flist(self, p):
768         """flist : flag
769         | flist flag"""
770         if len(p) == 2:
771             p[0] = [p[1]]
772         else:
773             p[0] = p[1] + [p[2]]
774
775     def p_flag(self, p):
776         """flag : MANUAL_PRINT
777         | MANUAL_ENDIAN
778         | DONT_TRACE
779         | TYPEONLY
780         | AUTOENDIAN
781         | AUTOREPLY"""
782         if len(p) == 1:
783             return
784         p[0] = p[1]
785
786     def p_typedef(self, p):
787         """typedef : TYPEDEF ID '{' block_statements_opt '}' ';'"""
788         p[0] = Typedef(p[2], [], p[4])
789
790     def p_typedef_flist(self, p):
791         """typedef : flist TYPEDEF ID '{' block_statements_opt '}' ';'"""
792         p[0] = Typedef(p[3], p[1], p[5])
793
794     def p_typedef_alias(self, p):
795         """typedef : TYPEDEF declaration"""
796         p[0] = Using(p[2].fieldname, [], p[2])
797
798     def p_typedef_alias_flist(self, p):
799         """typedef : flist TYPEDEF declaration"""
800         p[0] = Using(p[3].fieldname, p[1], p[3])
801
802     def p_block_statements_opt(self, p):
803         """block_statements_opt : block_statements"""
804         p[0] = p[1]
805
806     def p_block_statements(self, p):
807         """block_statements : block_statement
808         | block_statements block_statement"""
809         if len(p) == 2:
810             p[0] = [p[1]]
811         else:
812             p[0] = p[1] + [p[2]]
813
814     def p_block_statement(self, p):
815         """block_statement : declaration
816         | option"""
817         p[0] = p[1]
818
819     def p_enum_statements(self, p):
820         """enum_statements : enum_statement
821         | enum_statements enum_statement"""
822         if len(p) == 2:
823             p[0] = [p[1]]
824         else:
825             p[0] = p[1] + [p[2]]
826
827     def p_enum_statement(self, p):
828         """enum_statement : ID '=' NUM ','
829         | ID ','
830         | ID '[' field_options ']' ','
831         | ID '=' NUM '[' field_options ']' ','"""
832         if len(p) == 3:
833             p[0] = {"id": p[1]}
834         elif len(p) == 5:
835             p[0] = {"id": p[1], "value": p[3]}
836         elif len(p) == 6:
837             p[0] = {"id": p[1], "option": p[3]}
838         elif len(p) == 8:
839             p[0] = {"id": p[1], "value": p[3], "option": p[5]}
840         else:
841             self._parse_error("ERROR", self._token_coord(p, 1))
842
843     def p_field_options(self, p):
844         """field_options : field_option
845         | field_options field_option"""
846         if len(p) == 2:
847             p[0] = p[1]
848         else:
849             p[0] = {**p[1], **p[2]}
850
851     def p_field_option(self, p):
852         """field_option : ID
853         | ID '=' assignee ','
854         | ID '=' assignee
855
856         """
857         if len(p) == 2:
858             p[0] = {p[1]: None}
859         else:
860             p[0] = {p[1]: p[3]}
861
862     def p_variable_name(self, p):
863         """variable_name : ID
864         | TYPE
865         | SEVERITY
866         | DESCRIPTION
867         | COUNTERS
868         | PATHS
869         """
870         p[0] = p[1]
871
872     def p_comment(self, p):
873         """comment : COMMENT"""
874         self.last_comment = p[1]
875         p[0] = []
876
877     def p_declaration(self, p):
878         """declaration : type_specifier variable_name ';'
879         | type_specifier variable_name '[' field_options ']' ';'
880         """
881         if len(p) == 7:
882             p[0] = Field(p[1], p[2], p[4])
883         elif len(p) == 4:
884             p[0] = Field(p[1], p[2])
885         else:
886             self._parse_error("ERROR", self._token_coord(p, 1))
887         self.fields.append(p[2])
888
889     def p_declaration_array_vla(self, p):
890         """declaration : type_specifier variable_name '[' ']' ';'"""
891         p[0] = Array(p[1], p[2], 0, modern_vla=True)
892
893     def p_declaration_array(self, p):
894         """declaration : type_specifier variable_name '[' NUM ']' ';'
895         | type_specifier variable_name '[' ID ']' ';'"""
896
897         if len(p) != 7:
898             return self._parse_error(
899                 "array: %s" % p.value, self._coord(lineno=p.lineno)
900             )
901
902         # Make this error later
903         if type(p[4]) is int and p[4] == 0:
904             # XXX: Line number is wrong
905             self._parse_warning(
906                 "Old Style VLA: {} {}[{}];".format(p[1], p[2], p[4]),
907                 self._token_coord(p, 1),
908             )
909
910         if type(p[4]) is str and p[4] not in self.fields:
911             # Verify that length field exists
912             self._parse_error(
913                 "Missing length field: {} {}[{}];".format(p[1], p[2], p[4]),
914                 self._token_coord(p, 1),
915             )
916         p[0] = Array(p[1], p[2], p[4])
917
918     def p_option(self, p):
919         """option : OPTION ID '=' assignee ';'
920         | OPTION ID ';'"""
921         if len(p) == 4:
922             p[0] = Option(p[2])
923         else:
924             p[0] = Option(p[2], p[4])
925
926     def p_assignee(self, p):
927         """assignee : NUM
928         | TRUE
929         | FALSE
930         | STRING_LITERAL"""
931         p[0] = p[1]
932
933     def p_type_specifier(self, p):
934         """type_specifier : U8
935         | U16
936         | U32
937         | U64
938         | I8
939         | I16
940         | I32
941         | I64
942         | F64
943         | BOOL
944         | STRING"""
945         p[0] = p[1]
946
947     # Do a second pass later to verify that user defined types are defined
948     def p_typedef_specifier(self, p):
949         """type_specifier : ID"""
950         if p[1] not in global_types:
951             self._parse_error(
952                 "Undefined type: {}".format(p[1]), self._token_coord(p, 1)
953             )
954         p[0] = p[1]
955
956     def p_union(self, p):
957         """union : UNION ID '{' block_statements_opt '}' ';'"""
958         p[0] = Union(p[2], [], p[4])
959
960     def p_union_flist(self, p):
961         """union : flist UNION ID '{' block_statements_opt '}' ';'"""
962         p[0] = Union(p[3], p[1], p[5])
963
964     # Error rule for syntax errors
965     def p_error(self, p):
966         if p:
967             if p.type == "COMMENT":
968                 self.parser.errok()
969                 return
970             self._parse_error("before: %s" % p.value, self._coord(lineno=p.lineno))
971         else:
972             self._parse_error("At end of input", self.filename)
973
974     def build(self, **kwargs):
975         self.parser = yacc.yacc(module=self, **kwargs)
976
977
978 class VPPAPI:
979     def __init__(self, debug=False, filename="", logger=None, revision=None):
980         self.lexer = lex.lex(module=VPPAPILexer(filename), debug=debug)
981         self.parser = VPPAPIParser(filename, logger, revision=revision)
982         self.parser.build(write_tables=False, debug=debug)
983         self.logger = logger
984         self.revision = revision
985         self.filename = filename
986
987     def parse_string(self, code, debug=0, lineno=1):
988         self.lexer.lineno = lineno
989         return self.parser.parser.parse(code, lexer=self.lexer, debug=debug)
990
991     def parse_fd(self, fd, debug=0):
992         data = fd.read()
993         return self.parse_string(data, debug=debug)
994
995     def parse_filename(self, filename, debug=0):
996         if self.revision:
997             git_show = "git show {}:{}".format(self.revision, filename)
998             proc = Popen(git_show.split(), stdout=PIPE, encoding="utf-8")
999             try:
1000                 data, errs = proc.communicate()
1001                 if proc.returncode != 0:
1002                     print(
1003                         "File not found: {}:{}".format(self.revision, filename),
1004                         file=sys.stderr,
1005                     )
1006                     sys.exit(2)
1007                 return self.parse_string(data, debug=debug)
1008             except Exception:
1009                 sys.exit(3)
1010         else:
1011             try:
1012                 with open(filename, encoding="utf-8") as fd:
1013                     return self.parse_fd(fd, None)
1014             except FileNotFoundError:
1015                 print("File not found: {}".format(filename), file=sys.stderr)
1016                 sys.exit(2)
1017
1018     def process(self, objs):
1019         s = {}
1020         s["Option"] = {}
1021         s["Define"] = []
1022         s["Service"] = []
1023         s["types"] = []
1024         s["Import"] = []
1025         s["Counters"] = []
1026         s["Paths"] = []
1027         crc = 0
1028         for o in objs:
1029             try:
1030                 crc = binascii.crc32(o.crc, crc) & 0xFFFFFFFF
1031             except AttributeError:
1032                 pass
1033
1034             if type(o) is list:
1035                 for o2 in o:
1036                     if isinstance(o2, Service):
1037                         o2.process(s)
1038             else:
1039                 o.process(s)
1040
1041         msgs = {d.name: d for d in s["Define"]}
1042         svcs = {s.caller: s for s in s["Service"]}
1043         replies = {s.reply: s for s in s["Service"]}
1044         seen_services = {}
1045
1046         s["file_crc"] = crc
1047
1048         for service in svcs:
1049             if service not in msgs:
1050                 raise ValueError(
1051                     "Service definition refers to unknown message"
1052                     " definition: {}".format(service)
1053                 )
1054             if svcs[service].reply != "null" and svcs[service].reply not in msgs:
1055                 raise ValueError(
1056                     "Service definition refers to unknown message"
1057                     " definition in reply: {}".format(svcs[service].reply)
1058                 )
1059             if service in replies:
1060                 raise ValueError(
1061                     "Service definition refers to message"
1062                     " marked as reply: {}".format(service)
1063                 )
1064             for event in svcs[service].events:
1065                 if event not in msgs:
1066                     raise ValueError(
1067                         "Service definition refers to unknown "
1068                         "event: {} in message: {}".format(event, service)
1069                     )
1070                 seen_services[event] = True
1071
1072         # Create services implicitly
1073         for d in msgs:
1074             if d in seen_services:
1075                 continue
1076             if d.endswith("_reply"):
1077                 if d[:-6] in svcs:
1078                     continue
1079                 if d[:-6] not in msgs:
1080                     raise ValueError("{} missing calling message".format(d))
1081                 continue
1082             if d.endswith("_dump"):
1083                 if d in svcs:
1084                     continue
1085                 if d[:-5] + "_details" in msgs:
1086                     s["Service"].append(Service(d, d[:-5] + "_details", stream=True))
1087                 else:
1088                     raise ValueError("{} missing details message".format(d))
1089                 continue
1090
1091             if d.endswith("_details"):
1092                 if d[:-8] + "_get" in msgs:
1093                     if d[:-8] + "_get" in svcs:
1094                         continue
1095                     raise ValueError(
1096                         "{} should be in a stream service".format(d[:-8] + "_get")
1097                     )
1098                 if d[:-8] + "_dump" in msgs:
1099                     continue
1100                 raise ValueError("{} missing dump or get message".format(d))
1101
1102             if d in svcs:
1103                 continue
1104             if d + "_reply" in msgs:
1105                 s["Service"].append(Service(d, d + "_reply"))
1106             else:
1107                 raise ValueError(
1108                     "{} missing reply message ({}) or service definition".format(
1109                         d, d + "_reply"
1110                     )
1111                 )
1112
1113         return s
1114
1115     def process_imports(self, objs, in_import, result):  # -> List
1116         for o in objs:
1117             # Only allow the following object types from imported file
1118             if in_import and not isinstance(o, (Enum, Import, Typedef, Union, Using)):
1119                 continue
1120             if isinstance(o, Import):
1121                 result.append(o)
1122                 result = self.process_imports(o.result, True, result)
1123             else:
1124                 result.append(o)
1125         return result
1126
1127
1128 # Add message ids to each message.
1129 def add_msg_id(s):
1130     for o in s:
1131         o.block.insert(0, Field("u16", "_vl_msg_id"))
1132     return s
1133
1134
1135 dirlist = []
1136
1137
1138 def dirlist_add(dirs):
1139     global dirlist
1140     if dirs:
1141         dirlist = dirlist + dirs
1142
1143
1144 def dirlist_get():
1145     return dirlist
1146
1147
1148 def foldup_blocks(block, crc):
1149     for b in block:
1150         # Look up CRC in user defined types
1151         if b.fieldtype.startswith("vl_api_"):
1152             # Recursively
1153             t = global_types[b.fieldtype]
1154             try:
1155                 crc = binascii.crc32(t.crc, crc) & 0xFFFFFFFF
1156                 crc = foldup_blocks(t.block, crc)
1157             except AttributeError:
1158                 pass
1159     return crc
1160
1161
1162 def foldup_crcs(s):
1163     for f in s:
1164         f.crc = foldup_blocks(f.block, binascii.crc32(f.crc) & 0xFFFFFFFF)
1165
1166
1167 def run_vppapigen(
1168     input_file=None,
1169     output=sys.stdout,
1170     includedir=None,
1171     debug=False,
1172     show_name=None,
1173     output_module="C",
1174     outputdir=None,
1175     pluginpath="",
1176     git_revision=None,
1177 ):
1178     # reset globals
1179     dirlist.clear()
1180     global_types.clear()
1181     seen_imports.clear()
1182
1183     dirlist_add(includedir)
1184     if not debug:
1185         sys.excepthook = exception_handler
1186
1187     # Filename
1188     if show_name:
1189         filename = show_name[0]
1190     elif input_file:
1191         filename = input_file
1192     else:
1193         filename = ""
1194
1195     if debug:
1196         logging.basicConfig(stream=sys.stdout, level=logging.WARNING)
1197     else:
1198         logging.basicConfig()
1199
1200     #
1201     # Generate representation
1202     #
1203     from importlib.machinery import SourceFileLoader
1204
1205     # Default path
1206     pluginpath = ""
1207     if not pluginpath:
1208         cand = []
1209         cand.append(os.path.dirname(os.path.realpath(__file__)))
1210         cand.append(os.path.dirname(os.path.realpath(__file__)) + "/../share/vpp/")
1211         for c in cand:
1212             c += "/"
1213             if os.path.isfile("{}vppapigen_{}.py".format(c, output_module.lower())):
1214                 pluginpath = c
1215                 break
1216     else:
1217         pluginpath = pluginpath + "/"
1218     if pluginpath == "":
1219         log.exception("Output plugin not found")
1220         return 1
1221     module_path = "{}vppapigen_{}.py".format(pluginpath, output_module.lower())
1222
1223     try:
1224         plugin = SourceFileLoader(output_module, module_path).load_module()
1225     except Exception as err:
1226         log.exception("Error importing output plugin: %s, %s", module_path, err)
1227         return 1
1228
1229     parser = VPPAPI(debug=debug, filename=filename, logger=log, revision=git_revision)
1230
1231     try:
1232         if not input_file:
1233             parsed_objects = parser.parse_fd(sys.stdin, log)
1234         else:
1235             parsed_objects = parser.parse_filename(input_file, log)
1236     except ParseError as e:
1237         print("Parse error: ", e, file=sys.stderr)
1238         sys.exit(1)
1239
1240     # Build a list of objects. Hash of lists.
1241     result = []
1242
1243     # if the variable is not set in the plugin, assume it to be false.
1244     try:
1245         plugin.process_imports
1246     except AttributeError:
1247         plugin.process_imports = False
1248
1249     if plugin.process_imports:
1250         result = parser.process_imports(parsed_objects, False, result)
1251         s = parser.process(result)
1252     else:
1253         s = parser.process(parsed_objects)
1254         imports = parser.process_imports(parsed_objects, False, result)
1255         s["imported"] = parser.process(imports)
1256
1257     # Add msg_id field
1258     s["Define"] = add_msg_id(s["Define"])
1259
1260     # Fold up CRCs
1261     foldup_crcs(s["Define"])
1262
1263     #
1264     # Debug
1265     if debug:
1266         import pprint
1267
1268         pp = pprint.PrettyPrinter(indent=4, stream=sys.stderr)
1269         for t in s["Define"]:
1270             pp.pprint([t.name, t.flags, t.block])
1271         for t in s["types"]:
1272             pp.pprint([t.name, t.block])
1273
1274     result = plugin.run(outputdir, filename, s)
1275     if result:
1276         if isinstance(output, str):
1277             with open(output, "w", encoding="UTF-8") as f:
1278                 print(result, file=f)
1279         else:
1280             print(result, file=output)
1281     else:
1282         log.exception("Running plugin failed: %s %s", filename, result)
1283         return 1
1284     return 0
1285
1286
1287 def run_kw_vppapigen(kwargs):
1288     return run_vppapigen(**kwargs)
1289
1290
1291 #
1292 # Main
1293 #
1294 def main():
1295     if sys.version_info < (
1296         3,
1297         5,
1298     ):
1299         log.exception(
1300             "vppapigen requires a supported version of python. "
1301             "Please use version 3.5 or greater. "
1302             "Using %s",
1303             sys.version,
1304         )
1305         return 1
1306
1307     cliparser = argparse.ArgumentParser(description="VPP API generator")
1308     cliparser.add_argument("--pluginpath", default="")
1309     cliparser.add_argument("--includedir", action="append")
1310     cliparser.add_argument("--outputdir", action="store")
1311     cliparser.add_argument("--input")
1312     cliparser.add_argument(
1313         "--output",
1314         nargs="?",
1315         type=argparse.FileType("w", encoding="UTF-8"),
1316         default=sys.stdout,
1317     )
1318
1319     cliparser.add_argument("output_module", nargs="?", default="C")
1320     cliparser.add_argument("--debug", action="store_true")
1321     cliparser.add_argument("--show-name", nargs=1)
1322     cliparser.add_argument(
1323         "--git-revision", help="Git revision to use for opening files"
1324     )
1325     args = cliparser.parse_args()
1326
1327     return run_vppapigen(
1328         includedir=args.includedir,
1329         debug=args.debug,
1330         outputdir=args.outputdir,
1331         show_name=args.show_name,
1332         input_file=args.input,
1333         output_module=args.output_module,
1334         pluginpath=args.pluginpath,
1335         git_revision=args.git_revision,
1336         output=args.output,
1337     )
1338
1339
1340 if __name__ == "__main__":
1341     sys.exit(main())