#!/usr/bin/python3 -i # # Copyright (c) 2013-2019 The Khronos Group Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import re import sys from collections import OrderedDict, namedtuple from functools import reduce from generator import OutputGenerator, write class UnhandledCaseError(RuntimeError): def __init__(self): super().__init__('Got a case in the validity generator that we have not explicitly handled.') def _genericIterateIntersection(a, b): """Iterate through all elements in a that are also in b. Somewhat like a set's intersection(), but not type-specific so it can work with OrderedDicts, etc. It also returns a generator instead of a set, so you can pick what container type you'd like, if any. """ return (x for x in a if x in b) def _make_ordered_dict(gen): """Make an ordered dict (with None as the values) from a generator.""" return OrderedDict(((x, None) for x in gen)) def _orderedDictIntersection(a, b): return _make_ordered_dict(_genericIterateIntersection(a, b)) def _genericIsDisjoint(a, b): """Return true if nothing in a is also in b. Like a set's is_disjoint(), but not type-specific so it can work with OrderedDicts, etc. """ for _ in _genericIterateIntersection(a, b): return False # if we never enter the loop... return True class LengthEntry: """An entry in a (comma-separated) len attribute""" def __init__(self, val): self.other_param_name = None self.null_terminated = False self.number = None if val == 'null-terminated': self.null_terminated = True return if val.isdigit(): self.number = int(val) return # Must be another param name. self.other_param_name = val @staticmethod def parse_len_from_param(param): """Get a list of LengthEntry.""" return [LengthEntry(elt) for elt in param.get('len').split(',')] def _getElemName(elem, default=None): """Get the name associated with an element, either a name child or name attribute.""" name_elem = elem.find('name') if name_elem is not None: return name_elem.text # Fallback if there is no child. return elem.get('name', default) def _getElemType(elem, default=None): """Get the type associated with an element, either a type child or type attribute.""" type_elem = elem.find('type') if type_elem is not None: return type_elem.text # Fallback if there is no child. return elem.get('type', default) def _findNamedElem(elems, name): """Traverse a collection of elements with 'name' nodes or attributes, looking for and returning one with the right name. NOTE: Many places where this is used might be better served by changing to a dictionary. """ for elem in elems: if _getElemName(elem) == name: return elem return None def _findNamedObject(collection, name): """Traverse a collection of elements with 'name' attributes, looking for and returning one with the right name. NOTE: Many places where this is used might be better served by changing to a dictionary. """ for elt in collection: if elt.name == name: return elt return None class ValidityCollection: """Combines validity for a single entity.""" def __init__(self, entity, conventions): self.entity = entity self.conventions = conventions self.lines = [] def possiblyAddExtensionRequirement(self, entity_data, entity_preface): """Add an extension-related validity statement if required. entity_preface is a string that goes between "must be enabled prior to " and the name of the entity, and normally ends in a macro. For instance, might be "calling flink:" for a function. """ if entity_data.extensionname: msg = 'The {} extension must: be enabled prior to {}{}'.format( self.conventions.formatExtension(entity_data.extensionname), entity_preface, self.entity) self.addValidityEntry(msg, 'extension', 'notenabled') def addValidityEntry(self, msg, *args): """Add a validity entry, optionally with a VUID anchor. If any trailing arguments are supplied, an anchor is generated by concatenating them with dashes at the end of the VUID anchor name. """ parts = ['*'] if args: parts.append('[[{}]]'.format( '-'.join(['VUID', self.entity] + list(args)))) parts.append(msg) self.lines.append(' '.join(parts)) def addText(self, msg): """Add already formatted validity text.""" if not msg: return msg = msg.rstrip() if not msg: return self.lines.append(msg) @property def text(self): """Access validity statements as a single string.""" if not self.lines: return None return '\n'.join(self.lines) + '\n' class ValidityOutputGenerator(OutputGenerator): """ValidityOutputGenerator - subclass of OutputGenerator. Generates AsciiDoc includes of valid usage information, for reference pages and the Vulkan specification. Similar to DocOutputGenerator. ---- methods ---- ValidityOutputGenerator(errFile, warnFile, diagFile) - args as for OutputGenerator. Defines additional internal state. ---- methods overriding base class ---- beginFile(genOpts) endFile() beginFeature(interface, emit) endFeature() genCmd(cmdinfo) """ def __init__(self, errFile = sys.stderr, warnFile = sys.stderr, diagFile = sys.stdout): OutputGenerator.__init__(self, errFile, warnFile, diagFile) @property def null(self): """Preferred spelling of NULL. Delegates to the object implementing ConventionsBase. """ return self.conventions.null @property def structtype_member_name(self): """Return name of the structure type member. Delegates to the object implementing ConventionsBase. """ return self.conventions.structtype_member_name @property def nextpointer_member_name(self): """Return name of the structure pointer chain member. Delegates to the object implementing ConventionsBase. """ return self.conventions.nextpointer_member_name def makeProseList(self, elements, connective='and'): """Make a (comma-separated) list for use in prose. Adds a connective (by default, 'and') before the last element if there are more than 1. Delegates to the object implementing ConventionsBase. """ return self.conventions.makeProseList(elements, connective) def makeValidityCollection(self, entity_name): """Create a ValidityCollection object, passing along our Conventions.""" return ValidityCollection(entity_name, self.conventions) def beginFile(self, genOpts): if not genOpts.conventions: raise RuntimeError('Must specify conventions object to generator options') self.conventions = genOpts.conventions # Vulkan says 'must: be a valid pointer' a lot, OpenXR just says # 'must: be a pointer'. self.valid_pointer_text = ' '.join((self.conventions.valid_pointer_prefix, 'pointer')) OutputGenerator.beginFile(self, genOpts) def endFile(self): OutputGenerator.endFile(self) def beginFeature(self, interface, emit): # Start processing in superclass OutputGenerator.beginFeature(self, interface, emit) self.currentExtension = interface.get('name') def endFeature(self): # Finish processing in superclass OutputGenerator.endFeature(self) @property def struct_macro(self): """Get the appropriate format macro for a structure.""" # delegate to conventions return self.conventions.struct_macro def makeStructName(self, name): """Prepend the appropriate format macro for a structure to a structure type name.""" # delegate to conventions return self.conventions.makeStructName(name) def makeParameterName(self, name): """Prepend the appropriate format macro for a parameter/member to a parameter name.""" return 'pname:' + name def makeBaseTypeName(self, name): """Prepend the appropriate format macro for a 'base type' to a type name.""" return 'basetype:' + name def makeEnumerationName(self, name): """Prepend the appropriate format macro for an enumeration type to a enum type name.""" return 'elink:' + name def makeFuncPointerName(self, name): """Prepend the appropriate format macro for a function pointer type to a type name.""" return 'tlink:' + name def makeExternalTypeName(self, name): """Prepend the appropriate format macro for an external type like uint32_t to a type name.""" # delegate to conventions return self.conventions.makeExternalTypeName(name) def makeEnumerantName(self, name): """Prepend the appropriate format macro for an enumerate (value) to a enum value name.""" return 'ename:' + name def makeAnchor(self, blockname, pname, category): """Create a unique namespaced Valid Usage anchor name. blockname - command or structure pname - parameter or member (may be None) category - distinct implicit VU type """ # For debugging # return '* ' if pname is not None: return '* [[VUID-%s-%s-%s]] ' % (blockname, pname, category) return '* [[VUID-%s-%s]] ' % (blockname, category) def writeInclude(self, directory, basename, validity, threadsafety, commandpropertiesentry, successcodes, errorcodes): """Generate an include file. directory - subdirectory to put file in (relative pathname) basename - base name of the file contents - contents of the file (Asciidoc boilerplate aside) """ # Create subdirectory, if needed directory = self.genOpts.directory + '/' + directory self.makeDir(directory) # Create validity file filename = directory + '/' + basename + '.txt' self.logMsg('diag', '# Generating include file:', filename) fp = open(filename, 'w', encoding='utf-8') # Asciidoc anchor write(self.conventions.warning_comment, file=fp) # Valid Usage if validity: write('.Valid Usage (Implicit)', file=fp) write('****', file=fp) write(validity, file=fp, end='') write('****', file=fp) write('', file=fp) # Host Synchronization if threadsafety: write('.Host Synchronization', file=fp) write('****', file=fp) write(threadsafety, file=fp, end='') write('****', file=fp) write('', file=fp) # Command Properties - contained within a block, to avoid table numbering if commandpropertiesentry: write('.Command Properties', file=fp) write('****', file=fp) write('[options="header", width="100%"]', file=fp) write('|====', file=fp) write('|<>|<>|<>|<>', file=fp) write(commandpropertiesentry, file=fp) write('|====', file=fp) write('****', file=fp) write('', file=fp) # Success Codes - contained within a block, to avoid table numbering has_success = (successcodes is not None and len(successcodes) > 0) has_errors = (errorcodes is not None and len(errorcodes) > 0) if has_success or has_errors: write('.Return Codes', file=fp) write('****', file=fp) if has_success: write('ifndef::doctype-manpage[]', file=fp) write('<>::', file=fp) write('endif::doctype-manpage[]', file=fp) write('ifdef::doctype-manpage[]', file=fp) write('On success, this command returns::', file=fp) write('endif::doctype-manpage[]', file=fp) write(successcodes, file=fp) if has_errors: write('ifndef::doctype-manpage[]', file=fp) write('<>::', file=fp) write('endif::doctype-manpage[]', file=fp) write('ifdef::doctype-manpage[]', file=fp) write('On failure, this command returns::', file=fp) write('endif::doctype-manpage[]', file=fp) write(errorcodes, file=fp) write('****', file=fp) write('', file=fp) fp.close() def paramIsPointer(self, param): """Check if the parameter passed in is a pointer.""" tail = param.find('type').tail return tail is not None and '*' in tail def paramIsStaticArray(self, param): """Check if the parameter passed in is a static array.""" tail = param.find('name').tail return tail and tail[0] == '[' def staticArrayLength(self, param): """Get the length of a parameter that's been identified as a static array.""" paramenumsize = param.find('enum') if paramenumsize is not None: return paramenumsize.text return param.find('name').tail[1:-1] def paramIsArray(self, param): """Check if the parameter passed in is a pointer to an array.""" return param.get('len') is not None def getHandleParent(self, typename): """Get the parent of a handle object.""" types = self.registry.tree.findall("types/type") elem = _findNamedElem(types, typename) if elem: return elem.get('parent') return None def iterateHandleAncestors(self, typename): """Iterate through the ancestors of a handle type.""" current = self.getHandleParent(typename) while current is not None: yield current current = self.getHandleParent(current) def getHandleAncestors(self, typename): """Get the ancestors of a handle object.""" ancestors = [] current = typename while True: current = self.getHandleParent(current) if current is None: return ancestors ancestors.append(current) def getHandleDispatchableAncestors(self, typename): """Get the ancestors of a handle object.""" ancestors = [] current = typename while True: current = self.getHandleParent(current) if current is None: return ancestors if self.isHandleTypeDispatchable(current): ancestors.append(current) def isHandleTypeDispatchable(self, handlename): """Check if a parent object is dispatchable or not.""" handle = self.registry.tree.find("types/type/[name='" + handlename + "'][@category='handle']") if handle is not None and _getElemType(handle) == 'VK_DEFINE_HANDLE': return True else: return False def isHandleOptional(self, param, params): # Simple, if it's optional, return true if param.get('optional') is not None: return True # If no validity is being generated, it usually means that validity is complex and not absolute, so let's say yes. if param.get('noautovalidity') is not None: return True # If the parameter is an array and we haven't already returned, find out if any of the len parameters are optional if self.paramIsArray(param): for length in LengthEntry.parse_len_from_param(param): if not length.other_param_name: # don't care about constants or "null-terminated" continue other_param = _findNamedElem(params, length.other_param_name) if other_param is None: self.logMsg('warn', length.other_param_name, 'is listed as a length for parameter', param, 'but no such parameter exists') if other_param and other_param.get('optional'): return True return False def getTypeCategory(self, typename): """Get the category of a type.""" types = self.registry.tree.findall("types/type") elem = _findNamedElem(types, typename) if elem is not None: return elem.get('category') return None def makeAsciiDocPreChunk(self, blockname, param, params): """Make a chunk of text for the end of a parameter if it is an array.""" param_name = _getElemName(param) paramtype = _getElemType(param) # General pre-amble. Check optionality and add stuff. asciidoc = self.makeAnchor(blockname, param_name, 'parameter') optionallengths = [] if self.paramIsStaticArray(param): asciidoc += 'Any given element of ' elif self.paramIsArray(param): # Find all the parameters that are called out as optional, so we can document that they might be zero, and the array may be ignored lengths = LengthEntry.parse_len_from_param(param) for length in lengths: if not length.other_param_name: # don't care about constants or "null-terminated" continue other_param = _findNamedElem(params, length.other_param_name) if other_param is not None and other_param.get('optional') is not None: if self.paramIsPointer(other_param): optionallengths.append( 'the value referenced by ' + self.makeParameterName(length.other_param_name)) else: optionallengths.append( self.makeParameterName(length.other_param_name)) # Document that these arrays may be ignored if any of the length values are 0 if optionallengths or param.get('optional') is not None: asciidoc += 'If ' if optionallengths: # TODO switch to commented line, but this introduces cosmetic changes. #asciidoc += self.makeProseList(optionallengths, 'or') asciidoc += ', or '.join(optionallengths) if len(optionallengths) == 1: asciidoc += ' is ' else: asciidoc += ' are ' asciidoc += 'not `0`, ' if optionallengths and param.get('optional') is not None: asciidoc += 'and ' if param.get('optional') is not None: asciidoc += self.makeParameterName(param_name) asciidoc += ' is not `NULL`, ' elif param.get('optional'): # Don't generate this stub for bitflags if self.getTypeCategory(paramtype) != 'bitmask': if param.get('optional').split(',')[0] == 'true': asciidoc += 'If ' asciidoc += self.makeParameterName(param_name) asciidoc += ' is not ' if self.paramIsArray(param) or self.paramIsPointer(param) or self.isHandleTypeDispatchable(paramtype): asciidoc += self.null elif self.getTypeCategory(paramtype) == 'handle': asciidoc += 'dlink:' + self.conventions.api_prefix + 'NULL_HANDLE' else: asciidoc += '`0`' asciidoc += ', ' return asciidoc def createValidationLineForParameterIntroChunk(self, blockname, param, params, typetext): """Make the generic asciidoc line chunk portion used for all parameters. May return an empty string if nothing to validate. """ asciidoc = '' param_name = _getElemName(param) paramtype = _getElemType(param) asciidoc += self.makeAsciiDocPreChunk(blockname, param, params) asciidoc += self.makeParameterName(param_name) asciidoc += ' must: be ' if self.paramIsArray(param): # Arrays. These are hard to get right, apparently lengths = param.get('len').split(',') if lengths[0] == 'null-terminated': asciidoc += 'a null-terminated ' elif lengths[0] == '1': asciidoc += 'a ' + self.valid_pointer_text + ' to ' else: asciidoc += 'a ' + self.valid_pointer_text + ' to an array of ' # Handle equations, which are currently denoted with latex if 'latexmath:' in lengths[0]: asciidoc += lengths[0] else: asciidoc += self.makeParameterName(lengths[0]) asciidoc += ' ' for length in lengths[1:]: if length == 'null-terminated': # This should always be the last thing. # If it ever isn't for some bizarre reason, then this will need some massaging. asciidoc += 'null-terminated ' elif length == '1': asciidoc += self.valid_pointer_text + 's to ' else: asciidoc += self.valid_pointer_text + 's to arrays of ' # Handle equations, which are currently denoted with latex if 'latexmath:' in length: asciidoc += length else: asciidoc += self.makeParameterName(length) asciidoc += ' ' # Void pointers don't actually point at anything - remove the word "to" if paramtype == 'void': if lengths[-1] == '1': if len(lengths) > 1: asciidoc = asciidoc[:-5] # Take care of the extra s added by the post array chunk function. #HACK# else: asciidoc = asciidoc[:-4] else: # An array of void values is a byte array. asciidoc += 'byte' elif paramtype == 'char': # A null terminated array of chars is a string if lengths[-1] == 'null-terminated': asciidoc += 'UTF-8 string' else: # Else it's just a bunch of chars asciidoc += 'char value' elif param.text is not None: # If a value is "const" that means it won't get modified, so it must be valid going into the function. if 'const' in param.text: typecategory = self.getTypeCategory(paramtype) if (typecategory not in ('struct', 'union', 'basetype') and typecategory is not None) \ or not self.isStructAlwaysValid(blockname, paramtype): asciidoc += 'valid ' asciidoc += typetext # pluralize if len(lengths) > 1 or (lengths[0] != '1' and lengths[0] != 'null-terminated'): asciidoc += 's' elif self.paramIsPointer(param): # Handle pointers - which are really special case arrays (i.e. they don't have a length) #TODO should do something here if someone ever uses some intricate comma-separated `optional` pointercount = param.find('type').tail.count('*') # Treat void* as an int if paramtype == 'void': pointercount -= 1 # Could be multi-level pointers (e.g. ppData - pointer to a pointer). Handle that. asciidoc += 'a ' asciidoc += (self.valid_pointer_text + ' to a ') * pointercount # Handle void* and pointers to it if paramtype == 'void': # If there is only void*, it is just optional int - we don't need any language. if pointercount == 0 and param.get('optional') is not None: return '' # early return if param.get('optional') is None or param.get('optional').split(',')[pointercount]: # The last void* is just optional int (e.g. to be filled by the impl.) typetext = 'pointer value' # If a value is "const" that means it won't get modified, so it must be valid going into the function. if param.text is not None and paramtype != 'void': if 'const' in param.text: asciidoc += 'valid ' asciidoc += typetext else: # Non-pointer, non-optional things must be valid asciidoc += 'a valid ' asciidoc += typetext if asciidoc != '': asciidoc += '\n' # Add additional line for non-optional bitmasks isOutputParam = self.paramIsPointer(param) and not (param.text and 'const' in param.text) if self.getTypeCategory(paramtype) == 'bitmask' and not isOutputParam: isMandatory = param.get('optional') is None #TODO does not really handle if someone tries something like optional="true,false" if isMandatory: asciidoc += self.makeAnchor(blockname, param_name, 'requiredbitmask') if self.paramIsArray(param): asciidoc += 'Each element of ' asciidoc += 'pname:' asciidoc += param_name asciidoc += ' must: not be `0`' asciidoc += '\n' return asciidoc def makeAsciiDocLineForParameter(self, blockname, param, params, typetext): if param.get('noautovalidity') is not None: return '' asciidoc = self.createValidationLineForParameterIntroChunk(blockname, param, params, typetext) return asciidoc def isStructAlwaysValid(self, blockname, structname): """Try to do check if a structure is always considered valid (i.e. there's no rules to its acceptance).""" struct = self.registry.tree.find("types/type[@name='" + structname + "']") if struct.get('returnedonly'): return True params = struct.findall('member') for param in params: param_name = _getElemName(param) paramtype = _getElemType(param) typecategory = self.getTypeCategory(paramtype) if param_name in (self.structtype_member_name, self.nextpointer_member_name): return False if param.get('noautovalidity'): return False if paramtype in ('void', 'char') or self.paramIsArray(param) or self.paramIsPointer(param): if self.makeAsciiDocLineForParameter(blockname, param, params, '') != '': return False elif typecategory in ('handle', 'enum', 'bitmask'): return False elif typecategory in ('struct', 'union'): if self.isStructAlwaysValid(blockname, paramtype) is False: return False return True def fix_an(self, text): """Fix 'a' vs 'an' in a string""" return re.sub(r' a ([a-z]+:)?([aAeEiIoOxX]\w+\b)(?!:)', r' an \1\2', text) def createValidationLineForParameter(self, blockname, param, params, typecategory): """Make an entire asciidoc line for a given parameter.""" asciidoc = '' param_name = _getElemName(param) paramtype = _getElemType(param) type_name = paramtype is_array = self.paramIsArray(param) is_pointer = self.paramIsPointer(param) needs_recursive_validity = (is_array or is_pointer or not self.isStructAlwaysValid(blockname, type_name)) typetext = None if type_name in ('void', 'char'): # Chars and void are special cases - needs care inside the generator functions # A null-terminated char array is a string, else it's chars. # An array of void values is a byte array, a void pointer is just a pointer to nothing in particular typetext = '' elif typecategory == 'bitmask': bitsname = type_name.replace('Flags', 'FlagBits') if self.registry.tree.find("enums[@name='" + bitsname + "']") is None: # Empty bit mask: presumably just a placeholder (or only in an extension not enabled for this build) asciidoc += self.makeAnchor(blockname, param_name, 'zerobitmask') asciidoc += self.makeParameterName(param_name) asciidoc += ' must: be `0`' asciidoc += '\n' else: const_in_text = param.text is not None and 'const' in param.text if is_array: if const_in_text: # input an array of bitmask values template = 'combinations of {bitsname} value' else: template = '{paramtype} value' elif is_pointer: if const_in_text: template = 'combination of {bitsname} values' else: template = '{paramtype} value' else: template = 'combination of {bitsname} values' # The above few cases all use makeEnumerationName, just with different context. typetext = template.format(bitsname=self.makeEnumerationName(bitsname), paramtype=self.makeEnumerationName(type_name)) elif typecategory == 'handle': typetext = '{} handle'.format(self.makeStructName(type_name)) elif typecategory == 'enum': typetext = '{} value'.format(self.makeEnumerationName(type_name)) elif typecategory == 'funcpointer': typetext = '{} value'.format(self.makeFuncPointerName(type_name)) elif typecategory == 'struct': if needs_recursive_validity: typetext = '{} structure'.format( self.makeStructName(type_name)) else: # TODO right now just skipping generating a line here. # I think this was intentional in general, but maybe not in that particular case. #raise UnhandledCaseError() pass elif typecategory == 'union': if needs_recursive_validity: typetext = '{} union'.format(self.makeStructName(type_name)) else: # TODO right now just skipping generating a line here. # I think this was intentional in general: # we do hit this in Vulkan. pass elif self.paramIsArray(param) or self.paramIsPointer(param): typetext = '{} value'.format(self.makeBaseTypeName(paramtype)) elif typecategory is None: # "a valid uint32_t value" doesn't make much sense. pass # If any of the above conditions matched and set typetext, # we call using it. if typetext is not None: asciidoc += self.makeAsciiDocLineForParameter( blockname, param, params, typetext) return self.fix_an(asciidoc) def makeAsciiDocHandleParent(self, blockname, param, params): """Make an asciidoc validity entry for a handle's parent object. Creates 'parent' VUID. """ asciidoc = '' param_name = _getElemName(param) paramtype = _getElemType(param) # Deal with handle parents handleparent = self.getHandleParent(paramtype) if handleparent is None: return asciidoc otherparam = None for p in params: if _getElemType(p) == handleparent: otherparam = p break if otherparam is None: return asciidoc parentreference = _getElemName(otherparam) asciidoc += self.makeAnchor(blockname, param_name, 'parent') if self.isHandleOptional(param, params): if self.paramIsArray(param): asciidoc += 'Each element of ' asciidoc += self.makeParameterName(param_name) asciidoc += ' that is a valid handle' else: asciidoc += 'If ' asciidoc += self.makeParameterName(param_name) asciidoc += ' is a valid handle, it' else: if self.paramIsArray(param): asciidoc += 'Each element of ' asciidoc += self.makeParameterName(param_name) asciidoc += ' must: have been created, allocated, or retrieved from ' asciidoc += self.makeParameterName(parentreference) asciidoc += '\n' return asciidoc def makeAsciiDocHandlesCommonAncestor(self, blockname, handles, params): """Make an asciidoc validity entry for a common ancestors between handles. Only handles parent validity for signatures taking multiple handles any ancestors also being supplied to this function. (e.g. "Each of x, y, and z must: come from the same slink:ParentHandle") See self.makeAsciiDocHandleParent() for instances where the parent handle is named and also passed. Creates 'commonparent' VUID. """ asciidoc = '' if len(handles) > 1: ancestormap = {} anyoptional = False # Find all the ancestors for param in handles: paramtype = _getElemType(param) if not self.paramIsPointer(param) or (param.text and 'const' in param.text): ancestors = self.getHandleDispatchableAncestors(paramtype) ancestormap[param] = ancestors anyoptional |= self.isHandleOptional(param, params) # Remove redundant ancestor lists for param in handles: paramtype = _getElemType(param) removals = [] for ancestors in ancestormap.items(): if paramtype in ancestors[1]: removals.append(ancestors[0]) if removals != []: for removal in removals: del(ancestormap[removal]) # Intersect if len(ancestormap.values()) > 1: current = list(ancestormap.values())[0] for ancestors in list(ancestormap.values())[1:]: current = [val for val in current if val in ancestors] if len(current) > 0: commonancestor = current[0] if len(ancestormap.keys()) > 1: asciidoc += self.makeAnchor(blockname, None, 'commonparent') parametertexts = [] for param in ancestormap.keys(): param_name = _getElemName(param) parametertext = self.makeParameterName(param_name) if self.paramIsArray(param): parametertext = 'the elements of ' + parametertext parametertexts.append(parametertext) parametertexts.sort() if len(parametertexts) > 2: asciidoc += 'Each of ' else: asciidoc += 'Both of ' asciidoc += ", ".join(parametertexts[:-1]) asciidoc += ', and ' asciidoc += parametertexts[-1] if anyoptional is True: asciidoc += ' that are valid handles' asciidoc += ' must: have been created, allocated, or retrieved from the same ' asciidoc += self.makeStructName(commonancestor) asciidoc += '\n' return asciidoc def makeStructureTypeFromName(self, structname): """Create text for a structure type name, like ename:VK_STRUCTURE_TYPE_CREATE_INSTANCE_INFO""" return self.makeEnumerantName(self.conventions.generate_structure_type_from_name(structname)) def makeStructureType(self, structname, param): """Generate an asciidoc validity line for the type value of a struct.""" param_name = _getElemName(param) asciidoc = self.makeAnchor(structname, param_name, self.structtype_member_name) asciidoc += self.makeParameterName(param_name) asciidoc += ' must: be ' values = param.get('values') if values: # Extract each enumerant value. They could be validated in the # same fashion as validextensionstructs in # makeStructureExtensionPointer, although that's not relevant in # the current extension struct model. asciidoc += self.makeProseList(( self.makeEnumerantName(v) for v in values.split(',')), 'or') elif 'Base' in structname: # This type doesn't even have any values for its type, # and it seems like it might be a base struct that we'd expect to lack its own type, # so omit the entire statement return '' else: self.logMsg('warn', 'No values were marked-up for the structure type member of', structname, 'so making one up!') asciidoc += self.makeStructureTypeFromName(structname) asciidoc += '\n' return asciidoc def makeStructureExtensionPointer(self, blockname, param): """Generate an asciidoc validity line for the pointer chain member value of a struct.""" param_name = _getElemName(param) if param.get('validextensionstructs') is not None: self.logMsg('warn', blockname, 'validextensionstructs is deprecated/removed', '\n') asciidoc = self.makeAnchor(blockname, param_name, self.nextpointer_member_name) validextensionstructs = self.registry.validextensionstructs.get(blockname) extensionstructs = [] if validextensionstructs is not None: # Check each structure name and skip it if not required by the # generator. This allows tagging extension structs in the XML # that are only included in validity when needed for the spec # being targeted. for struct in validextensionstructs: # Unpleasantly breaks encapsulation. Should be a method in the registry class type = self.registry.lookupElementInfo(struct, self.registry.typedict) if type is None: self.logMsg('warn', 'makeStructureExtensionPointer: struct', struct, 'is in a validextensionstructs= attribute but is not in the registry') elif type.required: extensionstructs.append('slink:' + struct) else: self.logMsg('diag', 'makeStructureExtensionPointer: struct', struct, 'IS NOT required') if not extensionstructs: asciidoc += self.makeParameterName(param_name) asciidoc += ' must: be ' asciidoc += self.null elif len(extensionstructs) == 1: asciidoc += self.makeParameterName(param_name) asciidoc += ' must: be ' asciidoc += self.null asciidoc +=' or a pointer to a valid instance of ' asciidoc += extensionstructs[0] else: asciidoc += 'Each ' asciidoc += self.makeParameterName(param_name) asciidoc += ' member of any structure (including this one) in the ' asciidoc += 'pname:' + self.nextpointer_member_name asciidoc += ' chain must: be either ' asciidoc += self.null asciidoc += ' or a pointer to a valid instance of ' asciidoc += self.makeProseList(extensionstructs, 'or') asciidoc += '\n' asciidoc += self.makeAnchor(blockname, self.structtype_member_name, 'unique') asciidoc += 'Each pname:' + self.structtype_member_name + ' member in the ' asciidoc += 'pname:' + self.nextpointer_member_name asciidoc += ' chain must: be unique' asciidoc += '\n' return asciidoc def makeValidUsageStatementsReturnedOnly(self, cmd, blockname, params): """Generate all the valid usage information for a given struct that's only ever filled out by the implementation other than the structure type and pointer chain members. """ # Start the asciidoc block for this asciidoc = '' for param in params: param_name = _getElemName(param) paramtype = _getElemType(param) # Valid usage ID tags (VUID) are generated for various # conditions based on the name of the block (structure or # command), name of the element (member or parameter), and type # of VU statement. if param.get('noautovalidity') is None: # Generate language to independently validate a parameter if self.conventions.is_structure_type_member(paramtype, param_name): asciidoc += self.makeStructureType(blockname, param) elif self.conventions.is_nextpointer_member(paramtype, param_name) and cmd.get('structextends') is None: asciidoc += self.makeStructureExtensionPointer(blockname, param) # In case there's nothing to report, return None if asciidoc == '': return None return asciidoc def makeValidUsageStatements(self, cmd, blockname, params): """Generate all the valid usage information for a given struct or command.""" # Start the asciidoc block for this asciidoc = '' handles = [] arraylengths = set() for param in params: param_name = _getElemName(param) paramtype = _getElemType(param) # Valid usage ID tags (VUID) are generated for various # conditions based on the name of the block (structure or # command), name of the element (member or parameter), and type # of VU statement. # Get the type's category typecategory = self.getTypeCategory(paramtype) if param.get('noautovalidity') is None: # Generate language to independently validate a parameter if self.conventions.is_structure_type_member(paramtype, param_name): asciidoc += self.makeStructureType(blockname, param) elif self.conventions.is_nextpointer_member(paramtype, param_name): if cmd.get('structextends') is None: asciidoc += self.makeStructureExtensionPointer(blockname, param) else: asciidoc += self.createValidationLineForParameter(blockname, param, params, typecategory) # Ensure that any parenting is properly validated, and list that a handle was found if typecategory == 'handle': handles.append(param) # Get the array length for this parameter arraylength = param.get('len') if arraylength is not None: arraylengths.update(set(arraylength.split(','))) # For any vkQueue* functions, there might be queue type data if 'vkQueue' in blockname: # The queue type must be valid queuetypes = cmd.get('queues') if queuetypes: queuebits = [] for queuetype in re.findall(r'([^,]+)', queuetypes): queuebits.append(queuetype.replace('_',' ')) asciidoc += self.makeAnchor(blockname, None, 'queuetype') asciidoc += 'The pname:queue must: support ' if len(queuebits) == 1: asciidoc += queuebits[0] else: asciidoc += (', ').join(queuebits[:-1]) asciidoc += ', or ' asciidoc += queuebits[-1] asciidoc += ' operations' asciidoc += '\n' if 'vkCmd' in blockname: # The commandBuffer parameter must be being recorded asciidoc += self.makeAnchor(blockname, 'commandBuffer', 'recording') asciidoc += 'pname:commandBuffer must: be in the <>' asciidoc += '\n' # # Start of valid queue type validation - command pool must have been # allocated against a queue with at least one of the valid queue types asciidoc += self.makeAnchor(blockname, 'commandBuffer', 'cmdpool') # # This test for vkCmdFillBuffer is a hack, since we have no path # to conditionally have queues enabled or disabled by an extension. # As the VU stuff is all moving out (hopefully soon), this hack solves the issue for now if blockname == 'vkCmdFillBuffer': if 'VK_KHR_maintenance1' in self.registry.requiredextensions: asciidoc += 'The sname:VkCommandPool that pname:commandBuffer was allocated from must: support transfer, graphics or compute operations\n' else: asciidoc += 'The sname:VkCommandPool that pname:commandBuffer was allocated from must: support graphics or compute operations\n' else: # The queue type must be valid queuetypes = cmd.get('queues') queuebits = [] for queuetype in re.findall(r'([^,]+)', queuetypes): queuebits.append(queuetype.replace('_',' ')) asciidoc += 'The sname:VkCommandPool that pname:commandBuffer was allocated from must: support ' if len(queuebits) == 1: asciidoc += queuebits[0] else: asciidoc += (', ').join(queuebits[:-1]) asciidoc += ', or ' asciidoc += queuebits[-1] asciidoc += ' operations' asciidoc += '\n' # Must be called inside/outside a renderpass appropriately renderpass = cmd.get('renderpass') if renderpass != 'both': asciidoc += self.makeAnchor(blockname, None, 'renderpass') asciidoc += 'This command must: only be called ' asciidoc += renderpass asciidoc += ' of a render pass instance' asciidoc += '\n' # Must be in the right level command buffer cmdbufferlevel = cmd.get('cmdbufferlevel') if cmdbufferlevel != 'primary,secondary': asciidoc += self.makeAnchor(blockname, None, 'bufferlevel') asciidoc += 'pname:commandBuffer must: be a ' asciidoc += cmdbufferlevel asciidoc += ' sname:VkCommandBuffer' asciidoc += '\n' # Any non-optional arraylengths should specify they must be greater than 0 non_optional_array_lengths = ((param, _getElemName(param)) for param in params if _getElemName(param) in arraylengths and not param.get('optional')) for param, param_name in non_optional_array_lengths: # Get all the array dependencies arrays = cmd.findall( "param/[@len='{}'][@optional='true']".format(param_name)) # Get all the optional array dependencies, including those not generating validity for some reason optionalarrays = arrays + \ cmd.findall( "param/[@len='{}'][@noautovalidity='true']".format(param_name)) # If arraylength can ever be not a legal part of an # asciidoc anchor name, this will need to be altered. asciidoc += self.makeAnchor(blockname, param_name, 'arraylength') # Allow lengths to be arbitrary if all their dependents are optional if optionalarrays and len(optionalarrays) == len(arrays): asciidoc += 'If ' if len(optionalarrays) > 1: asciidoc += 'any of ' # TODO uncomment following statement once cosmetic changes OK # asciidoc += self.makeProseList((self.makeParameterName(_getElemName(array)) # for array in optionalarrays), # 'or') if len(optionalarrays) > 1: # TODO remove following statement once cosmetic changes OK asciidoc += self.makeProseList((self.makeParameterName(_getElemName(array)) for array in optionalarrays), 'or') asciidoc += ' are ' else: # TODO remove following statement once cosmetic changes OK asciidoc += ', '.join((self.makeParameterName(_getElemName(array)) for array in optionalarrays)) asciidoc += ' is ' asciidoc += 'not ' + self.null + ', ' if self.paramIsPointer(param): asciidoc += 'the value referenced by ' elif self.paramIsPointer(param): asciidoc += 'The value referenced by ' asciidoc += self.makeParameterName(param_name) asciidoc += ' must: be greater than `0`' asciidoc += '\n' # Find the parents of all objects referenced in this command for param in handles: paramtype = _getElemType(param) # Don't detect a parent for return values! if not self.paramIsPointer(param) or (param.text is not None and 'const' in param.text): parent = self.getHandleParent(paramtype) if parent is not None: asciidoc += self.makeAsciiDocHandleParent(blockname, param, params) # Find the common ancestor of all objects referenced in this command asciidoc += self.makeAsciiDocHandlesCommonAncestor(blockname, handles, params) # In case there's nothing to report, return None if asciidoc == '': return None # Delimit the asciidoc block return asciidoc def makeThreadSafetyBlock(self, cmd, paramtext): """Generate C function pointer typedef for Element""" paramdecl = '' # Find and add any parameters that are thread unsafe explicitexternsyncparams = cmd.findall(paramtext + "[@externsync]") if explicitexternsyncparams is not None: for param in explicitexternsyncparams: externsyncattribs = param.get('externsync') param_name = _getElemName(param) for externsyncattrib in externsyncattribs.split(','): paramdecl += '* ' paramdecl += 'Host access to ' if externsyncattrib == 'true': if self.paramIsArray(param): paramdecl += 'each member of ' + \ self.makeParameterName(param_name) elif self.paramIsPointer(param): paramdecl += 'the object referenced by ' + \ self.makeParameterName(param_name) else: paramdecl += self.makeParameterName(param_name) else: paramdecl += 'pname:' paramdecl += externsyncattrib paramdecl += ' must: be externally synchronized\n' # Vulkan-specific # For any vkCmd* functions, the command pool is externally synchronized if cmd.find('proto/name') is not None and 'vkCmd' in cmd.find('proto/name').text: paramdecl += '* ' paramdecl += 'Host access to the sname:VkCommandPool that pname:commandBuffer was allocated from must: be externally synchronized' paramdecl += '\n' # Find and add any "implicit" parameters that are thread unsafe implicitexternsyncparams = cmd.find('implicitexternsyncparams') if implicitexternsyncparams is not None: for elem in implicitexternsyncparams: paramdecl += '* ' paramdecl += 'Host access to ' paramdecl += elem.text paramdecl += ' must: be externally synchronized\n' if not paramdecl: return None return paramdecl def makeCommandPropertiesTableEntry(self, cmd, name): if 'vkCmd' in name: # Must be called inside/outside a renderpass appropriately cmdbufferlevel = cmd.get('cmdbufferlevel') cmdbufferlevel = (' + \n').join(cmdbufferlevel.title().split(',')) renderpass = cmd.get('renderpass') renderpass = renderpass.capitalize() # # This test for vkCmdFillBuffer is a hack, since we have no path # to conditionally have queues enabled or disabled by an extension. # As the VU stuff is all moving out (hopefully soon), this hack solves the issue for now if name == 'vkCmdFillBuffer': if 'VK_KHR_maintenance1' in self.registry.requiredextensions: queues = 'Transfer + \nGraphics + \nCompute' else: queues = 'Graphics + \nCompute' else: queues = cmd.get('queues') queues = (' + \n').join(queues.title().split(',')) pipeline = cmd.get('pipeline') if pipeline: pipeline = pipeline.capitalize() else: pipeline = '' return '|' + cmdbufferlevel + '|' + renderpass + '|' + queues + '|' + pipeline elif 'vkQueue' in name: # Must be called inside/outside a renderpass appropriately queues = cmd.get('queues') if queues is None: queues = 'Any' else: queues = (' + \n').join(queues.upper().split(',')) return '|-|-|' + queues + '|-' return None # Check each enumerant name in the enums list and remove it if not # required by the generator. This allows specifying success and error # codes for extensions that are only included in validity when needed # for the spec being targeted. def findRequiredEnums(self, enums): out = [] for enum in enums: # Unpleasantly breaks encapsulation. Should be a method in the registry class ei = self.registry.lookupElementInfo(enum, self.registry.enumdict) if ei is None: self.logMsg('warn', 'findRequiredEnums: enum', enum, 'is in an attribute list but is not in the registry') elif ei.required: out.append(enum) else: self.logMsg('diag', 'findRequiredEnums: enum', enum, 'IS NOT required, skipping') return out def makeReturnCodeList(self, attrib, cmd, name): """Return a list of possible return codes for a function. attrib is either 'successcodes' or 'errorcodes'. """ return_lines = [] RETURN_CODE_FORMAT = '* ename:{}' codes_attr = cmd.get(attrib) if codes_attr: codes = self.findRequiredEnums(codes_attr.split(',')) if codes: return_lines.extend((RETURN_CODE_FORMAT.format(code) for code in codes)) applicable_ext_codes = (ext_code for ext_code in self.registry.commandextensionsuccesses if ext_code.command == name) for ext_code in applicable_ext_codes: line = RETURN_CODE_FORMAT.format(ext_code.value) if ext_code.extension: line += ' [only if {} is enabled]'.format( self.conventions.formatExtension(ext_code.extension)) return_lines.append(line) if return_lines: return '\n'.join(return_lines) return None def makeSuccessCodes(self, cmd, name): return self.makeReturnCodeList('successcodes', cmd, name) def makeErrorCodes(self, cmd, name): return self.makeReturnCodeList('errorcodes', cmd, name) def genCmd(self, cmdinfo, name, alias): """Command generation.""" OutputGenerator.genCmd(self, cmdinfo, name, alias) # @@@ (Jon) something needs to be done here to handle aliases, probably # Get all the parameters params = cmdinfo.elem.findall('param') validity = self.makeValidUsageStatements(cmdinfo.elem, name, params) threadsafety = self.makeThreadSafetyBlock(cmdinfo.elem, 'param') # Vulkan-specific commandpropertiesentry = self.makeCommandPropertiesTableEntry(cmdinfo.elem, name) successcodes = self.makeSuccessCodes(cmdinfo.elem, name) errorcodes = self.makeErrorCodes(cmdinfo.elem, name) # OpenXR-specific # beginsstate = self.makeBeginState(cmdinfo.elem, name) # endsstate = self.makeEndState(cmdinfo.elem, name) # checksstatedata = self.makeCheckState(cmdinfo.elem, name) self.writeInclude('protos', name, validity, threadsafety, commandpropertiesentry, successcodes, errorcodes) def genStruct(self, typeinfo, typeName, alias): """Struct Generation.""" OutputGenerator.genStruct(self, typeinfo, typeName, alias) # @@@ (Jon) something needs to be done here to handle aliases, probably # Anything that's only ever returned can't be set by the user, so shouldn't have any validity information. params = [] validity = '' threadsafety = [] if typeinfo.elem.get('returnedonly') is None: params = typeinfo.elem.findall('member') generalvalidity = self.makeValidUsageStatements(typeinfo.elem, typeName, params) if generalvalidity: validity += generalvalidity threadsafety = self.makeThreadSafetyBlock(typeinfo.elem, 'member') self.writeInclude('structs', typeName, validity, threadsafety, None, None, None) else: # Need to generate structure type and next pointer chain member validation params = typeinfo.elem.findall('member') retvalidity = self.makeValidUsageStatementsReturnedOnly(typeinfo.elem, typeName, params) if retvalidity: validity += retvalidity self.writeInclude('structs', typeName, validity, None, None, None, None) def genGroup(self, groupinfo, groupName, alias): """Group (e.g. C "enum" type) generation. For the validity generator, this just tags individual enumerants as required or not. """ OutputGenerator.genGroup(self, groupinfo, groupName, alias) # @@@ (Jon) something needs to be done here to handle aliases, probably groupElem = groupinfo.elem # Loop over the nested 'enum' tags. Keep track of the minimum and # maximum numeric values, if they can be determined; but only for # core API enumerants, not extension enumerants. This is inferred # by looking for 'extends' attributes. for elem in groupElem.findall('enum'): name = elem.get('name') ei = self.registry.lookupElementInfo(name, self.registry.enumdict) # Tag enumerant as required or not ei.required = self.isEnumRequired(elem) def genType(self, typeinfo, name, alias): """Type Generation.""" OutputGenerator.genType(self, typeinfo, name, alias) # @@@ (Jon) something needs to be done here to handle aliases, probably category = typeinfo.elem.get('category') if category in ('struct', 'union'): self.genStruct(typeinfo, name, alias)