1#!/usr/bin/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 @abc.abstractmethod 157 def structtype_member_name(self): 158 """Return name of the structure type member. 159 160 Must implement. 161 """ 162 raise NotImplementedError() 163 164 @property 165 @abc.abstractmethod 166 def nextpointer_member_name(self): 167 """Return name of the structure pointer chain member. 168 169 Must implement. 170 """ 171 raise NotImplementedError() 172 173 @property 174 @abc.abstractmethod 175 def xml_api_name(self): 176 """Return the name used in the default API XML registry for the default API""" 177 raise NotImplementedError() 178 179 @abc.abstractmethod 180 def generate_structure_type_from_name(self, structname): 181 """Generate a structure type name, like XR_TYPE_CREATE_INSTANCE_INFO. 182 183 Must implement. 184 """ 185 raise NotImplementedError() 186 187 def makeStructName(self, name): 188 """Prepend the appropriate format macro for a structure to a structure type name. 189 190 Uses struct_macro, so just override that if you want to change behavior. 191 """ 192 return self.struct_macro + name 193 194 def makeExternalTypeName(self, name): 195 """Prepend the appropriate format macro for an external type like uint32_t to a type name. 196 197 Uses external_macro, so just override that if you want to change behavior. 198 """ 199 return self.external_macro + name 200 201 def _implMakeProseList(self, elements, fmt, with_verb, comma_for_two_elts=False, serial_comma=True): 202 """Internal-use implementation to make a (comma-separated) list for use in prose. 203 204 Adds a connective (by default, 'and') 205 before the last element if there are more than 1, 206 and only includes commas if there are more than 2 207 (if comma_for_two_elts is False). 208 209 Adds the right one of "is" or "are" to the end if with_verb is true. 210 211 Optionally adds a quantifier (like 'any') before a list of 2 or more, 212 if specified by fmt. 213 214 Do not edit these defaults, override self.makeProseList(). 215 """ 216 assert(serial_comma) # did not implement what we did not need 217 if isinstance(fmt, str): 218 fmt = ProseListFormats.from_string(fmt) 219 220 my_elts = list(elements) 221 if len(my_elts) > 1: 222 my_elts[-1] = '{} {}'.format(fmt.connective, my_elts[-1]) 223 224 if not comma_for_two_elts and len(my_elts) <= 2: 225 prose = ' '.join(my_elts) 226 else: 227 prose = ', '.join(my_elts) 228 229 quantifier = fmt.quantifier(len(my_elts)) 230 231 parts = [quantifier, prose] 232 233 if with_verb: 234 if len(my_elts) > 1: 235 parts.append(' are') 236 else: 237 parts.append(' is') 238 return ''.join(parts) 239 240 @property 241 @abc.abstractmethod 242 def file_suffix(self): 243 """Return suffix of generated Asciidoctor files""" 244 raise NotImplementedError 245 246 @abc.abstractmethod 247 def api_name(self, spectype=None): 248 """Return API or specification name for citations in ref pages. 249 250 spectype is the spec this refpage is for. 251 'api' (the default value) is the main API Specification. 252 If an unrecognized spectype is given, returns None. 253 254 Must implement.""" 255 raise NotImplementedError 256 257 def should_insert_may_alias_macro(self, genOpts): 258 """Return true if we should insert a "may alias" macro in this file. 259 260 Only used by OpenXR right now.""" 261 return False 262 263 @property 264 def command_prefix(self): 265 """Return the expected prefix of commands/functions. 266 267 Implemented in terms of api_prefix.""" 268 if not self._command_prefix: 269 self._command_prefix = self.api_prefix[:].replace('_', '').lower() 270 return self._command_prefix 271 272 @property 273 def type_prefix(self): 274 """Return the expected prefix of type names. 275 276 Implemented in terms of command_prefix (and in turn, api_prefix).""" 277 if not self._type_prefix: 278 self._type_prefix = ''.join( 279 (self.command_prefix[0:1].upper(), self.command_prefix[1:])) 280 return self._type_prefix 281 282 @property 283 @abc.abstractmethod 284 def api_prefix(self): 285 """Return API token prefix. 286 287 Typically two uppercase letters followed by an underscore. 288 289 Must implement.""" 290 raise NotImplementedError 291 292 @property 293 def extension_name_prefix(self): 294 """Return extension name prefix. 295 296 Typically two uppercase letters followed by an underscore. 297 298 Assumed to be the same as api_prefix, but some APIs use different 299 case conventions.""" 300 301 return self.api_prefix 302 303 @property 304 def write_contacts(self): 305 """Return whether contact list should be written to extension appendices""" 306 return False 307 308 @property 309 def write_extension_type(self): 310 """Return whether extension type should be written to extension appendices""" 311 return True 312 313 @property 314 def write_extension_number(self): 315 """Return whether extension number should be written to extension appendices""" 316 return True 317 318 @property 319 def write_extension_revision(self): 320 """Return whether extension revision number should be written to extension appendices""" 321 return True 322 323 @property 324 def write_refpage_include(self): 325 """Return whether refpage include should be written to extension appendices""" 326 return True 327 328 @property 329 def api_version_prefix(self): 330 """Return API core version token prefix. 331 332 Implemented in terms of api_prefix. 333 334 May override.""" 335 return self.api_prefix + 'VERSION_' 336 337 @property 338 def KHR_prefix(self): 339 """Return extension name prefix for KHR extensions. 340 341 Implemented in terms of api_prefix. 342 343 May override.""" 344 return self.api_prefix + 'KHR_' 345 346 @property 347 def EXT_prefix(self): 348 """Return extension name prefix for EXT extensions. 349 350 Implemented in terms of api_prefix. 351 352 May override.""" 353 return self.api_prefix + 'EXT_' 354 355 def writeFeature(self, featureExtraProtect, filename): 356 """Return True if OutputGenerator.endFeature should write this feature. 357 358 Defaults to always True. 359 Used in COutputGenerator. 360 361 May override.""" 362 return True 363 364 def requires_error_validation(self, return_type): 365 """Return True if the return_type element is an API result code 366 requiring error validation. 367 368 Defaults to always False. 369 370 May override.""" 371 return False 372 373 @property 374 def required_errors(self): 375 """Return a list of required error codes for validation. 376 377 Defaults to an empty list. 378 379 May override.""" 380 return [] 381 382 def is_voidpointer_alias(self, tag, text, tail): 383 """Return True if the declaration components (tag,text,tail) of an 384 element represents a void * type. 385 386 Defaults to a reasonable implementation. 387 388 May override.""" 389 return tag == 'type' and text == 'void' and tail.startswith('*') 390 391 def make_voidpointer_alias(self, tail): 392 """Reformat a void * declaration to include the API alias macro. 393 394 Defaults to a no-op. 395 396 Must override if you actually want to use this feature in your project.""" 397 return tail 398 399 def category_requires_validation(self, category): 400 """Return True if the given type 'category' always requires validation. 401 402 Defaults to a reasonable implementation. 403 404 May override.""" 405 return category in CATEGORIES_REQUIRING_VALIDATION 406 407 def type_always_valid(self, typename): 408 """Return True if the given type name is always valid (never requires validation). 409 410 This is for things like integers. 411 412 Defaults to a reasonable implementation. 413 414 May override.""" 415 return typename in TYPES_KNOWN_ALWAYS_VALID 416 417 @property 418 def should_skip_checking_codes(self): 419 """Return True if more than the basic validation of return codes should 420 be skipped for a command.""" 421 422 return False 423 424 @property 425 def generate_index_terms(self): 426 """Return True if asiidoctor index terms should be generated as part 427 of an API interface from the docgenerator.""" 428 429 return False 430 431 @property 432 def generate_enum_table(self): 433 """Return True if asciidoctor tables describing enumerants in a 434 group should be generated as part of group generation.""" 435 return False 436 437 @property 438 def generate_max_enum_in_docs(self): 439 """Return True if MAX_ENUM tokens should be generated in 440 documentation includes.""" 441 return False 442 443 def extension_name_split(self, name): 444 """Split an extension name, returning (vendor, rest of name). 445 The API prefix of the name is ignored.""" 446 447 match = EXT_NAME_DECOMPOSE_RE.match(name) 448 vendor = match.group('vendor') 449 bare_name = match.group('name') 450 451 return (vendor, bare_name) 452 453 @abc.abstractmethod 454 def extension_file_path(self, name): 455 """Return file path to an extension appendix relative to a directory 456 containing all such appendices. 457 - name - extension name 458 459 Must implement.""" 460 raise NotImplementedError 461 462 def extension_include_string(self, name): 463 """Return format string for include:: line for an extension appendix 464 file. 465 - name - extension name""" 466 467 return 'include::{{appendices}}/{}[]'.format( 468 self.extension_file_path(name)) 469 470 @property 471 def provisional_extension_warning(self): 472 """Return True if a warning should be included in extension 473 appendices for provisional extensions.""" 474 return True 475 476 @property 477 def generated_include_path(self): 478 """Return path relative to the generated reference pages, to the 479 generated API include files.""" 480 481 return '{generated}' 482 483 @property 484 def include_extension_appendix_in_refpage(self): 485 """Return True if generating extension refpages by embedding 486 extension appendix content (default), False otherwise 487 (OpenXR).""" 488 489 return True 490 491 def valid_flag_bit(self, bitpos): 492 """Return True if bitpos is an allowed numeric bit position for 493 an API flag. 494 495 Behavior depends on the data type used for flags (which may be 32 496 or 64 bits), and may depend on assumptions about compiler 497 handling of sign bits in enumerated types, as well.""" 498 return True 499 500 @property 501 def duplicate_aliased_structs(self): 502 """ 503 Should aliased structs have the original struct definition listed in the 504 generated docs snippet? 505 """ 506 return False 507 508 @property 509 def protectProtoComment(self): 510 """Return True if generated #endif should have a comment matching 511 the protection symbol used in the opening #ifdef/#ifndef.""" 512 return False 513 514 @property 515 def extra_refpage_headers(self): 516 """Return any extra headers (preceding the title) for generated 517 reference pages.""" 518 return '' 519 520 @property 521 def extra_refpage_body(self): 522 """Return any extra text (following the title) for generated 523 reference pages.""" 524 return '' 525 526 def is_api_version_name(self, name): 527 """Return True if name is an API version name.""" 528 529 return API_VERSION_NAME_RE.match(name) is not None 530 531 @property 532 def docgen_language(self): 533 """Return the language to be used in docgenerator [source] 534 blocks.""" 535 536 return 'c++' 537