hs-test: clean up Makefile for compatibility with ci-management
[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         self.last_comment = None
756
757     def p_define_flist(self, p):
758         """define : flist DEFINE ID '{' block_statements_opt '}' ';'"""
759         # Legacy typedef
760         if "typeonly" in p[1]:
761             self._parse_error(
762                 "legacy typedef. use typedef: {} {}[{}];".format(p[1], p[2], p[4]),
763                 self._token_coord(p, 1),
764             )
765         else:
766             p[0] = Define(p[3], p[1], p[5], self.last_comment)
767             self.last_comment = None
768
769     def p_flist(self, p):
770         """flist : flag
771         | flist flag"""
772         if len(p) == 2:
773             p[0] = [p[1]]
774         else:
775             p[0] = p[1] + [p[2]]
776
777     def p_flag(self, p):
778         """flag : MANUAL_PRINT
779         | MANUAL_ENDIAN
780         | DONT_TRACE
781         | TYPEONLY
782         | AUTOENDIAN
783         | AUTOREPLY"""
784         if len(p) == 1:
785             return
786         p[0] = p[1]
787
788     def p_typedef(self, p):
789         """typedef : TYPEDEF ID '{' block_statements_opt '}' ';'"""
790         p[0] = Typedef(p[2], [], p[4])
791
792     def p_typedef_flist(self, p):
793         """typedef : flist TYPEDEF ID '{' block_statements_opt '}' ';'"""
794         p[0] = Typedef(p[3], p[1], p[5])
795
796     def p_typedef_alias(self, p):
797         """typedef : TYPEDEF declaration"""
798         p[0] = Using(p[2].fieldname, [], p[2])
799
800     def p_typedef_alias_flist(self, p):
801         """typedef : flist TYPEDEF declaration"""
802         p[0] = Using(p[3].fieldname, p[1], p[3])
803
804     def p_block_statements_opt(self, p):
805         """block_statements_opt : block_statements"""
806         p[0] = p[1]
807
808     def p_block_statements(self, p):
809         """block_statements : block_statement
810         | block_statements block_statement"""
811         if len(p) == 2:
812             p[0] = [p[1]]
813         else:
814             p[0] = p[1] + [p[2]]
815
816     def p_block_statement(self, p):
817         """block_statement : declaration
818         | option"""
819         p[0] = p[1]
820
821     def p_enum_statements(self, p):
822         """enum_statements : enum_statement
823         | enum_statements enum_statement"""
824         if len(p) == 2:
825             p[0] = [p[1]]
826         else:
827             p[0] = p[1] + [p[2]]
828
829     def p_enum_statement(self, p):
830         """enum_statement : ID '=' NUM ','
831         | ID ','
832         | ID '[' field_options ']' ','
833         | ID '=' NUM '[' field_options ']' ','"""
834         if len(p) == 3:
835             p[0] = {"id": p[1]}
836         elif len(p) == 5:
837             p[0] = {"id": p[1], "value": p[3]}
838         elif len(p) == 6:
839             p[0] = {"id": p[1], "option": p[3]}
840         elif len(p) == 8:
841             p[0] = {"id": p[1], "value": p[3], "option": p[5]}
842         else:
843             self._parse_error("ERROR", self._token_coord(p, 1))
844
845     def p_field_options(self, p):
846         """field_options : field_option
847         | field_options field_option"""
848         if len(p) == 2:
849             p[0] = p[1]
850         else:
851             p[0] = {**p[1], **p[2]}
852
853     def p_field_option(self, p):
854         """field_option : ID
855         | ID '=' assignee ','
856         | ID '=' assignee
857
858         """
859         if len(p) == 2:
860             p[0] = {p[1]: None}
861         else:
862             p[0] = {p[1]: p[3]}
863
864     def p_variable_name(self, p):
865         """variable_name : ID
866         | TYPE
867         | SEVERITY
868         | DESCRIPTION
869         | COUNTERS
870         | PATHS
871         """
872         p[0] = p[1]
873
874     def p_comment(self, p):
875         """comment : COMMENT"""
876         self.last_comment = p[1]
877         p[0] = []
878
879     def p_declaration(self, p):
880         """declaration : type_specifier variable_name ';'
881         | type_specifier variable_name '[' field_options ']' ';'
882         """
883         if len(p) == 7:
884             p[0] = Field(p[1], p[2], p[4])
885         elif len(p) == 4:
886             p[0] = Field(p[1], p[2])
887         else:
888             self._parse_error("ERROR", self._token_coord(p, 1))
889         self.fields.append(p[2])
890
891     def p_declaration_array_vla(self, p):
892         """declaration : type_specifier variable_name '[' ']' ';'"""
893         p[0] = Array(p[1], p[2], 0, modern_vla=True)
894
895     def p_declaration_array(self, p):
896         """declaration : type_specifier variable_name '[' NUM ']' ';'
897         | type_specifier variable_name '[' ID ']' ';'"""
898
899         if len(p) != 7:
900             return self._parse_error(
901                 "array: %s" % p.value, self._coord(lineno=p.lineno)
902             )
903
904         # Make this error later
905         if type(p[4]) is int and p[4] == 0:
906             # XXX: Line number is wrong
907             self._parse_warning(
908                 "Old Style VLA: {} {}[{}];".format(p[1], p[2], p[4]),
909                 self._token_coord(p, 1),
910             )
911
912         if type(p[4]) is str and p[4] not in self.fields:
913             # Verify that length field exists
914             self._parse_error(
915                 "Missing length field: {} {}[{}];".format(p[1], p[2], p[4]),
916                 self._token_coord(p, 1),
917             )
918         p[0] = Array(p[1], p[2], p[4])
919
920     def p_option(self, p):
921         """option : OPTION ID '=' assignee ';'
922         | OPTION ID ';'"""
923         if len(p) == 4:
924             p[0] = Option(p[2])
925         else:
926             p[0] = Option(p[2], p[4])
927
928     def p_assignee(self, p):
929         """assignee : NUM
930         | TRUE
931         | FALSE
932         | STRING_LITERAL"""
933         p[0] = p[1]
934
935     def p_type_specifier(self, p):
936         """type_specifier : U8
937         | U16
938         | U32
939         | U64
940         | I8
941         | I16
942         | I32
943         | I64
944         | F64
945         | BOOL
946         | STRING"""
947         p[0] = p[1]
948
949     # Do a second pass later to verify that user defined types are defined
950     def p_typedef_specifier(self, p):
951         """type_specifier : ID"""
952         if p[1] not in global_types:
953             self._parse_error(
954                 "Undefined type: {}".format(p[1]), self._token_coord(p, 1)
955             )
956         p[0] = p[1]
957
958     def p_union(self, p):
959         """union : UNION ID '{' block_statements_opt '}' ';'"""
960         p[0] = Union(p[2], [], p[4])
961
962     def p_union_flist(self, p):
963         """union : flist UNION ID '{' block_statements_opt '}' ';'"""
964         p[0] = Union(p[3], p[1], p[5])
965
966     # Error rule for syntax errors
967     def p_error(self, p):
968         if p:
969             if p.type == "COMMENT":
970                 self.parser.errok()
971                 return
972             self._parse_error("before: %s" % p.value, self._coord(lineno=p.lineno))
973         else:
974             self._parse_error("At end of input", self.filename)
975
976     def build(self, **kwargs):
977         self.parser = yacc.yacc(module=self, **kwargs)
978
979
980 class VPPAPI:
981     def __init__(self, debug=False, filename="", logger=None, revision=None):
982         self.lexer = lex.lex(module=VPPAPILexer(filename), debug=debug)
983         self.parser = VPPAPIParser(filename, logger, revision=revision)
984         self.parser.build(write_tables=False, debug=debug)
985         self.logger = logger
986         self.revision = revision
987         self.filename = filename
988
989     def parse_string(self, code, debug=0, lineno=1):
990         self.lexer.lineno = lineno
991         return self.parser.parser.parse(code, lexer=self.lexer, debug=debug)
992
993     def parse_fd(self, fd, debug=0):
994         data = fd.read()
995         return self.parse_string(data, debug=debug)
996
997     def parse_filename(self, filename, debug=0):
998         if self.revision:
999             git_show = "git show {}:{}".format(self.revision, filename)
1000             proc = Popen(git_show.split(), stdout=PIPE, encoding="utf-8")
1001             try:
1002                 data, errs = proc.communicate()
1003                 if proc.returncode != 0:
1004                     print(
1005                         "File not found: {}:{}".format(self.revision, filename),
1006                         file=sys.stderr,
1007                     )
1008                     sys.exit(2)
1009                 return self.parse_string(data, debug=debug)
1010             except Exception:
1011                 sys.exit(3)
1012         else:
1013             try:
1014                 with open(filename, encoding="utf-8") as fd:
1015                     return self.parse_fd(fd, None)
1016             except FileNotFoundError:
1017                 print("File not found: {}".format(filename), file=sys.stderr)
1018                 sys.exit(2)
1019
1020     def process(self, objs):
1021         s = {}
1022         s["Option"] = {}
1023         s["Define"] = []
1024         s["Service"] = []
1025         s["types"] = []
1026         s["Import"] = []
1027         s["Counters"] = []
1028         s["Paths"] = []
1029         crc = 0
1030         for o in objs:
1031             try:
1032                 crc = binascii.crc32(o.crc, crc) & 0xFFFFFFFF
1033             except AttributeError:
1034                 pass
1035
1036             if type(o) is list:
1037                 for o2 in o:
1038                     if isinstance(o2, Service):
1039                         o2.process(s)
1040             else:
1041                 o.process(s)
1042
1043         msgs = {d.name: d for d in s["Define"]}
1044         svcs = {s.caller: s for s in s["Service"]}
1045         replies = {s.reply: s for s in s["Service"]}
1046         seen_services = {}
1047
1048         s["file_crc"] = crc
1049
1050         for service in svcs:
1051             if service not in msgs:
1052                 raise ValueError(
1053                     "Service definition refers to unknown message"
1054                     " definition: {}".format(service)
1055                 )
1056             if svcs[service].reply != "null" and svcs[service].reply not in msgs:
1057                 raise ValueError(
1058                     "Service definition refers to unknown message"
1059                     " definition in reply: {}".format(svcs[service].reply)
1060                 )
1061             if service in replies:
1062                 raise ValueError(
1063                     "Service definition refers to message"
1064                     " marked as reply: {}".format(service)
1065                 )
1066             for event in svcs[service].events:
1067                 if event not in msgs:
1068                     raise ValueError(
1069                         "Service definition refers to unknown "
1070                         "event: {} in message: {}".format(event, service)
1071                     )
1072                 seen_services[event] = True
1073
1074         # Create services implicitly
1075         for d in msgs:
1076             if d in seen_services:
1077                 continue
1078             if d.endswith("_reply"):
1079                 if d[:-6] in svcs:
1080                     continue
1081                 if d[:-6] not in msgs:
1082                     raise ValueError("{} missing calling message".format(d))
1083                 continue
1084             if d.endswith("_dump"):
1085                 if d in svcs:
1086                     continue
1087                 if d[:-5] + "_details" in msgs:
1088                     s["Service"].append(Service(d, d[:-5] + "_details", stream=True))
1089                 else:
1090                     raise ValueError("{} missing details message".format(d))
1091                 continue
1092
1093             if d.endswith("_details"):
1094                 if d[:-8] + "_get" in msgs:
1095                     if d[:-8] + "_get" in svcs:
1096                         continue
1097                     raise ValueError(
1098                         "{} should be in a stream service".format(d[:-8] + "_get")
1099                     )
1100                 if d[:-8] + "_dump" in msgs:
1101                     continue
1102                 raise ValueError("{} missing dump or get message".format(d))
1103
1104             if d in svcs:
1105                 continue
1106             if d + "_reply" in msgs:
1107                 s["Service"].append(Service(d, d + "_reply"))
1108             else:
1109                 raise ValueError(
1110                     "{} missing reply message ({}) or service definition".format(
1111                         d, d + "_reply"
1112                     )
1113                 )
1114
1115         return s
1116
1117     def process_imports(self, objs, in_import, result):  # -> List
1118         for o in objs:
1119             # Only allow the following object types from imported file
1120             if in_import and not isinstance(o, (Enum, Import, Typedef, Union, Using)):
1121                 continue
1122             if isinstance(o, Import):
1123                 result.append(o)
1124                 result = self.process_imports(o.result, True, result)
1125             else:
1126                 result.append(o)
1127         return result
1128
1129
1130 # Add message ids to each message.
1131 def add_msg_id(s):
1132     for o in s:
1133         o.block.insert(0, Field("u16", "_vl_msg_id"))
1134     return s
1135
1136
1137 dirlist = []
1138
1139
1140 def dirlist_add(dirs):
1141     global dirlist
1142     if dirs:
1143         dirlist = dirlist + dirs
1144
1145
1146 def dirlist_get():
1147     return dirlist
1148
1149
1150 def foldup_blocks(block, crc):
1151     for b in block:
1152         # Look up CRC in user defined types
1153         if b.fieldtype.startswith("vl_api_"):
1154             # Recursively
1155             t = global_types[b.fieldtype]
1156             try:
1157                 crc = binascii.crc32(t.crc, crc) & 0xFFFFFFFF
1158                 crc = foldup_blocks(t.block, crc)
1159             except AttributeError:
1160                 pass
1161     return crc
1162
1163
1164 def foldup_crcs(s):
1165     for f in s:
1166         f.crc = foldup_blocks(f.block, binascii.crc32(f.crc) & 0xFFFFFFFF)
1167
1168
1169 def run_vppapigen(
1170     input_file=None,
1171     output=sys.stdout,
1172     includedir=None,
1173     debug=False,
1174     show_name=None,
1175     output_module="C",
1176     outputdir=None,
1177     pluginpath="",
1178     git_revision=None,
1179 ):
1180     # reset globals
1181     dirlist.clear()
1182     global_types.clear()
1183     seen_imports.clear()
1184
1185     dirlist_add(includedir)
1186     if not debug:
1187         sys.excepthook = exception_handler
1188
1189     # Filename
1190     if show_name:
1191         filename = show_name[0]
1192     elif input_file:
1193         filename = input_file
1194     else:
1195         filename = ""
1196
1197     if debug:
1198         logging.basicConfig(stream=sys.stdout, level=logging.WARNING)
1199     else:
1200         logging.basicConfig()
1201
1202     #
1203     # Generate representation
1204     #
1205     from importlib.machinery import SourceFileLoader
1206
1207     # Default path
1208     pluginpath = ""
1209     if not pluginpath:
1210         cand = []
1211         cand.append(os.path.dirname(os.path.realpath(__file__)))
1212         cand.append(os.path.dirname(os.path.realpath(__file__)) + "/../share/vpp/")
1213         for c in cand:
1214             c += "/"
1215             if os.path.isfile("{}vppapigen_{}.py".format(c, output_module.lower())):
1216                 pluginpath = c
1217                 break
1218     else:
1219         pluginpath = pluginpath + "/"
1220     if pluginpath == "":
1221         log.exception("Output plugin not found")
1222         return 1
1223     module_path = "{}vppapigen_{}.py".format(pluginpath, output_module.lower())
1224
1225     try:
1226         plugin = SourceFileLoader(output_module, module_path).load_module()
1227     except Exception as err:
1228         log.exception("Error importing output plugin: %s, %s", module_path, err)
1229         return 1
1230
1231     parser = VPPAPI(debug=debug, filename=filename, logger=log, revision=git_revision)
1232
1233     try:
1234         if not input_file:
1235             parsed_objects = parser.parse_fd(sys.stdin, log)
1236         else:
1237             parsed_objects = parser.parse_filename(input_file, log)
1238     except ParseError as e:
1239         print("Parse error: ", e, file=sys.stderr)
1240         sys.exit(1)
1241
1242     # Build a list of objects. Hash of lists.
1243     result = []
1244
1245     # if the variable is not set in the plugin, assume it to be false.
1246     try:
1247         plugin.process_imports
1248     except AttributeError:
1249         plugin.process_imports = False
1250
1251     if plugin.process_imports:
1252         result = parser.process_imports(parsed_objects, False, result)
1253         s = parser.process(result)
1254     else:
1255         s = parser.process(parsed_objects)
1256         imports = parser.process_imports(parsed_objects, False, result)
1257         s["imported"] = parser.process(imports)
1258
1259     # Add msg_id field
1260     s["Define"] = add_msg_id(s["Define"])
1261
1262     # Fold up CRCs
1263     foldup_crcs(s["Define"])
1264
1265     #
1266     # Debug
1267     if debug:
1268         import pprint
1269
1270         pp = pprint.PrettyPrinter(indent=4, stream=sys.stderr)
1271         for t in s["Define"]:
1272             pp.pprint([t.name, t.flags, t.block])
1273         for t in s["types"]:
1274             pp.pprint([t.name, t.block])
1275
1276     result = plugin.run(outputdir, filename, s)
1277     if result:
1278         if isinstance(output, str):
1279             with open(output, "w", encoding="UTF-8") as f:
1280                 print(result, file=f)
1281         else:
1282             print(result, file=output)
1283     else:
1284         log.exception("Running plugin failed: %s %s", filename, result)
1285         return 1
1286     return 0
1287
1288
1289 def run_kw_vppapigen(kwargs):
1290     return run_vppapigen(**kwargs)
1291
1292
1293 #
1294 # Main
1295 #
1296 def main():
1297     if sys.version_info < (
1298         3,
1299         5,
1300     ):
1301         log.exception(
1302             "vppapigen requires a supported version of python. "
1303             "Please use version 3.5 or greater. "
1304             "Using %s",
1305             sys.version,
1306         )
1307         return 1
1308
1309     cliparser = argparse.ArgumentParser(description="VPP API generator")
1310     cliparser.add_argument("--pluginpath", default="")
1311     cliparser.add_argument("--includedir", action="append")
1312     cliparser.add_argument("--outputdir", action="store")
1313     cliparser.add_argument("--input")
1314     cliparser.add_argument(
1315         "--output",
1316         nargs="?",
1317         type=argparse.FileType("w", encoding="UTF-8"),
1318         default=sys.stdout,
1319     )
1320
1321     cliparser.add_argument("output_module", nargs="?", default="C")
1322     cliparser.add_argument("--debug", action="store_true")
1323     cliparser.add_argument("--show-name", nargs=1)
1324     cliparser.add_argument(
1325         "--git-revision", help="Git revision to use for opening files"
1326     )
1327     args = cliparser.parse_args()
1328
1329     return run_vppapigen(
1330         includedir=args.includedir,
1331         debug=args.debug,
1332         outputdir=args.outputdir,
1333         show_name=args.show_name,
1334         input_file=args.input,
1335         output_module=args.output_module,
1336         pluginpath=args.pluginpath,
1337         git_revision=args.git_revision,
1338         output=args.output,
1339     )
1340
1341
1342 if __name__ == "__main__":
1343     sys.exit(main())