|
1501
|
1 #!/usr/bin/env python3 -i
|
|
|
2 #
|
|
|
3 # Copyright 2013-2025 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
|
|
|
10 from enum import Enum
|
|
|
11 import abc
|
|
|
12 import re
|
|
|
13
|
|
|
14 # Type categories that respond "False" to isStructAlwaysValid
|
|
|
15 # basetype is home to typedefs like ..Bool32
|
|
|
16 CATEGORIES_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
|
|
|
23 TYPES_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
|
|
|
35 EXT_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.
|
|
|
40 # Handles both simple versions (VK_VERSION_1_0) and component-specific versions (VK_BASE_VERSION_1_0)
|
|
|
41 API_VERSION_NAME_RE = re.compile(r'(?P<apivariant>[A-Za-z]+)(?:_(?:BASE|COMPUTE|GRAPHICS))?_VERSION_(?P<major>[0-9]+)_(?P<minor>[0-9]+)')
|
|
|
42
|
|
|
43 class ProseListFormats(Enum):
|
|
|
44 """A connective, possibly with a quantifier."""
|
|
|
45 AND = 0
|
|
|
46 EACH_AND = 1
|
|
|
47 OR = 2
|
|
|
48 ANY_OR = 3
|
|
|
49
|
|
|
50 @classmethod
|
|
|
51 def from_string(cls, s):
|
|
|
52 if s == 'or':
|
|
|
53 return cls.OR
|
|
|
54 if s == 'and':
|
|
|
55 return cls.AND
|
|
|
56 raise RuntimeError(f"Unrecognized string connective: {s}")
|
|
|
57
|
|
|
58 @property
|
|
|
59 def connective(self):
|
|
|
60 if self in (ProseListFormats.OR, ProseListFormats.ANY_OR):
|
|
|
61 return 'or'
|
|
|
62 return 'and'
|
|
|
63
|
|
|
64 def quantifier(self, n):
|
|
|
65 """Return the desired quantifier for a list of a given length."""
|
|
|
66 if self == ProseListFormats.ANY_OR:
|
|
|
67 if n > 1:
|
|
|
68 return 'any of '
|
|
|
69 elif self == ProseListFormats.EACH_AND:
|
|
|
70 if n > 2:
|
|
|
71 return 'each of '
|
|
|
72 if n == 2:
|
|
|
73 return 'both of '
|
|
|
74 return ''
|
|
|
75
|
|
|
76
|
|
|
77 class ConventionsBase(abc.ABC):
|
|
|
78 """WG-specific conventions."""
|
|
|
79
|
|
|
80 def __init__(self):
|
|
|
81 self._command_prefix = None
|
|
|
82 self._type_prefix = None
|
|
|
83
|
|
|
84 def formatVersionOrExtension(self, name):
|
|
|
85 """Mark up an API version or extension name as a link in the spec."""
|
|
|
86
|
|
|
87 # Is this a version name?
|
|
|
88 match = API_VERSION_NAME_RE.match(name)
|
|
|
89 if match is not None:
|
|
|
90 return self.formatVersion(name,
|
|
|
91 match.group('apivariant'),
|
|
|
92 match.group('major'),
|
|
|
93 match.group('minor'))
|
|
|
94 else:
|
|
|
95 # If not, assumed to be an extension name. Might be worth checking.
|
|
|
96 return self.formatExtension(name)
|
|
|
97
|
|
|
98 def formatVersion(self, name, apivariant, major, minor):
|
|
|
99 """Mark up an API version name as a link in the spec."""
|
|
|
100 return f'`<<{name}>>`'
|
|
|
101
|
|
|
102 def formatExtension(self, name):
|
|
|
103 """Mark up an extension name as a link in the spec."""
|
|
|
104 return f'`<<{name}>>`'
|
|
|
105
|
|
|
106 def formatSPIRVlink(self, name):
|
|
|
107 """Mark up a SPIR-V extension name as an external link in the spec.
|
|
|
108 Since these are external links, the formatting probably will be
|
|
|
109 the same for all APIs creating such links, so long as they use
|
|
|
110 the asciidoctor {spirv} attribute for the base path to the SPIR-V
|
|
|
111 extensions."""
|
|
|
112
|
|
|
113 (vendor, _) = self.extension_name_split(name)
|
|
|
114
|
|
|
115 return f'{{spirv}}/{vendor}/{name}.html[{name}]'
|
|
|
116
|
|
|
117 @property
|
|
|
118 @abc.abstractmethod
|
|
|
119 def null(self):
|
|
|
120 """Preferred spelling of NULL."""
|
|
|
121 raise NotImplementedError
|
|
|
122
|
|
|
123 def makeProseList(self, elements, fmt=ProseListFormats.AND, with_verb=False, *args, **kwargs):
|
|
|
124 """Make a (comma-separated) list for use in prose.
|
|
|
125
|
|
|
126 Adds a connective (by default, 'and')
|
|
|
127 before the last element if there are more than 1.
|
|
|
128
|
|
|
129 Adds the right one of "is" or "are" to the end if with_verb is true.
|
|
|
130
|
|
|
131 Optionally adds a quantifier (like 'any') before a list of 2 or more,
|
|
|
132 if specified by fmt.
|
|
|
133
|
|
|
134 Override with a different method or different call to
|
|
|
135 _implMakeProseList if you want to add a comma for two elements,
|
|
|
136 or not use a serial comma.
|
|
|
137 """
|
|
|
138 return self._implMakeProseList(elements, fmt, with_verb, *args, **kwargs)
|
|
|
139
|
|
|
140 @property
|
|
|
141 def struct_macro(self):
|
|
|
142 """Get the appropriate format macro for a structure.
|
|
|
143
|
|
|
144 May override.
|
|
|
145 """
|
|
|
146 return 'slink:'
|
|
|
147
|
|
|
148 @property
|
|
|
149 def external_macro(self):
|
|
|
150 """Get the appropriate format macro for an external type like uint32_t.
|
|
|
151
|
|
|
152 May override.
|
|
|
153 """
|
|
|
154 return 'code:'
|
|
|
155
|
|
|
156 @property
|
|
|
157 def allows_x_number_suffix(self):
|
|
|
158 """Whether vendor tags can be suffixed with X and a number to mark experimental extensions."""
|
|
|
159 return False
|
|
|
160
|
|
|
161 @property
|
|
|
162 @abc.abstractmethod
|
|
|
163 def structtype_member_name(self):
|
|
|
164 """Return name of the structure type member.
|
|
|
165
|
|
|
166 Must implement.
|
|
|
167 """
|
|
|
168 raise NotImplementedError()
|
|
|
169
|
|
|
170 @property
|
|
|
171 @abc.abstractmethod
|
|
|
172 def nextpointer_member_name(self):
|
|
|
173 """Return name of the structure pointer chain member.
|
|
|
174
|
|
|
175 Must implement.
|
|
|
176 """
|
|
|
177 raise NotImplementedError()
|
|
|
178
|
|
|
179 @property
|
|
|
180 @abc.abstractmethod
|
|
|
181 def xml_api_name(self):
|
|
|
182 """Return the name used in the default API XML registry for the default API"""
|
|
|
183 raise NotImplementedError()
|
|
|
184
|
|
|
185 @abc.abstractmethod
|
|
|
186 def generate_structure_type_from_name(self, structname):
|
|
|
187 """Generate a structure type name, like XR_TYPE_CREATE_INSTANCE_INFO.
|
|
|
188
|
|
|
189 Must implement.
|
|
|
190 """
|
|
|
191 raise NotImplementedError()
|
|
|
192
|
|
|
193 def makeStructName(self, name):
|
|
|
194 """Prepend the appropriate format macro for a structure to a structure type name.
|
|
|
195
|
|
|
196 Uses struct_macro, so just override that if you want to change behavior.
|
|
|
197 """
|
|
|
198 return self.struct_macro + name
|
|
|
199
|
|
|
200 def makeExternalTypeName(self, name):
|
|
|
201 """Prepend the appropriate format macro for an external type like uint32_t to a type name.
|
|
|
202
|
|
|
203 Uses external_macro, so just override that if you want to change behavior.
|
|
|
204 """
|
|
|
205 return self.external_macro + name
|
|
|
206
|
|
|
207 def _implMakeProseList(self, elements, fmt, with_verb, comma_for_two_elts=False, serial_comma=True):
|
|
|
208 """Internal-use implementation to make a (comma-separated) list for use in prose.
|
|
|
209
|
|
|
210 Adds a connective (by default, 'and')
|
|
|
211 before the last element if there are more than 1,
|
|
|
212 and only includes commas if there are more than 2
|
|
|
213 (if comma_for_two_elts is False).
|
|
|
214
|
|
|
215 Adds the right one of "is" or "are" to the end if with_verb is true.
|
|
|
216
|
|
|
217 Optionally adds a quantifier (like 'any') before a list of 2 or more,
|
|
|
218 if specified by fmt.
|
|
|
219
|
|
|
220 Do not edit these defaults, override self.makeProseList().
|
|
|
221 """
|
|
|
222 assert serial_comma # did not implement what we did not need
|
|
|
223 if isinstance(fmt, str):
|
|
|
224 fmt = ProseListFormats.from_string(fmt)
|
|
|
225
|
|
|
226 my_elts = list(elements)
|
|
|
227 if len(my_elts) > 1:
|
|
|
228 my_elts[-1] = f'{fmt.connective} {my_elts[-1]}'
|
|
|
229
|
|
|
230 if not comma_for_two_elts and len(my_elts) <= 2:
|
|
|
231 prose = ' '.join(my_elts)
|
|
|
232 else:
|
|
|
233 prose = ', '.join(my_elts)
|
|
|
234
|
|
|
235 quantifier = fmt.quantifier(len(my_elts))
|
|
|
236
|
|
|
237 parts = [quantifier, prose]
|
|
|
238
|
|
|
239 if with_verb:
|
|
|
240 if len(my_elts) > 1:
|
|
|
241 parts.append(' are')
|
|
|
242 else:
|
|
|
243 parts.append(' is')
|
|
|
244 return ''.join(parts)
|
|
|
245
|
|
|
246 @property
|
|
|
247 @abc.abstractmethod
|
|
|
248 def file_suffix(self):
|
|
|
249 """Return suffix of generated Asciidoctor files"""
|
|
|
250 raise NotImplementedError
|
|
|
251
|
|
|
252 @abc.abstractmethod
|
|
|
253 def api_name(self, spectype=None):
|
|
|
254 """Return API or specification name for citations in ref pages.
|
|
|
255
|
|
|
256 spectype is the spec this refpage is for.
|
|
|
257 'api' (the default value) is the main API Specification.
|
|
|
258 If an unrecognized spectype is given, returns None.
|
|
|
259
|
|
|
260 Must implement."""
|
|
|
261 raise NotImplementedError
|
|
|
262
|
|
|
263 def should_insert_may_alias_macro(self, genOpts):
|
|
|
264 """Return true if we should insert a "may alias" macro in this file.
|
|
|
265
|
|
|
266 Only used by OpenXR right now."""
|
|
|
267 return False
|
|
|
268
|
|
|
269 @property
|
|
|
270 def command_prefix(self):
|
|
|
271 """Return the expected prefix of commands/functions.
|
|
|
272
|
|
|
273 Implemented in terms of api_prefix."""
|
|
|
274 if not self._command_prefix:
|
|
|
275 self._command_prefix = self.api_prefix[:].replace('_', '').lower()
|
|
|
276 return self._command_prefix
|
|
|
277
|
|
|
278 @property
|
|
|
279 def type_prefix(self):
|
|
|
280 """Return the expected prefix of type names.
|
|
|
281
|
|
|
282 Implemented in terms of command_prefix (and in turn, api_prefix)."""
|
|
|
283 if not self._type_prefix:
|
|
|
284 self._type_prefix = ''.join(
|
|
|
285 (self.command_prefix[0:1].upper(), self.command_prefix[1:]))
|
|
|
286 return self._type_prefix
|
|
|
287
|
|
|
288 @property
|
|
|
289 @abc.abstractmethod
|
|
|
290 def api_prefix(self):
|
|
|
291 """Return API token prefix.
|
|
|
292
|
|
|
293 Typically two uppercase letters followed by an underscore.
|
|
|
294
|
|
|
295 Must implement."""
|
|
|
296 raise NotImplementedError
|
|
|
297
|
|
|
298 @property
|
|
|
299 def extension_name_prefix(self):
|
|
|
300 """Return extension name prefix.
|
|
|
301
|
|
|
302 Typically two uppercase letters followed by an underscore.
|
|
|
303
|
|
|
304 Assumed to be the same as api_prefix, but some APIs use different
|
|
|
305 case conventions."""
|
|
|
306
|
|
|
307 return self.api_prefix
|
|
|
308
|
|
|
309 def extension_short_description(self, elem):
|
|
|
310 """Return a short description of an extension for use in refpages.
|
|
|
311
|
|
|
312 elem is an ElementTree for the <extension> tag in the XML.
|
|
|
313 The default behavior is to use the 'type' field of this tag, but not
|
|
|
314 all APIs support this field."""
|
|
|
315
|
|
|
316 ext_type = elem.get('type')
|
|
|
317
|
|
|
318 if ext_type is not None:
|
|
|
319 return f'{ext_type} extension'
|
|
|
320 else:
|
|
|
321 return ''
|
|
|
322
|
|
|
323 @property
|
|
|
324 def write_contacts(self):
|
|
|
325 """Return whether contact list should be written to extension appendices"""
|
|
|
326 return False
|
|
|
327
|
|
|
328 @property
|
|
|
329 def write_extension_type(self):
|
|
|
330 """Return whether extension type should be written to extension appendices"""
|
|
|
331 return True
|
|
|
332
|
|
|
333 @property
|
|
|
334 def write_extension_number(self):
|
|
|
335 """Return whether extension number should be written to extension appendices"""
|
|
|
336 return True
|
|
|
337
|
|
|
338 @property
|
|
|
339 def write_extension_revision(self):
|
|
|
340 """Return whether extension revision number should be written to extension appendices"""
|
|
|
341 return True
|
|
|
342
|
|
|
343 @property
|
|
|
344 def write_refpage_include(self):
|
|
|
345 """Return whether refpage include should be written to extension appendices"""
|
|
|
346 return True
|
|
|
347
|
|
|
348 @property
|
|
|
349 def api_version_prefix(self):
|
|
|
350 """Return API core version token prefix.
|
|
|
351
|
|
|
352 Implemented in terms of api_prefix.
|
|
|
353
|
|
|
354 May override."""
|
|
|
355 return f"{self.api_prefix}VERSION_"
|
|
|
356
|
|
|
357 @property
|
|
|
358 def KHR_prefix(self):
|
|
|
359 """Return extension name prefix for KHR extensions.
|
|
|
360
|
|
|
361 Implemented in terms of api_prefix.
|
|
|
362
|
|
|
363 May override."""
|
|
|
364 return f"{self.api_prefix}KHR_"
|
|
|
365
|
|
|
366 @property
|
|
|
367 def EXT_prefix(self):
|
|
|
368 """Return extension name prefix for EXT extensions.
|
|
|
369
|
|
|
370 Implemented in terms of api_prefix.
|
|
|
371
|
|
|
372 May override."""
|
|
|
373 return f"{self.api_prefix}EXT_"
|
|
|
374
|
|
|
375 def writeFeature(self, featureName, featureExtraProtect, filename):
|
|
|
376 """Return True if OutputGenerator.endFeature should write this feature.
|
|
|
377
|
|
|
378 Defaults to always True.
|
|
|
379 Used in COutputGenerator.
|
|
|
380
|
|
|
381 May override."""
|
|
|
382 return True
|
|
|
383
|
|
|
384 def requires_error_validation(self, return_type):
|
|
|
385 """Return True if the return_type element is an API result code
|
|
|
386 requiring error validation.
|
|
|
387
|
|
|
388 Defaults to always False.
|
|
|
389
|
|
|
390 May override."""
|
|
|
391 return False
|
|
|
392
|
|
|
393 @property
|
|
|
394 def required_errors(self):
|
|
|
395 """Return a list of required error codes for validation.
|
|
|
396
|
|
|
397 Defaults to an empty list.
|
|
|
398
|
|
|
399 May override."""
|
|
|
400 return []
|
|
|
401
|
|
|
402 def is_voidpointer_alias(self, tag, text, tail):
|
|
|
403 """Return True if the declaration components (tag,text,tail) of an
|
|
|
404 element represents a void * type.
|
|
|
405
|
|
|
406 Defaults to a reasonable implementation.
|
|
|
407
|
|
|
408 May override."""
|
|
|
409 return tag == 'type' and text == 'void' and tail.startswith('*')
|
|
|
410
|
|
|
411 def make_voidpointer_alias(self, tail):
|
|
|
412 """Reformat a void * declaration to include the API alias macro.
|
|
|
413
|
|
|
414 Defaults to a no-op.
|
|
|
415
|
|
|
416 Must override if you actually want to use this feature in your project."""
|
|
|
417 return tail
|
|
|
418
|
|
|
419 def category_requires_validation(self, category):
|
|
|
420 """Return True if the given type 'category' always requires validation.
|
|
|
421
|
|
|
422 Defaults to a reasonable implementation.
|
|
|
423
|
|
|
424 May override."""
|
|
|
425 return category in CATEGORIES_REQUIRING_VALIDATION
|
|
|
426
|
|
|
427 def type_always_valid(self, typename):
|
|
|
428 """Return True if the given type name is always valid (never requires validation).
|
|
|
429
|
|
|
430 This is for things like integers.
|
|
|
431
|
|
|
432 Defaults to a reasonable implementation.
|
|
|
433
|
|
|
434 May override."""
|
|
|
435 return typename in TYPES_KNOWN_ALWAYS_VALID
|
|
|
436
|
|
|
437 @property
|
|
|
438 def should_skip_checking_codes(self):
|
|
|
439 """Return True if more than the basic validation of return codes should
|
|
|
440 be skipped for a command."""
|
|
|
441
|
|
|
442 return False
|
|
|
443
|
|
|
444 @property
|
|
|
445 def generate_index_terms(self):
|
|
|
446 """Return True if asiidoctor index terms should be generated as part
|
|
|
447 of an API interface from the docgenerator."""
|
|
|
448
|
|
|
449 return False
|
|
|
450
|
|
|
451 @property
|
|
|
452 def generate_enum_table(self):
|
|
|
453 """Return True if asciidoctor tables describing enumerants in a
|
|
|
454 group should be generated as part of group generation."""
|
|
|
455 return False
|
|
|
456
|
|
|
457 @property
|
|
|
458 def generate_max_enum_in_docs(self):
|
|
|
459 """Return True if MAX_ENUM tokens should be generated in
|
|
|
460 documentation includes."""
|
|
|
461 return False
|
|
|
462
|
|
|
463 def extension_name_split(self, name):
|
|
|
464 """Split an extension name, returning (vendor, rest of name).
|
|
|
465 The API prefix of the name is ignored."""
|
|
|
466
|
|
|
467 match = EXT_NAME_DECOMPOSE_RE.match(name)
|
|
|
468 vendor = match.group('vendor')
|
|
|
469 bare_name = match.group('name')
|
|
|
470
|
|
|
471 return (vendor, bare_name)
|
|
|
472
|
|
|
473 @abc.abstractmethod
|
|
|
474 def extension_file_path(self, name):
|
|
|
475 """Return file path to an extension appendix relative to a directory
|
|
|
476 containing all such appendices.
|
|
|
477 - name - extension name
|
|
|
478
|
|
|
479 Must implement."""
|
|
|
480 raise NotImplementedError
|
|
|
481
|
|
|
482 def extension_include_string(self, name):
|
|
|
483 """Return format string for include:: line for an extension appendix
|
|
|
484 file.
|
|
|
485 - name - extension name"""
|
|
|
486
|
|
|
487 return f'include::{{appendices}}/{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'
|