1#!/usr/bin/python3 -i
2#
3# Copyright 2013-2024 The Khronos Group Inc.
4#
5# SPDX-License-Identifier: Apache-2.0
6"""Base class for source/header/doc generators, as well as some utility functions."""
7
8from __future__ import unicode_literals
9
10import io
11import os
12import pdb
13import re
14import shutil
15import sys
16import tempfile
17try:
18    from pathlib import Path
19except ImportError:
20    from pathlib2 import Path  # type: ignore
21
22from spec_tools.util import getElemName, getElemType
23
24
25def write(*args, **kwargs):
26    file = kwargs.pop('file', sys.stdout)
27    end = kwargs.pop('end', '\n')
28    file.write(' '.join(str(arg) for arg in args))
29    file.write(end)
30
31
32def noneStr(s):
33    """Return string argument, or "" if argument is None.
34
35    Used in converting etree Elements into text.
36    s - string to convert"""
37    if s:
38        return s
39    return ""
40
41
42def enquote(s):
43    """Return string argument with surrounding quotes,
44      for serialization into Python code."""
45    if s:
46        if isinstance(s, str):
47            return f"'{s}'"
48        else:
49            return s
50    return None
51
52
53def regSortCategoryKey(feature):
54    """Sort key for regSortFeatures.
55    Sorts by category of the feature name string:
56
57    - Core API features (those defined with a `<feature>` tag)
58        - (sort VKSC after VK - this is Vulkan-specific)
59    - ARB/KHR/OES (Khronos extensions)
60    - other       (EXT/vendor extensions)"""
61
62    if feature.elem.tag == 'feature':
63        if feature.name.startswith('VKSC'):
64            return 0.5
65        else:
66            return 0
67
68    if feature.category.upper() in ['ARB', 'KHR', 'OES']:
69        return 1
70
71    return 2
72
73
74def regSortOrderKey(feature):
75    """Sort key for regSortFeatures - key is the sortorder attribute."""
76
77    return feature.sortorder
78
79
80def regSortNameKey(feature):
81    """Sort key for regSortFeatures - key is the extension name."""
82
83    return feature.name
84
85
86def regSortFeatureVersionKey(feature):
87    """Sort key for regSortFeatures - key is the feature version.
88    `<extension>` elements all have version number 0."""
89
90    return float(feature.versionNumber)
91
92
93def regSortExtensionNumberKey(feature):
94    """Sort key for regSortFeatures - key is the extension number.
95    `<feature>` elements all have extension number 0."""
96
97    return int(feature.number)
98
99
100def regSortFeatures(featureList):
101    """Default sort procedure for features.
102
103    - Sorts by explicit sort order (default 0) relative to other features
104    - then by feature category ('feature' or 'extension'),
105    - then by version number (for features)
106    - then by extension number (for extensions)"""
107    featureList.sort(key=regSortExtensionNumberKey)
108    featureList.sort(key=regSortFeatureVersionKey)
109    featureList.sort(key=regSortCategoryKey)
110    featureList.sort(key=regSortOrderKey)
111
112
113class MissingGeneratorOptionsError(RuntimeError):
114    """Error raised when a Generator tries to do something that requires GeneratorOptions but it is None."""
115
116    def __init__(self, msg=None):
117        full_msg = 'Missing generator options object self.genOpts'
118        if msg:
119            full_msg += ': ' + msg
120        super().__init__(full_msg)
121
122
123class MissingRegistryError(RuntimeError):
124    """Error raised when a Generator tries to do something that requires a Registry object but it is None."""
125
126    def __init__(self, msg=None):
127        full_msg = 'Missing Registry object self.registry'
128        if msg:
129            full_msg += ': ' + msg
130        super().__init__(full_msg)
131
132
133class MissingGeneratorOptionsConventionsError(RuntimeError):
134    """Error raised when a Generator tries to do something that requires a Conventions object but it is None."""
135
136    def __init__(self, msg=None):
137        full_msg = 'Missing Conventions object self.genOpts.conventions'
138        if msg:
139            full_msg += ': ' + msg
140        super().__init__(full_msg)
141
142
143class GeneratorOptions:
144    """Base class for options used during header/documentation production.
145
146    These options are target language independent, and used by
147    Registry.apiGen() and by base OutputGenerator objects."""
148
149    def __init__(self,
150                 conventions=None,
151                 filename=None,
152                 directory='.',
153                 genpath=None,
154                 apiname=None,
155                 mergeApiNames=None,
156                 profile=None,
157                 versions='.*',
158                 emitversions='.*',
159                 defaultExtensions=None,
160                 addExtensions=None,
161                 removeExtensions=None,
162                 emitExtensions=None,
163                 emitSpirv=None,
164                 emitFormats=None,
165                 reparentEnums=True,
166                 sortProcedure=regSortFeatures,
167                 requireCommandAliases=False,
168                 requireDepends=True,
169                ):
170        """Constructor.
171
172        Arguments:
173
174        - conventions - may be mandatory for some generators:
175        an object that implements ConventionsBase
176        - filename - basename of file to generate, or None to write to stdout.
177        - directory - directory in which to generate filename
178        - genpath - path to previously generated files, such as apimap.py
179        - apiname - string matching `<api>` 'apiname' attribute, e.g. 'gl'.
180        - mergeApiNames - If not None, a comma separated list of API names
181          to merge into the API specified by 'apiname'
182        - profile - string specifying API profile , e.g. 'core', or None.
183        - versions - regex matching API versions to process interfaces for.
184        Normally `'.*'` or `'[0-9][.][0-9]'` to match all defined versions.
185        - emitversions - regex matching API versions to actually emit
186        interfaces for (though all requested versions are considered
187        when deciding which interfaces to generate). For GL 4.3 glext.h,
188        this might be `'1[.][2-5]|[2-4][.][0-9]'`.
189        - defaultExtensions - If not None, a string which must in its
190        entirety match the pattern in the "supported" attribute of
191        the `<extension>`. Defaults to None. Usually the same as apiname.
192        - addExtensions - regex matching names of additional extensions
193        to include. Defaults to None.
194        - removeExtensions - regex matching names of extensions to
195        remove (after defaultExtensions and addExtensions). Defaults
196        to None.
197        - emitExtensions - regex matching names of extensions to actually emit
198        interfaces for (though all requested versions are considered when
199        deciding which interfaces to generate). Defaults to None.
200        - emitSpirv - regex matching names of extensions and capabilities
201        to actually emit interfaces for.
202        - emitFormats - regex matching names of formats to actually emit
203        interfaces for.
204        - reparentEnums - move <enum> elements which extend an enumerated
205        type from <feature> or <extension> elements to the target <enums>
206        element. This is required for almost all purposes, but the
207        InterfaceGenerator relies on the list of interfaces in the <feature>
208        or <extension> being complete. Defaults to True.
209        - sortProcedure - takes a list of FeatureInfo objects and sorts
210        them in place to a preferred order in the generated output.
211        - requireCommandAliases - if True, treat command aliases
212        as required dependencies.
213        - requireDepends - whether to follow API dependencies when emitting
214        APIs.
215
216        Default is
217          - core API versions
218          - Khronos (ARB/KHR/OES) extensions
219          - All other extensions
220          - By core API version number or extension number in each group.
221
222        The regex patterns can be None or empty, in which case they match
223        nothing."""
224        self.conventions = conventions
225        """may be mandatory for some generators:
226        an object that implements ConventionsBase"""
227
228        self.filename = filename
229        "basename of file to generate, or None to write to stdout."
230
231        self.genpath = genpath
232        """path to previously generated files, such as apimap.py"""
233
234        self.directory = directory
235        "directory in which to generate filename"
236
237        self.apiname = apiname
238        "string matching `<api>` 'apiname' attribute, e.g. 'gl'."
239
240        self.mergeApiNames = mergeApiNames
241        "comma separated list of API names to merge into the API specified by 'apiname'"
242
243        self.profile = profile
244        "string specifying API profile , e.g. 'core', or None."
245
246        self.versions = self.emptyRegex(versions)
247        """regex matching API versions to process interfaces for.
248        Normally `'.*'` or `'[0-9][.][0-9]'` to match all defined versions."""
249
250        self.emitversions = self.emptyRegex(emitversions)
251        """regex matching API versions to actually emit
252        interfaces for (though all requested versions are considered
253        when deciding which interfaces to generate). For GL 4.3 glext.h,
254        this might be `'1[.][2-5]|[2-4][.][0-9]'`."""
255
256        self.defaultExtensions = defaultExtensions
257        """If not None, a string which must in its
258        entirety match the pattern in the "supported" attribute of
259        the `<extension>`. Defaults to None. Usually the same as apiname."""
260
261        self.addExtensions = self.emptyRegex(addExtensions)
262        """regex matching names of additional extensions
263        to include. Defaults to None."""
264
265        self.removeExtensions = self.emptyRegex(removeExtensions)
266        """regex matching names of extensions to
267        remove (after defaultExtensions and addExtensions). Defaults
268        to None."""
269
270        self.emitExtensions = self.emptyRegex(emitExtensions)
271        """regex matching names of extensions to actually emit
272        interfaces for (though all requested versions are considered when
273        deciding which interfaces to generate)."""
274
275        self.emitSpirv = self.emptyRegex(emitSpirv)
276        """regex matching names of extensions and capabilities
277        to actually emit interfaces for."""
278
279        self.emitFormats = self.emptyRegex(emitFormats)
280        """regex matching names of formats
281        to actually emit interfaces for."""
282
283        self.reparentEnums = reparentEnums
284        """boolean specifying whether to remove <enum> elements from
285        <feature> or <extension> when extending an <enums> type."""
286
287        self.sortProcedure = sortProcedure
288        """takes a list of FeatureInfo objects and sorts
289        them in place to a preferred order in the generated output.
290        Default is core API versions, ARB/KHR/OES extensions, all
291        other extensions, alphabetically within each group."""
292
293        self.codeGenerator = False
294        """True if this generator makes compilable code"""
295
296        self.registry = None
297        """Populated later with the registry object."""
298
299        self.requireCommandAliases = requireCommandAliases
300        """True if alias= attributes of <command> tags are transitively
301        required."""
302
303        self.requireDepends = requireDepends
304        """True if dependencies of API tags are transitively required."""
305
306    def emptyRegex(self, pat):
307        """Substitute a regular expression which matches no version
308        or extension names for None or the empty string."""
309        if not pat:
310            return '_nomatch_^'
311
312        return pat
313
314
315class OutputGenerator:
316    """Generate specified API interfaces in a specific style, such as a C header.
317
318    Base class for generating API interfaces.
319    Manages basic logic, logging, and output file control.
320    Derived classes actually generate formatted output.
321    """
322
323    # categoryToPath - map XML 'category' to include file directory name
324    categoryToPath = {
325        'bitmask': 'flags',
326        'enum': 'enums',
327        'funcpointer': 'funcpointers',
328        'handle': 'handles',
329        'define': 'defines',
330        'basetype': 'basetypes',
331    }
332
333    def breakName(self, name, msg):
334        """Break into debugger if this is a special name"""
335
336        # List of string names to break on
337        bad = (
338        )
339
340        if name in bad and True:
341            print('breakName {}: {}'.format(name, msg))
342            pdb.set_trace()
343
344    def __init__(self, errFile=sys.stderr, warnFile=sys.stderr, diagFile=sys.stdout):
345        """Constructor
346
347        - errFile, warnFile, diagFile - file handles to write errors,
348          warnings, diagnostics to. May be None to not write."""
349        self.outFile = None
350        self.errFile = errFile
351        self.warnFile = warnFile
352        self.diagFile = diagFile
353        # Internal state
354        self.featureName = None
355        """The current feature name being generated."""
356
357        self.genOpts = None
358        """The GeneratorOptions subclass instance."""
359
360        self.registry = None
361        """The specification registry object."""
362
363        self.featureDictionary = {}
364        """The dictionary of dictionaries of API features."""
365
366        # Used for extension enum value generation
367        self.extBase = 1000000000
368        self.extBlockSize = 1000
369        self.madeDirs = {}
370
371        # API dictionary, which may be loaded by the beginFile method of
372        # derived generators.
373        self.apidict = None
374
375        # File suffix for generated files, set in beginFile below.
376        self.file_suffix = ''
377
378    def logMsg(self, level, *args):
379        """Write a message of different categories to different
380        destinations.
381
382        - `level`
383          - 'diag' (diagnostic, voluminous)
384          - 'warn' (warning)
385          - 'error' (fatal error - raises exception after logging)
386
387        - `*args` - print()-style arguments to direct to corresponding log"""
388        if level == 'error':
389            strfile = io.StringIO()
390            write('ERROR:', *args, file=strfile)
391            if self.errFile is not None:
392                write(strfile.getvalue(), file=self.errFile)
393            raise UserWarning(strfile.getvalue())
394        elif level == 'warn':
395            if self.warnFile is not None:
396                write('WARNING:', *args, file=self.warnFile)
397        elif level == 'diag':
398            if self.diagFile is not None:
399                write('DIAG:', *args, file=self.diagFile)
400        else:
401            raise UserWarning(
402                '*** FATAL ERROR in Generator.logMsg: unknown level:' + level)
403
404    def enumToValue(self, elem, needsNum, bitwidth = 32,
405                    forceSuffix = False, parent_for_alias_dereference=None):
406        """Parse and convert an `<enum>` tag into a value.
407
408        - elem - <enum> Element
409        - needsNum - generate a numeric representation of the element value
410        - bitwidth - size of the numeric representation in bits (32 or 64)
411        - forceSuffix - if True, always use a 'U' / 'ULL' suffix on integers
412        - parent_for_alias_dereference - if not None, an Element containing
413          the parent of elem, used to look for elements this is an alias of
414
415        Returns a list:
416
417        - first element - integer representation of the value, or None
418          if needsNum is False. The value must be a legal number
419          if needsNum is True.
420        - second element - string representation of the value
421
422        There are several possible representations of values.
423
424        - A 'value' attribute simply contains the value.
425        - A 'bitpos' attribute defines a value by specifying the bit
426          position which is set in that value.
427        - An 'offset','extbase','extends' triplet specifies a value
428          as an offset to a base value defined by the specified
429          'extbase' extension name, which is then cast to the
430          typename specified by 'extends'. This requires probing
431          the registry database, and imbeds knowledge of the
432          API extension enum scheme in this function.
433        - An 'alias' attribute contains the name of another enum
434          which this is an alias of. The other enum must be
435          declared first when emitting this enum."""
436        if self.genOpts is None:
437            raise MissingGeneratorOptionsError()
438        if self.genOpts.conventions is None:
439            raise MissingGeneratorOptionsConventionsError()
440
441        name = elem.get('name')
442        numVal = None
443        if 'value' in elem.keys():
444            value = elem.get('value')
445            # print('About to translate value =', value, 'type =', type(value))
446            if needsNum:
447                numVal = int(value, 0)
448            # If there is a non-integer, numeric 'type' attribute (e.g. 'u' or
449            # 'ull'), append it to the string value.
450            # t = enuminfo.elem.get('type')
451            # if t is not None and t != '' and t != 'i' and t != 's':
452            #     value += enuminfo.type
453            if forceSuffix:
454              if bitwidth == 64:
455                value = value + 'ULL'
456              else:
457                value = value + 'U'
458            self.logMsg('diag', 'Enum', name, '-> value [', numVal, ',', value, ']')
459            return [numVal, value]
460        if 'bitpos' in elem.keys():
461            value = elem.get('bitpos')
462            bitpos = int(value, 0)
463            numVal = 1 << bitpos
464            value = '0x%08x' % numVal
465            if bitwidth == 64 or bitpos >= 32:
466              value = value + 'ULL'
467            elif forceSuffix:
468              value = value + 'U'
469            self.logMsg('diag', 'Enum', name, '-> bitpos [', numVal, ',', value, ']')
470            return [numVal, value]
471        if 'offset' in elem.keys():
472            # Obtain values in the mapping from the attributes
473            enumNegative = False
474            offset = int(elem.get('offset'), 0)
475            extnumber = int(elem.get('extnumber'), 0)
476            extends = elem.get('extends')
477            if 'dir' in elem.keys():
478                enumNegative = True
479            self.logMsg('diag', 'Enum', name, 'offset =', offset,
480                        'extnumber =', extnumber, 'extends =', extends,
481                        'enumNegative =', enumNegative)
482            # Now determine the actual enumerant value, as defined
483            # in the "Layers and Extensions" appendix of the spec.
484            numVal = self.extBase + (extnumber - 1) * self.extBlockSize + offset
485            if enumNegative:
486                numVal *= -1
487            value = '%d' % numVal
488            # More logic needed!
489            self.logMsg('diag', 'Enum', name, '-> offset [', numVal, ',', value, ']')
490            return [numVal, value]
491        if 'alias' in elem.keys():
492            alias_of = elem.get('alias')
493            if parent_for_alias_dereference is None:
494                return (None, alias_of)
495            siblings = parent_for_alias_dereference.findall('enum')
496            for sib in siblings:
497                sib_name = sib.get('name')
498                if sib_name == alias_of:
499                    return self.enumToValue(sib, needsNum)
500            raise RuntimeError("Could not find the aliased enum value")
501        return [None, None]
502
503    def checkDuplicateEnums(self, enums):
504        """Check enumerated values for duplicates.
505
506        -  enums - list of `<enum>` Elements
507
508        returns the list with duplicates stripped"""
509        # Dictionaries indexed by name and numeric value.
510        # Entries are [ Element, numVal, strVal ] matching name or value
511
512        nameMap = {}
513        valueMap = {}
514
515        stripped = []
516        for elem in enums:
517            name = elem.get('name')
518            (numVal, strVal) = self.enumToValue(elem, True)
519
520            if name in nameMap:
521                # Duplicate name found; check values
522                (name2, numVal2, strVal2) = nameMap[name]
523
524                # Duplicate enum values for the same name are benign. This
525                # happens when defining the same enum conditionally in
526                # several extension blocks.
527                if (strVal2 == strVal or (numVal is not None
528                                          and numVal == numVal2)):
529                    True
530                    # self.logMsg('info', 'checkDuplicateEnums: Duplicate enum (' + name +
531                    #             ') found with the same value:' + strVal)
532                else:
533                    self.logMsg('warn', 'checkDuplicateEnums: Duplicate enum (' + name
534                                + ') found with different values:' + strVal
535                                + ' and ' + strVal2)
536
537                # Do not add the duplicate to the returned list
538                continue
539            elif numVal in valueMap:
540                # Duplicate value found (such as an alias); report it, but
541                # still add this enum to the list.
542                (name2, numVal2, strVal2) = valueMap[numVal]
543
544                msg = 'Two enums found with the same value: {} = {} = {}'.format(
545                    name, name2.get('name'), strVal)
546                self.logMsg('error', msg)
547
548            # Track this enum to detect followon duplicates
549            nameMap[name] = [elem, numVal, strVal]
550            if numVal is not None:
551                valueMap[numVal] = [elem, numVal, strVal]
552
553            # Add this enum to the list
554            stripped.append(elem)
555
556        # Return the list
557        return stripped
558
559    def misracstyle(self):
560        return False;
561
562    def misracppstyle(self):
563        return False;
564
565    def buildEnumCDecl(self, expand, groupinfo, groupName):
566        """Generate the C declaration for an enum"""
567        if self.genOpts is None:
568            raise MissingGeneratorOptionsError()
569        if self.genOpts.conventions is None:
570            raise MissingGeneratorOptionsConventionsError()
571
572        groupElem = groupinfo.elem
573
574        # Determine the required bit width for the enum group.
575        # 32 is the default, which generates C enum types for the values.
576        bitwidth = 32
577
578        # If the constFlagBits preference is set, 64 is the default for bitmasks
579        if self.genOpts.conventions.constFlagBits and groupElem.get('type') == 'bitmask':
580            bitwidth = 64
581
582        # Check for an explicitly defined bitwidth, which will override any defaults.
583        if groupElem.get('bitwidth'):
584            try:
585                bitwidth = int(groupElem.get('bitwidth'))
586            except ValueError as ve:
587                self.logMsg('error', 'Invalid value for bitwidth attribute (', groupElem.get('bitwidth'), ') for ', groupName, ' - must be an integer value\n')
588                exit(1)
589
590        usebitmask = False
591        usedefine = False
592
593        # Bitmask flags can be generated as either "static const uint{32,64}_t" values,
594        # or as 32-bit C enums. 64-bit types must use uint64_t values.
595        if groupElem.get('type') == 'bitmask':
596            if bitwidth > 32 or self.misracppstyle():
597                usebitmask = True
598            if self.misracstyle():
599                usedefine = True
600
601        if usedefine or usebitmask:
602            # Validate the bitwidth and generate values appropriately
603            if bitwidth > 64:
604                self.logMsg('error', 'Invalid value for bitwidth attribute (', groupElem.get('bitwidth'), ') for bitmask type ', groupName, ' - must be less than or equal to 64\n')
605                exit(1)
606            else:
607                return self.buildEnumCDecl_BitmaskOrDefine(groupinfo, groupName, bitwidth, usedefine)
608        else:
609            # Validate the bitwidth and generate values appropriately
610            if bitwidth > 32:
611                self.logMsg('error', 'Invalid value for bitwidth attribute (', groupElem.get('bitwidth'), ') for enum type ', groupName, ' - must be less than or equal to 32\n')
612                exit(1)
613            else:
614                return self.buildEnumCDecl_Enum(expand, groupinfo, groupName)
615
616    def buildEnumCDecl_BitmaskOrDefine(self, groupinfo, groupName, bitwidth, usedefine):
617        """Generate the C declaration for an "enum" that is actually a
618        set of flag bits"""
619        groupElem = groupinfo.elem
620        flagTypeName = groupElem.get('name')
621
622        # Prefix
623        body = "// Flag bits for " + flagTypeName + "\n"
624
625        if bitwidth == 64:
626            body += "typedef VkFlags64 %s;\n" % flagTypeName;
627        else:
628            body += "typedef VkFlags %s;\n" % flagTypeName;
629
630        # Maximum allowable value for a flag (unsigned 64-bit integer)
631        maxValidValue = 2**(64) - 1
632        minValidValue = 0
633
634        # Get a list of nested 'enum' tags.
635        enums = groupElem.findall('enum')
636
637        # Check for and report duplicates, and return a list with them
638        # removed.
639        enums = self.checkDuplicateEnums(enums)
640
641        # Accumulate non-numeric enumerant values separately and append
642        # them following the numeric values, to allow for aliases.
643        # NOTE: this does not do a topological sort yet, so aliases of
644        # aliases can still get in the wrong order.
645        aliasText = ''
646
647        # Loop over the nested 'enum' tags.
648        for elem in enums:
649            # Convert the value to an integer and use that to track min/max.
650            # Values of form -(number) are accepted but nothing more complex.
651            # Should catch exceptions here for more complex constructs. Not yet.
652            (numVal, strVal) = self.enumToValue(elem, True, bitwidth, True)
653            name = elem.get('name')
654
655            # Range check for the enum value
656            if numVal is not None and (numVal > maxValidValue or numVal < minValidValue):
657                self.logMsg('error', 'Allowable range for flag types in C is [', minValidValue, ',', maxValidValue, '], but', name, 'flag has a value outside of this (', strVal, ')\n')
658                exit(1)
659
660            decl = self.genRequirements(name, mustBeFound = False)
661
662            if self.isEnumRequired(elem):
663                protect = elem.get('protect')
664                if protect is not None:
665                    body += '#ifdef {}\n'.format(protect)
666
667                if usedefine:
668                    decl += "#define {} {}\n".format(name, strVal)
669                elif self.misracppstyle():
670                    decl += "static constexpr {} {} {{{}}};\n".format(flagTypeName, name, strVal)
671                else:
672                    # Some C compilers only allow initializing a 'static const' variable with a literal value.
673                    # So initializing an alias from another 'static const' value would fail to compile.
674                    # Work around this by chasing the aliases to get the actual value.
675                    while numVal is None:
676                        alias = self.registry.tree.find("enums/enum[@name='" + strVal + "']")
677                        if alias is not None:
678                            (numVal, strVal) = self.enumToValue(alias, True, bitwidth, True)
679                        else:
680                            self.logMsg('error', 'No such alias {} for enum {}'.format(strVal, name))
681                    decl += "static const {} {} = {};\n".format(flagTypeName, name, strVal)
682
683                if numVal is not None:
684                    body += decl
685                else:
686                    aliasText += decl
687
688                if protect is not None:
689                    body += '#endif\n'
690
691        # Now append the non-numeric enumerant values
692        body += aliasText
693
694        # Postfix
695
696        return ("bitmask", body)
697
698    def buildEnumCDecl_Enum(self, expand, groupinfo, groupName):
699        """Generate the C declaration for an enumerated type"""
700        groupElem = groupinfo.elem
701
702        # Break the group name into prefix and suffix portions for range
703        # enum generation
704        expandName = re.sub(r'([0-9]+|[a-z_])([A-Z0-9])', r'\1_\2', groupName).upper()
705        expandPrefix = expandName
706        expandSuffix = ''
707        expandSuffixMatch = re.search(r'[A-Z][A-Z]+$', groupName)
708        if expandSuffixMatch:
709            expandSuffix = '_' + expandSuffixMatch.group()
710            # Strip off the suffix from the prefix
711            expandPrefix = expandName.rsplit(expandSuffix, 1)[0]
712
713        # Prefix
714        body = ["typedef enum %s {" % groupName]
715
716        # @@ Should use the type="bitmask" attribute instead
717        isEnum = ('FLAG_BITS' not in expandPrefix)
718
719        # Allowable range for a C enum - which is that of a signed 32-bit integer
720        maxValidValue = 2**(32 - 1) - 1
721        minValidValue = (maxValidValue * -1) - 1
722
723        # Get a list of nested 'enum' tags.
724        enums = groupElem.findall('enum')
725
726        # Check for and report duplicates, and return a list with them
727        # removed.
728        enums = self.checkDuplicateEnums(enums)
729
730        # Loop over the nested 'enum' tags. Keep track of the minimum and
731        # maximum numeric values, if they can be determined; but only for
732        # core API enumerants, not extension enumerants. This is inferred
733        # by looking for 'extends' attributes.
734        minName = None
735
736        # Accumulate non-numeric enumerant values separately and append
737        # them following the numeric values, to allow for aliases.
738        # NOTE: this does not do a topological sort yet, so aliases of
739        # aliases can still get in the wrong order.
740        aliasText = []
741
742        maxName = None
743        minValue = None
744        maxValue = None
745        for elem in enums:
746            # Convert the value to an integer and use that to track min/max.
747            # Values of form -(number) are accepted but nothing more complex.
748            # Should catch exceptions here for more complex constructs. Not yet.
749            (numVal, strVal) = self.enumToValue(elem, True)
750            name = elem.get('name')
751
752            # Extension enumerants are only included if they are required
753            if self.isEnumRequired(elem):
754                decl = ''
755
756                protect = elem.get('protect')
757                if protect is not None:
758                    decl += '#ifdef {}\n'.format(protect)
759
760                # Indent requirements comment, if there is one
761                requirements = self.genRequirements(name, mustBeFound = False)
762                if requirements != '':
763                    requirements = '  ' + requirements
764                decl += requirements
765                decl += '    {} = {},'.format(name, strVal)
766
767                if protect is not None:
768                    decl += '\n#endif'
769
770                if numVal is not None:
771                    body.append(decl)
772                else:
773                    aliasText.append(decl)
774
775            # Range check for the enum value
776            if numVal is not None and (numVal > maxValidValue or numVal < minValidValue):
777                self.logMsg('error', 'Allowable range for C enum types is [', minValidValue, ',', maxValidValue, '], but', name, 'has a value outside of this (', strVal, ')\n')
778                exit(1)
779
780            # Do not track min/max for non-numbers (numVal is None)
781            if isEnum and numVal is not None and elem.get('extends') is None:
782                if minName is None:
783                    minName = maxName = name
784                    minValue = maxValue = numVal
785                elif minValue is None or numVal < minValue:
786                    minName = name
787                    minValue = numVal
788                elif maxValue is None or numVal > maxValue:
789                    maxName = name
790                    maxValue = numVal
791
792        # Now append the non-numeric enumerant values
793        body.extend(aliasText)
794
795        # Generate min/max value tokens - legacy use case.
796        if isEnum and expand:
797            body.extend((f'    {expandPrefix}_BEGIN_RANGE{expandSuffix} = {minName},',
798                         f'    {expandPrefix}_END_RANGE{expandSuffix} = {maxName},',
799                         f'    {expandPrefix}_RANGE_SIZE{expandSuffix} = ({maxName} - {minName} + 1),'))
800
801        # Generate a range-padding value to ensure the enum is 32 bits, but
802        # only in code generators, so it does not appear in documentation
803        if (self.genOpts.codeGenerator or
804            self.conventions.generate_max_enum_in_docs):
805            body.append(f'    {expandPrefix}_MAX_ENUM{expandSuffix} = 0x7FFFFFFF')
806
807        # Postfix
808        body.append("} %s;" % groupName)
809
810        # Determine appropriate section for this declaration
811        if groupElem.get('type') == 'bitmask':
812            section = 'bitmask'
813        else:
814            section = 'group'
815
816        return (section, '\n'.join(body))
817
818    def buildConstantCDecl(self, enuminfo, name, alias):
819        """Generate the C declaration for a constant (a single <enum>
820        value).
821
822        <enum> tags may specify their values in several ways, but are
823        usually just integers or floating-point numbers."""
824
825        (_, strVal) = self.enumToValue(enuminfo.elem, False)
826
827        if self.misracppstyle() and enuminfo.elem.get('type') and not alias:
828            # Generate e.g.: static constexpr uint32_t x = ~static_cast<uint32_t>(1U);
829            # This appeases MISRA "underlying type" rules.
830            typeStr = enuminfo.elem.get('type');
831            invert = '~' in strVal
832            number = strVal.strip("()~UL")
833            if typeStr != "float":
834                number += 'U'
835            strVal = "~" if invert else ""
836            strVal += "static_cast<" + typeStr + ">(" + number + ")"
837            body = 'static constexpr ' + typeStr.ljust(9) + name.ljust(33) + ' {' + strVal + '};'
838        elif enuminfo.elem.get('type') and not alias:
839            # Generate e.g.: #define x (~0ULL)
840            typeStr = enuminfo.elem.get('type');
841            invert = '~' in strVal
842            paren = '(' in strVal
843            number = strVal.strip("()~UL")
844            if typeStr != "float":
845                if typeStr == "uint64_t":
846                    number += 'ULL'
847                else:
848                    number += 'U'
849            strVal = "~" if invert else ""
850            strVal += number
851            if paren:
852                strVal = "(" + strVal + ")";
853            body = '#define ' + name.ljust(33) + ' ' + strVal;
854        else:
855            body = '#define ' + name.ljust(33) + ' ' + strVal
856
857        return body
858
859    def makeDir(self, path):
860        """Create a directory, if not already done.
861
862        Generally called from derived generators creating hierarchies."""
863        self.logMsg('diag', 'OutputGenerator::makeDir(' + path + ')')
864        if path not in self.madeDirs:
865            # This can get race conditions with multiple writers, see
866            # https://stackoverflow.com/questions/273192/
867            if not os.path.exists(path):
868                os.makedirs(path)
869            self.madeDirs[path] = None
870
871    def beginFile(self, genOpts):
872        """Start a new interface file
873
874        - genOpts - GeneratorOptions controlling what is generated and how"""
875
876        self.genOpts = genOpts
877        if self.genOpts is None:
878            raise MissingGeneratorOptionsError()
879        if self.genOpts.conventions is None:
880            raise MissingGeneratorOptionsConventionsError()
881        self.should_insert_may_alias_macro = \
882            self.genOpts.conventions.should_insert_may_alias_macro(self.genOpts)
883        self.file_suffix = self.genOpts.conventions.file_suffix
884
885        # Try to import the API dictionary, apimap.py, if it exists. Nothing
886        # in apimap.py cannot be extracted directly from the XML, and in the
887        # future we should do that.
888        if self.genOpts.genpath is not None:
889            try:
890                sys.path.insert(0, self.genOpts.genpath)
891                import apimap
892                self.apidict = apimap
893            except ImportError:
894                self.apidict = None
895
896        self.conventions = genOpts.conventions
897
898        # Open a temporary file for accumulating output.
899        if self.genOpts.filename is not None:
900            self.outFile = tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', newline='\n', delete=False)
901        else:
902            self.outFile = sys.stdout
903
904    def endFile(self):
905        if self.errFile:
906            self.errFile.flush()
907        if self.warnFile:
908            self.warnFile.flush()
909        if self.diagFile:
910            self.diagFile.flush()
911        if self.outFile:
912            self.outFile.flush()
913            if self.outFile != sys.stdout and self.outFile != sys.stderr:
914                self.outFile.close()
915
916            if self.genOpts is None:
917                raise MissingGeneratorOptionsError()
918
919            # On successfully generating output, move the temporary file to the
920            # target file.
921            if self.genOpts.filename is not None:
922                if sys.platform == 'win32':
923                    directory = Path(self.genOpts.directory)
924                    if not Path.exists(directory):
925                        os.makedirs(directory)
926                shutil.copy(self.outFile.name, self.genOpts.directory + '/' + self.genOpts.filename)
927                os.remove(self.outFile.name)
928        self.genOpts = None
929
930    def beginFeature(self, interface, emit):
931        """Write interface for a feature and tag generated features as having been done.
932
933        - interface - element for the `<version>` / `<extension>` to generate
934        - emit - actually write to the header only when True"""
935        self.emit = emit
936        self.featureName = interface.get('name')
937        # If there is an additional 'protect' attribute in the feature, save it
938        self.featureExtraProtect = interface.get('protect')
939
940    def endFeature(self):
941        """Finish an interface file, closing it when done.
942
943        Derived classes responsible for emitting feature"""
944        self.featureName = None
945        self.featureExtraProtect = None
946
947    def genRequirements(self, name, mustBeFound = True):
948        """Generate text showing what core versions and extensions introduce
949        an API. This exists in the base Generator class because it is used by
950        the shared enumerant-generating interfaces (buildEnumCDecl, etc.).
951        Here it returns an empty string for most generators, but can be
952        overridden by e.g. DocGenerator.
953
954        - name - name of the API
955        - mustBeFound - If True, when requirements for 'name' cannot be
956          determined, a warning comment is generated.
957        """
958
959        return ''
960
961    def validateFeature(self, featureType, featureName):
962        """Validate we are generating something only inside a `<feature>` tag"""
963        if self.featureName is None:
964            raise UserWarning('Attempt to generate', featureType,
965                              featureName, 'when not in feature')
966
967    def genType(self, typeinfo, name, alias):
968        """Generate interface for a type
969
970        - typeinfo - TypeInfo for a type
971
972        Extend to generate as desired in your derived class."""
973        self.validateFeature('type', name)
974
975    def genStruct(self, typeinfo, typeName, alias):
976        """Generate interface for a C "struct" type.
977
978        - typeinfo - TypeInfo for a type interpreted as a struct
979
980        Extend to generate as desired in your derived class."""
981        self.validateFeature('struct', typeName)
982
983        # The mixed-mode <member> tags may contain no-op <comment> tags.
984        # It is convenient to remove them here where all output generators
985        # will benefit.
986        for member in typeinfo.elem.findall('.//member'):
987            for comment in member.findall('comment'):
988                member.remove(comment)
989
990    def genGroup(self, groupinfo, groupName, alias):
991        """Generate interface for a group of enums (C "enum")
992
993        - groupinfo - GroupInfo for a group.
994
995        Extend to generate as desired in your derived class."""
996
997        self.validateFeature('group', groupName)
998
999    def genEnum(self, enuminfo, typeName, alias):
1000        """Generate interface for an enum (constant).
1001
1002        - enuminfo - EnumInfo for an enum
1003        - name - enum name
1004
1005        Extend to generate as desired in your derived class."""
1006        self.validateFeature('enum', typeName)
1007
1008    def genCmd(self, cmd, cmdinfo, alias):
1009        """Generate interface for a command.
1010
1011        - cmdinfo - CmdInfo for a command
1012
1013        Extend to generate as desired in your derived class."""
1014        self.validateFeature('command', cmdinfo)
1015
1016    def genSpirv(self, spirv, spirvinfo, alias):
1017        """Generate interface for a spirv element.
1018
1019        - spirvinfo - SpirvInfo for a command
1020
1021        Extend to generate as desired in your derived class."""
1022        return
1023
1024    def genFormat(self, format, formatinfo, alias):
1025        """Generate interface for a format element.
1026
1027        - formatinfo - FormatInfo
1028
1029        Extend to generate as desired in your derived class."""
1030        return
1031
1032    def genSyncStage(self, stageinfo):
1033        """Generate interface for a sync stage element.
1034
1035        - stageinfo - SyncStageInfo
1036
1037        Extend to generate as desired in your derived class."""
1038        return
1039
1040    def genSyncAccess(self, accessinfo):
1041        """Generate interface for a sync stage element.
1042
1043        - accessinfo - AccessInfo
1044
1045        Extend to generate as desired in your derived class."""
1046        return
1047
1048    def genSyncPipeline(self, pipelineinfo):
1049        """Generate interface for a sync stage element.
1050
1051        - pipelineinfo - SyncPipelineInfo
1052
1053        Extend to generate as desired in your derived class."""
1054        return
1055
1056    def makeProtoName(self, name, tail):
1057        """Turn a `<proto>` `<name>` into C-language prototype
1058        and typedef declarations for that name.
1059
1060        - name - contents of `<name>` tag
1061        - tail - whatever text follows that tag in the Element"""
1062        if self.genOpts is None:
1063            raise MissingGeneratorOptionsError()
1064        return self.genOpts.apientry + name + tail
1065
1066    def makeTypedefName(self, name, tail):
1067        """Make the function-pointer typedef name for a command."""
1068        if self.genOpts is None:
1069            raise MissingGeneratorOptionsError()
1070        return '(' + self.genOpts.apientryp + 'PFN_' + name + tail + ')'
1071
1072    def makeCParamDecl(self, param, aligncol):
1073        """Return a string which is an indented, formatted
1074        declaration for a `<param>` or `<member>` block (e.g. function parameter
1075        or structure/union member).
1076
1077        - param - Element (`<param>` or `<member>`) to format
1078        - aligncol - if non-zero, attempt to align the nested `<name>` element
1079          at this column"""
1080        if self.genOpts is None:
1081            raise MissingGeneratorOptionsError()
1082        if self.genOpts.conventions is None:
1083            raise MissingGeneratorOptionsConventionsError()
1084        indent = '    '
1085        paramdecl = indent
1086        prefix = noneStr(param.text)
1087
1088        for elem in param:
1089            text = noneStr(elem.text)
1090            tail = noneStr(elem.tail)
1091
1092            if self.should_insert_may_alias_macro and self.genOpts.conventions.is_voidpointer_alias(elem.tag, text, tail):
1093                # OpenXR-specific macro insertion - but not in apiinc for the spec
1094                tail = self.genOpts.conventions.make_voidpointer_alias(tail)
1095            if elem.tag == 'name' and aligncol > 0:
1096                self.logMsg('diag', 'Aligning parameter', elem.text, 'to column', self.genOpts.alignFuncParam)
1097                # Align at specified column, if possible
1098                paramdecl = paramdecl.rstrip()
1099                oldLen = len(paramdecl)
1100                # This works around a problem where very long type names -
1101                # longer than the alignment column - would run into the tail
1102                # text.
1103                paramdecl = paramdecl.ljust(aligncol - 1) + ' '
1104                newLen = len(paramdecl)
1105                self.logMsg('diag', 'Adjust length of parameter decl from', oldLen, 'to', newLen, ':', paramdecl)
1106
1107            if (self.misracppstyle() and prefix.find('const ') != -1):
1108                # Change pointer type order from e.g. "const void *" to "void const *".
1109                # If the string starts with 'const', reorder it to be after the first type.
1110                paramdecl += prefix.replace('const ', '') + text + ' const' + tail
1111            else:
1112                paramdecl += prefix + text + tail
1113
1114            # Clear prefix for subsequent iterations
1115            prefix = ''
1116
1117        paramdecl = paramdecl + prefix
1118
1119        if aligncol == 0:
1120            # Squeeze out multiple spaces other than the indentation
1121            paramdecl = indent + ' '.join(paramdecl.split())
1122        return paramdecl
1123
1124    def getCParamTypeLength(self, param):
1125        """Return the length of the type field is an indented, formatted
1126        declaration for a `<param>` or `<member>` block (e.g. function parameter
1127        or structure/union member).
1128
1129        - param - Element (`<param>` or `<member>`) to identify"""
1130        if self.genOpts is None:
1131            raise MissingGeneratorOptionsError()
1132        if self.genOpts.conventions is None:
1133            raise MissingGeneratorOptionsConventionsError()
1134
1135        # Allow for missing <name> tag
1136        newLen = 0
1137        paramdecl = '    ' + noneStr(param.text)
1138        for elem in param:
1139            text = noneStr(elem.text)
1140            tail = noneStr(elem.tail)
1141
1142            if self.should_insert_may_alias_macro and self.genOpts.conventions.is_voidpointer_alias(elem.tag, text, tail):
1143                # OpenXR-specific macro insertion
1144                tail = self.genOpts.conventions.make_voidpointer_alias(tail)
1145            if elem.tag == 'name':
1146                # Align at specified column, if possible
1147                newLen = len(paramdecl.rstrip())
1148                self.logMsg('diag', 'Identifying length of', elem.text, 'as', newLen)
1149            paramdecl += text + tail
1150
1151        return newLen
1152
1153    def getMaxCParamTypeLength(self, info):
1154        """Return the length of the longest type field for a member/parameter.
1155
1156        - info - TypeInfo or CommandInfo.
1157        """
1158        lengths = (self.getCParamTypeLength(member)
1159                   for member in info.getMembers())
1160        return max(lengths)
1161
1162    def getHandleParent(self, typename):
1163        """Get the parent of a handle object."""
1164        if self.registry is None:
1165            raise MissingRegistryError()
1166
1167        info = self.registry.typedict.get(typename)
1168        if info is None:
1169            return None
1170
1171        elem = info.elem
1172        if elem is not None:
1173            return elem.get('parent')
1174
1175        return None
1176
1177    def iterateHandleAncestors(self, typename):
1178        """Iterate through the ancestors of a handle type."""
1179        current = self.getHandleParent(typename)
1180        while current is not None:
1181            yield current
1182            current = self.getHandleParent(current)
1183
1184    def getHandleAncestors(self, typename):
1185        """Get the ancestors of a handle object."""
1186        return list(self.iterateHandleAncestors(typename))
1187
1188    def getTypeCategory(self, typename):
1189        """Get the category of a type."""
1190        if self.registry is None:
1191            raise MissingRegistryError()
1192
1193        info = self.registry.typedict.get(typename)
1194        if info is None:
1195            return None
1196
1197        elem = info.elem
1198        if elem is not None:
1199            return elem.get('category')
1200        return None
1201
1202    def isStructAlwaysValid(self, structname):
1203        """Try to do check if a structure is always considered valid (i.e. there is no rules to its acceptance)."""
1204        # A conventions object is required for this call.
1205        if not self.conventions:
1206            raise RuntimeError("To use isStructAlwaysValid, be sure your options include a Conventions object.")
1207        if self.registry is None:
1208            raise MissingRegistryError()
1209
1210        if self.conventions.type_always_valid(structname):
1211            return True
1212
1213        category = self.getTypeCategory(structname)
1214        if self.conventions.category_requires_validation(category):
1215            return False
1216
1217        info = self.registry.typedict.get(structname)
1218        if info is None:
1219            self.logMsg('error', f'isStructAlwaysValid({structname}) - structure not found in typedict')
1220
1221        members = info.getMembers()
1222
1223        for member in members:
1224            member_name = getElemName(member)
1225            if member_name in (self.conventions.structtype_member_name,
1226                               self.conventions.nextpointer_member_name):
1227                return False
1228
1229            if member.get('noautovalidity'):
1230                return False
1231
1232            member_type = getElemType(member)
1233
1234            if member_type in ('void', 'char') or self.paramIsArray(member) or self.paramIsPointer(member):
1235                return False
1236
1237            if self.conventions.type_always_valid(member_type):
1238                continue
1239
1240            member_category = self.getTypeCategory(member_type)
1241
1242            if self.conventions.category_requires_validation(member_category):
1243                return False
1244
1245            if member_category in ('struct', 'union'):
1246                if self.isStructAlwaysValid(member_type) is False:
1247                    return False
1248
1249        return True
1250
1251    def paramIsArray(self, param):
1252        """Check if the parameter passed in is a pointer to an array.
1253
1254        param           the XML information for the param
1255        """
1256        return param.get('len') is not None
1257
1258    def paramIsPointer(self, param):
1259        """Check if the parameter passed in is a pointer.
1260
1261        param           the XML information for the param
1262        """
1263        tail = param.find('type').tail
1264        return tail is not None and '*' in tail
1265
1266    def isEnumRequired(self, elem):
1267        """Return True if this `<enum>` element is
1268        required, False otherwise
1269
1270        - elem - `<enum>` element to test"""
1271        required = elem.get('required') is not None
1272        self.logMsg('diag', 'isEnumRequired:', elem.get('name'),
1273                    '->', required)
1274        return required
1275
1276        # @@@ This code is overridden by equivalent code now run in
1277        # @@@ Registry.generateFeature
1278
1279        required = False
1280
1281        extname = elem.get('extname')
1282        if extname is not None:
1283            # 'supported' attribute was injected when the <enum> element was
1284            # moved into the <enums> group in Registry.parseTree()
1285            if self.genOpts.defaultExtensions == elem.get('supported'):
1286                required = True
1287            elif re.match(self.genOpts.addExtensions, extname) is not None:
1288                required = True
1289        elif elem.get('version') is not None:
1290            required = re.match(self.genOpts.emitversions, elem.get('version')) is not None
1291        else:
1292            required = True
1293
1294        return required
1295
1296    def makeCDecls(self, cmd):
1297        """Return C prototype and function pointer typedef for a
1298        `<command>` Element, as a two-element list of strings.
1299
1300        - cmd - Element containing a `<command>` tag"""
1301        if self.genOpts is None:
1302            raise MissingGeneratorOptionsError()
1303        proto = cmd.find('proto')
1304        params = cmd.findall('param')
1305        # Begin accumulating prototype and typedef strings
1306        pdecl = self.genOpts.apicall
1307        tdecl = 'typedef '
1308
1309        # Insert the function return type/name.
1310        # For prototypes, add APIENTRY macro before the name
1311        # For typedefs, add (APIENTRY *<name>) around the name and
1312        #   use the PFN_cmdnameproc naming convention.
1313        # Done by walking the tree for <proto> element by element.
1314        # etree has elem.text followed by (elem[i], elem[i].tail)
1315        #   for each child element and any following text
1316        # Leading text
1317        pdecl += noneStr(proto.text)
1318        tdecl += noneStr(proto.text)
1319        # For each child element, if it is a <name> wrap in appropriate
1320        # declaration. Otherwise append its contents and tail contents.
1321        for elem in proto:
1322            text = noneStr(elem.text)
1323            tail = noneStr(elem.tail)
1324            if elem.tag == 'name':
1325                pdecl += self.makeProtoName(text, tail)
1326                tdecl += self.makeTypedefName(text, tail)
1327            else:
1328                pdecl += text + tail
1329                tdecl += text + tail
1330
1331        if self.genOpts.alignFuncParam == 0:
1332            # Squeeze out multiple spaces - there is no indentation
1333            pdecl = ' '.join(pdecl.split())
1334            tdecl = ' '.join(tdecl.split())
1335
1336        # Now add the parameter declaration list, which is identical
1337        # for prototypes and typedefs. Concatenate all the text from
1338        # a <param> node without the tags. No tree walking required
1339        # since all tags are ignored.
1340        # Uses: self.indentFuncProto
1341        # self.indentFuncPointer
1342        # self.alignFuncParam
1343        n = len(params)
1344        # Indented parameters
1345        if n > 0:
1346            indentdecl = '(\n'
1347            indentdecl += ',\n'.join(self.makeCParamDecl(p, self.genOpts.alignFuncParam)
1348                                     for p in params)
1349            indentdecl += ');'
1350        else:
1351            indentdecl = '(void);'
1352        # Non-indented parameters
1353        paramdecl = '('
1354        if n > 0:
1355            paramnames = []
1356            if self.misracppstyle():
1357                for p in params:
1358                    param = ''
1359                    firstIter = True;
1360                    for t in p.itertext():
1361                        if (firstIter):
1362                            prefix = t
1363                            firstIter = False
1364                        else:
1365                            # Change pointer type order from e.g. "const void *" to "void const *".
1366                            # If the string starts with 'const', reorder it to be after the first type.
1367                            if (prefix.find('const ') != -1):
1368                                param += prefix.replace('const ', '') + t + ' const '
1369                            else:
1370                                param += prefix + t
1371                            # Clear prefix for subsequent iterations
1372                            prefix = ''
1373                    paramnames.append(param);
1374            else:
1375                paramnames = (''.join(t for t in p.itertext())
1376                              for p in params)
1377            paramdecl += ', '.join(paramnames)
1378        else:
1379            paramdecl += 'void'
1380        paramdecl += ");"
1381        return [pdecl + indentdecl, tdecl + paramdecl]
1382
1383    def newline(self):
1384        """Print a newline to the output file (utility function)"""
1385        write('', file=self.outFile)
1386
1387    def setRegistry(self, registry):
1388        self.registry = registry
1389