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