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