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