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