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