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