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