• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python3 -i
2#
3# Copyright (c) 2013-2020 The Khronos Group Inc.
4#
5# SPDX-License-Identifier: Apache-2.0
6
7from pathlib import Path
8
9from generator import GeneratorOptions, OutputGenerator, noneStr, write
10
11ENUM_TABLE_PREFIX = """
12[cols=",",options="header",]
13|=======================================================================
14|Enum |Description"""
15
16ENUM_TABLE_SUFFIX = """|======================================================================="""
17
18FLAG_BLOCK_PREFIX = """.Flag Descriptions
19****"""
20
21FLAG_BLOCK_SUFFIX = """****"""
22
23
24class DocGeneratorOptions(GeneratorOptions):
25    """DocGeneratorOptions - subclass of GeneratorOptions for
26    generating declaration snippets for the spec.
27
28    Shares many members with CGeneratorOptions, since
29    both are writing C-style declarations."""
30
31    def __init__(self,
32                 prefixText="",
33                 apicall='',
34                 apientry='',
35                 apientryp='',
36                 indentFuncProto=True,
37                 indentFuncPointer=False,
38                 alignFuncParam=0,
39                 secondaryInclude=False,
40                 expandEnumerants=True,
41                 extEnumerantAdditions=False,
42                 extEnumerantFormatString=" (Added by the {} extension)",
43                 **kwargs):
44        """Constructor.
45
46        Since this generator outputs multiple files at once,
47        the filename is just a "stamp" to indicate last generation time.
48
49        Shares many parameters/members with CGeneratorOptions, since
50        both are writing C-style declarations:
51
52        - prefixText - list of strings to prefix generated header with
53        (usually a copyright statement + calling convention macros).
54        - apicall - string to use for the function declaration prefix,
55        such as APICALL on Windows.
56        - apientry - string to use for the calling convention macro,
57        in typedefs, such as APIENTRY.
58        - apientryp - string to use for the calling convention macro
59        in function pointer typedefs, such as APIENTRYP.
60        - indentFuncProto - True if prototype declarations should put each
61        parameter on a separate line
62        - indentFuncPointer - True if typedefed function pointers should put each
63        parameter on a separate line
64        - alignFuncParam - if nonzero and parameters are being put on a
65        separate line, align parameter names at the specified column
66
67        Additional parameters/members:
68
69        - expandEnumerants - if True, add BEGIN/END_RANGE macros in enumerated
70        type declarations
71        - secondaryInclude - if True, add secondary (no xref anchor) versions
72        of generated files
73        - extEnumerantAdditions - if True, include enumerants added by extensions
74        in comment tables for core enumeration types.
75        - extEnumerantFormatString - A format string for any additional message for
76        enumerants from extensions if extEnumerantAdditions is True. The correctly-
77        marked-up extension name will be passed.
78        """
79        GeneratorOptions.__init__(self, **kwargs)
80        self.prefixText = prefixText
81        """list of strings to prefix generated header with (usually a copyright statement + calling convention macros)."""
82
83        self.apicall = apicall
84        """string to use for the function declaration prefix, such as APICALL on Windows."""
85
86        self.apientry = apientry
87        """string to use for the calling convention macro, in typedefs, such as APIENTRY."""
88
89        self.apientryp = apientryp
90        """string to use for the calling convention macro in function pointer typedefs, such as APIENTRYP."""
91
92        self.indentFuncProto = indentFuncProto
93        """True if prototype declarations should put each parameter on a separate line"""
94
95        self.indentFuncPointer = indentFuncPointer
96        """True if typedefed function pointers should put each parameter on a separate line"""
97
98        self.alignFuncParam = alignFuncParam
99        """if nonzero and parameters are being put on a separate line, align parameter names at the specified column"""
100
101        self.secondaryInclude = secondaryInclude
102        """if True, add secondary (no xref anchor) versions of generated files"""
103
104        self.expandEnumerants = expandEnumerants
105        """if True, add BEGIN/END_RANGE macros in enumerated type declarations"""
106
107        self.extEnumerantAdditions = extEnumerantAdditions
108        """if True, include enumerants added by extensions in comment tables for core enumeration types."""
109
110        self.extEnumerantFormatString = extEnumerantFormatString
111        """A format string for any additional message for
112        enumerants from extensions if extEnumerantAdditions is True. The correctly-
113        marked-up extension name will be passed."""
114
115
116class DocOutputGenerator(OutputGenerator):
117    """DocOutputGenerator - subclass of OutputGenerator.
118
119    Generates AsciiDoc includes with C-language API interfaces, for reference
120    pages and the corresponding specification. Similar to COutputGenerator,
121    but each interface is written into a different file as determined by the
122    options, only actual C types are emitted, and none of the boilerplate
123    preprocessor code is emitted."""
124
125    def __init__(self, *args, **kwargs):
126        super().__init__(*args, **kwargs)
127        # Keep track of all extension numbers
128        self.extension_numbers = set()
129
130    def beginFile(self, genOpts):
131        OutputGenerator.beginFile(self, genOpts)
132
133        # This should be a separate conventions property rather than an
134        # inferred type name pattern for different APIs.
135        self.result_type = genOpts.conventions.type_prefix + "Result"
136
137    def endFile(self):
138        OutputGenerator.endFile(self)
139
140    def beginFeature(self, interface, emit):
141        # Start processing in superclass
142        OutputGenerator.beginFeature(self, interface, emit)
143
144        # Decide if we're in a core <feature> or an <extension>
145        self.in_core = (interface.tag == 'feature')
146
147        # Verify that each <extension> has a unique number during doc
148        # generation
149        # TODO move this to consistency_tools
150        if not self.in_core:
151            extension_number = interface.get('number')
152            if extension_number is not None and extension_number != "0":
153                if extension_number in self.extension_numbers:
154                    self.logMsg('error', 'Duplicate extension number ', extension_number, ' detected in feature ', interface.get('name'), '\n')
155                    exit(1)
156                else:
157                    self.extension_numbers.add(extension_number)
158
159    def endFeature(self):
160        # Finish processing in superclass
161        OutputGenerator.endFeature(self)
162
163    def genRequirements(self, name, mustBeFound = True):
164        """Generate text showing what core versions and extensions introduce
165        an API. This relies on the map in api.py, which may be loaded at
166        runtime into self.apidict. If not present, no message is
167        generated.
168
169        - name - name of the API
170        - mustBeFound - If True, when requirements for 'name' cannot be
171          determined, a warning comment is generated.
172        """
173
174        if self.apidict:
175            if name in self.apidict.requiredBy:
176                features = []
177                for (base,dependency) in self.apidict.requiredBy[name]:
178                    if dependency is not None:
179                        features.append('{} with {}'.format(base, dependency))
180                    else:
181                        features.append(base)
182                return '// Provided by {}\n'.format(', '.join(features))
183            else:
184                if mustBeFound:
185                    self.logMsg('warn', 'genRequirements: API {} not found'.format(name))
186                return ''
187        else:
188            # No API dictionary available, return nothing
189            return ''
190
191    def writeInclude(self, directory, basename, contents):
192        """Generate an include file.
193
194        - directory - subdirectory to put file in
195        - basename - base name of the file
196        - contents - contents of the file (Asciidoc boilerplate aside)"""
197        # Create subdirectory, if needed
198        directory = self.genOpts.directory + '/' + directory
199        self.makeDir(directory)
200
201        # Create file
202        filename = directory + '/' + basename + '.txt'
203        self.logMsg('diag', '# Generating include file:', filename)
204        fp = open(filename, 'w', encoding='utf-8')
205
206        # Asciidoc anchor
207        write(self.genOpts.conventions.warning_comment, file=fp)
208        write('[[{0},{0}]]'.format(basename), file=fp)
209
210        if self.genOpts.conventions.generate_index_terms:
211            index_terms = []
212            if basename.startswith(self.conventions.command_prefix):
213                index_terms.append(basename[2:] + " (function)")
214            elif basename.startswith(self.conventions.type_prefix):
215                index_terms.append(basename[2:] + " (type)")
216            elif basename.startswith(self.conventions.api_prefix):
217                index_terms.append(basename[len(self.conventions.api_prefix):] + " (define)")
218            index_terms.append(basename)
219            write('indexterm:[{}]'.format(','.join(index_terms)), file=fp)
220
221        write('[source,c++]', file=fp)
222        write('----', file=fp)
223        write(contents, file=fp)
224        write('----', file=fp)
225        fp.close()
226
227        if self.genOpts.secondaryInclude:
228            # Create secondary no cross-reference include file
229            filename = directory + '/' + basename + '.no-xref.txt'
230            self.logMsg('diag', '# Generating include file:', filename)
231            fp = open(filename, 'w', encoding='utf-8')
232
233            # Asciidoc anchor
234            write(self.genOpts.conventions.warning_comment, file=fp)
235            write('// Include this no-xref version without cross reference id for multiple includes of same file', file=fp)
236            write('[source,c++]', file=fp)
237            write('----', file=fp)
238            write(contents, file=fp)
239            write('----', file=fp)
240            fp.close()
241
242    def writeTable(self, basename, values):
243        """Output a table of enumerants."""
244        directory = Path(self.genOpts.directory) / 'enums'
245        self.makeDir(str(directory))
246
247        filename = str(directory / '{}.comments.txt'.format(basename))
248        self.logMsg('diag', '# Generating include file:', filename)
249
250        with open(filename, 'w', encoding='utf-8') as fp:
251            write(self.conventions.warning_comment, file=fp)
252            write(ENUM_TABLE_PREFIX, file=fp)
253
254            for data in values:
255                write("|ename:{}".format(data['name']), file=fp)
256                write("|{}".format(data['comment']), file=fp)
257
258            write(ENUM_TABLE_SUFFIX, file=fp)
259
260    def writeFlagBox(self, basename, values):
261        """Output a box of flag bit comments."""
262        directory = Path(self.genOpts.directory) / 'enums'
263        self.makeDir(str(directory))
264
265        filename = str(directory / '{}.comments.txt'.format(basename))
266        self.logMsg('diag', '# Generating include file:', filename)
267
268        with open(filename, 'w', encoding='utf-8') as fp:
269            write(self.conventions.warning_comment, file=fp)
270            write(FLAG_BLOCK_PREFIX, file=fp)
271
272            for data in values:
273                write("* ename:{} -- {}".format(data['name'],
274                                                data['comment']),
275                      file=fp)
276
277            write(FLAG_BLOCK_SUFFIX, file=fp)
278
279    def genType(self, typeinfo, name, alias):
280        """Generate type."""
281        OutputGenerator.genType(self, typeinfo, name, alias)
282        typeElem = typeinfo.elem
283        # If the type is a struct type, traverse the embedded <member> tags
284        # generating a structure. Otherwise, emit the tag text.
285        category = typeElem.get('category')
286
287        if category in ('struct', 'union'):
288            # If the type is a struct type, generate it using the
289            # special-purpose generator.
290            self.genStruct(typeinfo, name, alias)
291        else:
292            body = self.genRequirements(name)
293            if alias:
294                # If the type is an alias, just emit a typedef declaration
295                body += 'typedef ' + alias + ' ' + name + ';\n'
296                self.writeInclude(OutputGenerator.categoryToPath[category],
297                                  name, body)
298            else:
299                # Replace <apientry /> tags with an APIENTRY-style string
300                # (from self.genOpts). Copy other text through unchanged.
301                # If the resulting text is an empty string, don't emit it.
302                body += noneStr(typeElem.text)
303                for elem in typeElem:
304                    if elem.tag == 'apientry':
305                        body += self.genOpts.apientry + noneStr(elem.tail)
306                    else:
307                        body += noneStr(elem.text) + noneStr(elem.tail)
308
309                if body:
310                    if category in OutputGenerator.categoryToPath:
311                        self.writeInclude(OutputGenerator.categoryToPath[category],
312                                          name, body + '\n')
313                    else:
314                        self.logMsg('diag', '# NOT writing include file for type:',
315                                    name, '- bad category: ', category)
316                else:
317                    self.logMsg('diag', '# NOT writing empty include file for type', name)
318
319    def genStruct(self, typeinfo, typeName, alias):
320        """Generate struct."""
321        OutputGenerator.genStruct(self, typeinfo, typeName, alias)
322
323        typeElem = typeinfo.elem
324
325        body = self.genRequirements(typeName)
326        if alias:
327            body += 'typedef ' + alias + ' ' + typeName + ';\n'
328        else:
329            body += 'typedef ' + typeElem.get('category') + ' ' + typeName + ' {\n'
330
331            targetLen = self.getMaxCParamTypeLength(typeinfo)
332            for member in typeElem.findall('.//member'):
333                body += self.makeCParamDecl(member, targetLen + 4)
334                body += ';\n'
335            body += '} ' + typeName + ';'
336
337        self.writeInclude('structs', typeName, body)
338
339    def genEnumTable(self, groupinfo, groupName):
340        """Generate tables of enumerant values and short descriptions from
341        the XML."""
342
343        values = []
344        got_comment = False
345        missing_comments = []
346        for elem in groupinfo.elem.findall('enum'):
347            if not elem.get('required'):
348                continue
349            name = elem.get('name')
350
351            data = {
352                'name': name,
353            }
354
355            (numVal, strVal) = self.enumToValue(elem, True)
356            data['value'] = numVal
357
358            extname = elem.get('extname')
359
360            added_by_extension_to_core = (extname is not None and self.in_core)
361            if added_by_extension_to_core and not self.genOpts.extEnumerantAdditions:
362                # We're skipping such values
363                continue
364
365            comment = elem.get('comment')
366            if comment:
367                got_comment = True
368            elif name.endswith('_UNKNOWN') and numVal == 0:
369                # This is a placeholder for 0-initialization to be clearly invalid.
370                # Just skip this silently
371                continue
372            else:
373                # Skip but record this in case it's an odd-one-out missing a comment.
374                missing_comments.append(name)
375                continue
376
377            if added_by_extension_to_core and self.genOpts.extEnumerantFormatString:
378                # Add a note to the comment
379                comment += self.genOpts.extEnumerantFormatString.format(
380                    self.conventions.formatExtension(extname))
381
382            data['comment'] = comment
383            values.append(data)
384
385        if got_comment:
386            # If any had a comment, output it.
387
388            if missing_comments:
389                self.logMsg('warn', 'The following values for', groupName,
390                            'were omitted from the table due to missing comment attributes:',
391                            ', '.join(missing_comments))
392
393            group_type = groupinfo.elem.get('type')
394            if groupName == self.result_type:
395                # Split this into success and failure
396                self.writeTable(groupName + '.success',
397                                (data for data in values
398                                 if data['value'] >= 0))
399                self.writeTable(groupName + '.error',
400                                (data for data in values
401                                 if data['value'] < 0))
402            elif group_type == 'bitmask':
403                self.writeFlagBox(groupName, values)
404            elif group_type == 'enum':
405                self.writeTable(groupName, values)
406            else:
407                raise RuntimeError("Unrecognized enums type: " + str(group_type))
408
409    def genGroup(self, groupinfo, groupName, alias):
410        """Generate group (e.g. C "enum" type)."""
411        OutputGenerator.genGroup(self, groupinfo, groupName, alias)
412
413        body = self.genRequirements(groupName)
414        if alias:
415            # If the group name is aliased, just emit a typedef declaration
416            # for the alias.
417            body += 'typedef ' + alias + ' ' + groupName + ';\n'
418        else:
419            expand = self.genOpts.expandEnumerants
420            (_, enumbody) = self.buildEnumCDecl(expand, groupinfo, groupName)
421            body += enumbody
422            if self.genOpts.conventions.generate_enum_table:
423                self.genEnumTable(groupinfo, groupName)
424
425        self.writeInclude('enums', groupName, body)
426
427    def genEnum(self, enuminfo, name, alias):
428        """Generate enumerant."""
429        OutputGenerator.genEnum(self, enuminfo, name, alias)
430        self.logMsg('diag', '# NOT writing compile-time constant', name)
431
432    def genCmd(self, cmdinfo, name, alias):
433        "Generate command."
434        OutputGenerator.genCmd(self, cmdinfo, name, alias)
435
436        return_type = cmdinfo.elem.find('proto/type')
437        if self.genOpts.conventions.requires_error_validation(return_type):
438            # This command returns an API result code, so check that it
439            # returns at least the required errors.
440            # TODO move this to consistency_tools
441            required_errors = set(self.genOpts.conventions.required_errors)
442            errorcodes = cmdinfo.elem.get('errorcodes').split(',')
443            if not required_errors.issubset(set(errorcodes)):
444                self.logMsg('error', 'Missing required error code for command: ', name, '\n')
445                exit(1)
446
447        body = self.genRequirements(name)
448        decls = self.makeCDecls(cmdinfo.elem)
449        body += decls[0]
450        self.writeInclude('protos', name, body)
451