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