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