1#!/usr/bin/env python3 -i 2# 3# Copyright 2013-2024 The Khronos Group Inc. 4# 5# SPDX-License-Identifier: Apache-2.0 6 7# Base class for working-group-specific style conventions, 8# used in generation. 9 10from enum import Enum 11import abc 12import re 13 14# Type categories that respond "False" to isStructAlwaysValid 15# basetype is home to typedefs like ..Bool32 16CATEGORIES_REQUIRING_VALIDATION = set(('handle', 17 'enum', 18 'bitmask', 19 'basetype', 20 None)) 21 22# These are basic C types pulled in via openxr_platform_defines.h 23TYPES_KNOWN_ALWAYS_VALID = set(('char', 24 'float', 25 'int8_t', 'uint8_t', 26 'int16_t', 'uint16_t', 27 'int32_t', 'uint32_t', 28 'int64_t', 'uint64_t', 29 'size_t', 30 'intptr_t', 'uintptr_t', 31 'int', 32 )) 33 34# Split an extension name into vendor ID and name portions 35EXT_NAME_DECOMPOSE_RE = re.compile(r'(?P<prefix>[A-Za-z]+)_(?P<vendor>[A-Za-z]+)_(?P<name>[\w_]+)') 36 37# Match an API version name. 38# Match object includes API prefix, major, and minor version numbers. 39# This could be refined further for specific APIs. 40API_VERSION_NAME_RE = re.compile(r'(?P<apivariant>[A-Za-z]+)_VERSION_(?P<major>[0-9]+)_(?P<minor>[0-9]+)') 41 42class ProseListFormats(Enum): 43 """A connective, possibly with a quantifier.""" 44 AND = 0 45 EACH_AND = 1 46 OR = 2 47 ANY_OR = 3 48 49 @classmethod 50 def from_string(cls, s): 51 if s == 'or': 52 return cls.OR 53 if s == 'and': 54 return cls.AND 55 raise RuntimeError("Unrecognized string connective: " + s) 56 57 @property 58 def connective(self): 59 if self in (ProseListFormats.OR, ProseListFormats.ANY_OR): 60 return 'or' 61 return 'and' 62 63 def quantifier(self, n): 64 """Return the desired quantifier for a list of a given length.""" 65 if self == ProseListFormats.ANY_OR: 66 if n > 1: 67 return 'any of ' 68 elif self == ProseListFormats.EACH_AND: 69 if n > 2: 70 return 'each of ' 71 if n == 2: 72 return 'both of ' 73 return '' 74 75 76class ConventionsBase(abc.ABC): 77 """WG-specific conventions.""" 78 79 def __init__(self): 80 self._command_prefix = None 81 self._type_prefix = None 82 83 def formatVersionOrExtension(self, name): 84 """Mark up an API version or extension name as a link in the spec.""" 85 86 # Is this a version name? 87 match = API_VERSION_NAME_RE.match(name) 88 if match is not None: 89 return self.formatVersion(name, 90 match.group('apivariant'), 91 match.group('major'), 92 match.group('minor')) 93 else: 94 # If not, assumed to be an extension name. Might be worth checking. 95 return self.formatExtension(name) 96 97 def formatVersion(self, name, apivariant, major, minor): 98 """Mark up an API version name as a link in the spec.""" 99 return '`<<{}>>`'.format(name) 100 101 def formatExtension(self, name): 102 """Mark up an extension name as a link in the spec.""" 103 return '`<<{}>>`'.format(name) 104 105 def formatSPIRVlink(self, name): 106 """Mark up a SPIR-V extension name as an external link in the spec. 107 Since these are external links, the formatting probably will be 108 the same for all APIs creating such links, so long as they use 109 the asciidoctor {spirv} attribute for the base path to the SPIR-V 110 extensions.""" 111 112 (vendor, _) = self.extension_name_split(name) 113 114 return f'{{spirv}}/{vendor}/{name}.html[{name}]' 115 116 @property 117 @abc.abstractmethod 118 def null(self): 119 """Preferred spelling of NULL.""" 120 raise NotImplementedError 121 122 def makeProseList(self, elements, fmt=ProseListFormats.AND, with_verb=False, *args, **kwargs): 123 """Make a (comma-separated) list for use in prose. 124 125 Adds a connective (by default, 'and') 126 before the last element if there are more than 1. 127 128 Adds the right one of "is" or "are" to the end if with_verb is true. 129 130 Optionally adds a quantifier (like 'any') before a list of 2 or more, 131 if specified by fmt. 132 133 Override with a different method or different call to 134 _implMakeProseList if you want to add a comma for two elements, 135 or not use a serial comma. 136 """ 137 return self._implMakeProseList(elements, fmt, with_verb, *args, **kwargs) 138 139 @property 140 def struct_macro(self): 141 """Get the appropriate format macro for a structure. 142 143 May override. 144 """ 145 return 'slink:' 146 147 @property 148 def external_macro(self): 149 """Get the appropriate format macro for an external type like uint32_t. 150 151 May override. 152 """ 153 return 'code:' 154 155 @property 156 def allows_x_number_suffix(self): 157 """Whether vendor tags can be suffixed with X and a number to mark experimental extensions.""" 158 return False 159 160 @property 161 @abc.abstractmethod 162 def structtype_member_name(self): 163 """Return name of the structure type member. 164 165 Must implement. 166 """ 167 raise NotImplementedError() 168 169 @property 170 @abc.abstractmethod 171 def nextpointer_member_name(self): 172 """Return name of the structure pointer chain member. 173 174 Must implement. 175 """ 176 raise NotImplementedError() 177 178 @property 179 @abc.abstractmethod 180 def xml_api_name(self): 181 """Return the name used in the default API XML registry for the default API""" 182 raise NotImplementedError() 183 184 @abc.abstractmethod 185 def generate_structure_type_from_name(self, structname): 186 """Generate a structure type name, like XR_TYPE_CREATE_INSTANCE_INFO. 187 188 Must implement. 189 """ 190 raise NotImplementedError() 191 192 def makeStructName(self, name): 193 """Prepend the appropriate format macro for a structure to a structure type name. 194 195 Uses struct_macro, so just override that if you want to change behavior. 196 """ 197 return self.struct_macro + name 198 199 def makeExternalTypeName(self, name): 200 """Prepend the appropriate format macro for an external type like uint32_t to a type name. 201 202 Uses external_macro, so just override that if you want to change behavior. 203 """ 204 return self.external_macro + name 205 206 def _implMakeProseList(self, elements, fmt, with_verb, comma_for_two_elts=False, serial_comma=True): 207 """Internal-use implementation to make a (comma-separated) list for use in prose. 208 209 Adds a connective (by default, 'and') 210 before the last element if there are more than 1, 211 and only includes commas if there are more than 2 212 (if comma_for_two_elts is False). 213 214 Adds the right one of "is" or "are" to the end if with_verb is true. 215 216 Optionally adds a quantifier (like 'any') before a list of 2 or more, 217 if specified by fmt. 218 219 Do not edit these defaults, override self.makeProseList(). 220 """ 221 assert serial_comma # did not implement what we did not need 222 if isinstance(fmt, str): 223 fmt = ProseListFormats.from_string(fmt) 224 225 my_elts = list(elements) 226 if len(my_elts) > 1: 227 my_elts[-1] = '{} {}'.format(fmt.connective, my_elts[-1]) 228 229 if not comma_for_two_elts and len(my_elts) <= 2: 230 prose = ' '.join(my_elts) 231 else: 232 prose = ', '.join(my_elts) 233 234 quantifier = fmt.quantifier(len(my_elts)) 235 236 parts = [quantifier, prose] 237 238 if with_verb: 239 if len(my_elts) > 1: 240 parts.append(' are') 241 else: 242 parts.append(' is') 243 return ''.join(parts) 244 245 @property 246 @abc.abstractmethod 247 def file_suffix(self): 248 """Return suffix of generated Asciidoctor files""" 249 raise NotImplementedError 250 251 @abc.abstractmethod 252 def api_name(self, spectype=None): 253 """Return API or specification name for citations in ref pages. 254 255 spectype is the spec this refpage is for. 256 'api' (the default value) is the main API Specification. 257 If an unrecognized spectype is given, returns None. 258 259 Must implement.""" 260 raise NotImplementedError 261 262 def should_insert_may_alias_macro(self, genOpts): 263 """Return true if we should insert a "may alias" macro in this file. 264 265 Only used by OpenXR right now.""" 266 return False 267 268 @property 269 def command_prefix(self): 270 """Return the expected prefix of commands/functions. 271 272 Implemented in terms of api_prefix.""" 273 if not self._command_prefix: 274 self._command_prefix = self.api_prefix[:].replace('_', '').lower() 275 return self._command_prefix 276 277 @property 278 def type_prefix(self): 279 """Return the expected prefix of type names. 280 281 Implemented in terms of command_prefix (and in turn, api_prefix).""" 282 if not self._type_prefix: 283 self._type_prefix = ''.join( 284 (self.command_prefix[0:1].upper(), self.command_prefix[1:])) 285 return self._type_prefix 286 287 @property 288 @abc.abstractmethod 289 def api_prefix(self): 290 """Return API token prefix. 291 292 Typically two uppercase letters followed by an underscore. 293 294 Must implement.""" 295 raise NotImplementedError 296 297 @property 298 def extension_name_prefix(self): 299 """Return extension name prefix. 300 301 Typically two uppercase letters followed by an underscore. 302 303 Assumed to be the same as api_prefix, but some APIs use different 304 case conventions.""" 305 306 return self.api_prefix 307 308 def extension_short_description(self, elem): 309 """Return a short description of an extension for use in refpages. 310 311 elem is an ElementTree for the <extension> tag in the XML. 312 The default behavior is to use the 'type' field of this tag, but not 313 all APIs support this field.""" 314 315 ext_type = elem.get('type') 316 317 if ext_type is not None: 318 return f'{ext_type} extension' 319 else: 320 return '' 321 322 @property 323 def write_contacts(self): 324 """Return whether contact list should be written to extension appendices""" 325 return False 326 327 @property 328 def write_extension_type(self): 329 """Return whether extension type should be written to extension appendices""" 330 return True 331 332 @property 333 def write_extension_number(self): 334 """Return whether extension number should be written to extension appendices""" 335 return True 336 337 @property 338 def write_extension_revision(self): 339 """Return whether extension revision number should be written to extension appendices""" 340 return True 341 342 @property 343 def write_refpage_include(self): 344 """Return whether refpage include should be written to extension appendices""" 345 return True 346 347 @property 348 def api_version_prefix(self): 349 """Return API core version token prefix. 350 351 Implemented in terms of api_prefix. 352 353 May override.""" 354 return self.api_prefix + 'VERSION_' 355 356 @property 357 def KHR_prefix(self): 358 """Return extension name prefix for KHR extensions. 359 360 Implemented in terms of api_prefix. 361 362 May override.""" 363 return self.api_prefix + 'KHR_' 364 365 @property 366 def EXT_prefix(self): 367 """Return extension name prefix for EXT extensions. 368 369 Implemented in terms of api_prefix. 370 371 May override.""" 372 return self.api_prefix + 'EXT_' 373 374 def writeFeature(self, featureName, featureExtraProtect, filename): 375 """Return True if OutputGenerator.endFeature should write this feature. 376 377 Defaults to always True. 378 Used in COutputGenerator. 379 380 May override.""" 381 return True 382 383 def requires_error_validation(self, return_type): 384 """Return True if the return_type element is an API result code 385 requiring error validation. 386 387 Defaults to always False. 388 389 May override.""" 390 return False 391 392 @property 393 def required_errors(self): 394 """Return a list of required error codes for validation. 395 396 Defaults to an empty list. 397 398 May override.""" 399 return [] 400 401 def is_voidpointer_alias(self, tag, text, tail): 402 """Return True if the declaration components (tag,text,tail) of an 403 element represents a void * type. 404 405 Defaults to a reasonable implementation. 406 407 May override.""" 408 return tag == 'type' and text == 'void' and tail.startswith('*') 409 410 def make_voidpointer_alias(self, tail): 411 """Reformat a void * declaration to include the API alias macro. 412 413 Defaults to a no-op. 414 415 Must override if you actually want to use this feature in your project.""" 416 return tail 417 418 def category_requires_validation(self, category): 419 """Return True if the given type 'category' always requires validation. 420 421 Defaults to a reasonable implementation. 422 423 May override.""" 424 return category in CATEGORIES_REQUIRING_VALIDATION 425 426 def type_always_valid(self, typename): 427 """Return True if the given type name is always valid (never requires validation). 428 429 This is for things like integers. 430 431 Defaults to a reasonable implementation. 432 433 May override.""" 434 return typename in TYPES_KNOWN_ALWAYS_VALID 435 436 @property 437 def should_skip_checking_codes(self): 438 """Return True if more than the basic validation of return codes should 439 be skipped for a command.""" 440 441 return False 442 443 @property 444 def generate_index_terms(self): 445 """Return True if asiidoctor index terms should be generated as part 446 of an API interface from the docgenerator.""" 447 448 return False 449 450 @property 451 def generate_enum_table(self): 452 """Return True if asciidoctor tables describing enumerants in a 453 group should be generated as part of group generation.""" 454 return False 455 456 @property 457 def generate_max_enum_in_docs(self): 458 """Return True if MAX_ENUM tokens should be generated in 459 documentation includes.""" 460 return False 461 462 def extension_name_split(self, name): 463 """Split an extension name, returning (vendor, rest of name). 464 The API prefix of the name is ignored.""" 465 466 match = EXT_NAME_DECOMPOSE_RE.match(name) 467 vendor = match.group('vendor') 468 bare_name = match.group('name') 469 470 return (vendor, bare_name) 471 472 @abc.abstractmethod 473 def extension_file_path(self, name): 474 """Return file path to an extension appendix relative to a directory 475 containing all such appendices. 476 - name - extension name 477 478 Must implement.""" 479 raise NotImplementedError 480 481 def extension_include_string(self, name): 482 """Return format string for include:: line for an extension appendix 483 file. 484 - name - extension name""" 485 486 return 'include::{{appendices}}/{}[]'.format( 487 self.extension_file_path(name)) 488 489 @property 490 def provisional_extension_warning(self): 491 """Return True if a warning should be included in extension 492 appendices for provisional extensions.""" 493 return True 494 495 @property 496 def generated_include_path(self): 497 """Return path relative to the generated reference pages, to the 498 generated API include files.""" 499 500 return '{generated}' 501 502 @property 503 def include_extension_appendix_in_refpage(self): 504 """Return True if generating extension refpages by embedding 505 extension appendix content (default), False otherwise 506 (OpenXR).""" 507 508 return True 509 510 def valid_flag_bit(self, bitpos): 511 """Return True if bitpos is an allowed numeric bit position for 512 an API flag. 513 514 Behavior depends on the data type used for flags (which may be 32 515 or 64 bits), and may depend on assumptions about compiler 516 handling of sign bits in enumerated types, as well.""" 517 return True 518 519 @property 520 def duplicate_aliased_structs(self): 521 """ 522 Should aliased structs have the original struct definition listed in the 523 generated docs snippet? 524 """ 525 return False 526 527 @property 528 def protectProtoComment(self): 529 """Return True if generated #endif should have a comment matching 530 the protection symbol used in the opening #ifdef/#ifndef.""" 531 return False 532 533 @property 534 def extra_refpage_headers(self): 535 """Return any extra headers (preceding the title) for generated 536 reference pages.""" 537 return '' 538 539 @property 540 def extra_refpage_body(self): 541 """Return any extra text (following the title) for generated 542 reference pages.""" 543 return '' 544 545 def is_api_version_name(self, name): 546 """Return True if name is an API version name.""" 547 548 return API_VERSION_NAME_RE.match(name) is not None 549 550 @property 551 def docgen_language(self): 552 """Return the language to be used in docgenerator [source] 553 blocks.""" 554 555 return 'c++' 556 557 @property 558 def docgen_source_options(self): 559 """Return block options to be used in docgenerator [source] blocks, 560 which are appended to the 'source' block type. 561 Can be empty.""" 562 563 return '%unbreakable' 564