| #!/usr/bin/python3 -i |
| # |
| # Copyright 2013-2021 The Khronos Group Inc. |
| # |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| import re |
| |
| |
| _A_VS_AN_RE = re.compile(r' a ([a-z]+:)?([aAeEiIoOxX]\w+\b)(?!:)') |
| |
| _STARTS_WITH_MACRO_RE = re.compile(r'^[a-z]+:.*') |
| |
| |
| def _checkAnchorComponents(anchor): |
| """Raise an exception if any component of a VUID anchor name is illegal.""" |
| if anchor: |
| # Any other invalid things in an anchor name should be detected here. |
| if any((' ' in anchor_part for anchor_part in anchor)): |
| raise RuntimeError("Illegal component of a VUID anchor name!") |
| |
| |
| def _fix_a_vs_an(s): |
| """Fix usage (often generated) of the indefinite article 'a' when 'an' is appropriate. |
| |
| Explicitly excludes the markup macros.""" |
| return _A_VS_AN_RE.sub(r' an \1\2', s) |
| |
| |
| class ValidityCollection: |
| """Combines validity for a single entity.""" |
| |
| def __init__(self, entity_name=None, conventions=None, strict=True): |
| self.entity_name = entity_name |
| self.conventions = conventions |
| self.lines = [] |
| self.strict = strict |
| |
| def possiblyAddExtensionRequirement(self, extension_name, entity_preface): |
| """Add an extension-related validity statement if required. |
| |
| entity_preface is a string that goes between "must be enabled prior to " |
| and the name of the entity, and normally ends in a macro. |
| For instance, might be "calling flink:" for a function. |
| """ |
| if extension_name and not extension_name.startswith(self.conventions.api_version_prefix): |
| msg = 'The {} extension must: be enabled prior to {}{}'.format( |
| self.conventions.formatExtension(extension_name), entity_preface, self.entity_name) |
| self.addValidityEntry(msg, anchor=('extension', 'notenabled')) |
| |
| def addValidityEntry(self, msg, anchor=None): |
| """Add a validity entry, optionally with a VUID anchor. |
| |
| If any trailing arguments are supplied, |
| an anchor is generated by concatenating them with dashes |
| at the end of the VUID anchor name. |
| """ |
| if not msg: |
| raise RuntimeError("Tried to add a blank validity line!") |
| parts = ['*'] |
| _checkAnchorComponents(anchor) |
| if anchor: |
| if not self.entity_name: |
| raise RuntimeError('Cannot add a validity entry with an anchor to a collection that does not know its entity name.') |
| parts.append('[[{}]]'.format( |
| '-'.join(['VUID', self.entity_name] + list(anchor)))) |
| parts.append(msg) |
| combined = _fix_a_vs_an(' '.join(parts)) |
| if combined in self.lines: |
| raise RuntimeError("Duplicate validity added!") |
| self.lines.append(combined) |
| |
| def addText(self, msg): |
| """Add already formatted validity text.""" |
| if self.strict: |
| raise RuntimeError('addText called when collection in strict mode') |
| if not msg: |
| return |
| msg = msg.rstrip() |
| if not msg: |
| return |
| self.lines.append(msg) |
| |
| def _extend(self, lines): |
| lines = list(lines) |
| dupes = set(lines).intersection(self.lines) |
| if dupes: |
| raise RuntimeError("The two sets contain some shared entries! " + str(dupes)) |
| self.lines.extend(lines) |
| |
| def __iadd__(self, other): |
| """Perform += with a string, iterable, or ValidityCollection.""" |
| if other is None: |
| pass |
| elif isinstance(other, str): |
| if self.strict: |
| raise RuntimeError( |
| 'Collection += a string when collection in strict mode') |
| if not other: |
| # empty string |
| pass |
| elif other.startswith('*'): |
| # Handle already-formatted |
| self.addText(other) |
| else: |
| # Do the formatting ourselves. |
| self.addValidityEntry(other) |
| elif isinstance(other, ValidityEntry): |
| if other: |
| if other.verbose: |
| print(self.entity_name, 'Appending', str(other)) |
| self.addValidityEntry(str(other), anchor=other.anchor) |
| elif isinstance(other, ValidityCollection): |
| if not self.entity_name == other.entity_name: |
| raise RuntimeError( |
| "Trying to combine two ValidityCollections for different entities!") |
| self._extend(other.lines) |
| else: |
| # Deal with other iterables. |
| self._extend(other) |
| |
| return self |
| |
| def __bool__(self): |
| """Is the collection non-empty?""" |
| empty = not self.lines |
| return not empty |
| |
| @property |
| def text(self): |
| """Access validity statements as a single string or None.""" |
| if not self.lines: |
| return None |
| return '\n'.join(self.lines) + '\n' |
| |
| def __str__(self): |
| """Access validity statements as a single string or empty string.""" |
| if not self: |
| return '' |
| return self.text |
| |
| def __repr__(self): |
| return '<ValidityCollection: {}>'.format(self.lines) |
| |
| |
| class ValidityEntry: |
| """A single validity line in progress.""" |
| |
| def __init__(self, text=None, anchor=None): |
| """Prepare to add a validity entry, optionally with a VUID anchor. |
| |
| An anchor is generated by concatenating the elements of the anchor tuple with dashes |
| at the end of the VUID anchor name. |
| """ |
| _checkAnchorComponents(anchor) |
| if isinstance(anchor, str): |
| # anchor needs to be a tuple |
| anchor = (anchor,) |
| |
| # VUID does not allow special chars except ":" |
| if anchor is not None: |
| anchor = [(anchor_value.replace('->', '::').replace('.', '::')) for anchor_value in anchor] |
| |
| self.anchor = anchor |
| self.parts = [] |
| self.verbose = False |
| if text: |
| self.append(text) |
| |
| def append(self, part): |
| """Append a part of a string. |
| |
| If this is the first entry part and the part doesn't start |
| with a markup macro, the first character will be capitalized.""" |
| if not self.parts and not _STARTS_WITH_MACRO_RE.match(part): |
| self.parts.append(part[:1].upper()) |
| self.parts.append(part[1:]) |
| else: |
| self.parts.append(part) |
| if self.verbose: |
| print('ValidityEntry', id(self), 'after append:', str(self)) |
| |
| def drop_end(self, n): |
| """Remove up to n trailing characters from the string.""" |
| temp = str(self) |
| n = min(len(temp), n) |
| self.parts = [temp[:-n]] |
| |
| def __iadd__(self, other): |
| """Perform += with a string,""" |
| self.append(other) |
| return self |
| |
| def __bool__(self): |
| """Return true if we have something more than just an anchor.""" |
| empty = not self.parts |
| return not empty |
| |
| def __str__(self): |
| """Access validity statement as a single string or empty string.""" |
| if not self: |
| raise RuntimeError("No parts added?") |
| return ''.join(self.parts).strip() |
| |
| def __repr__(self): |
| parts = ['<ValidityEntry: '] |
| if self: |
| parts.append('"') |
| parts.append(str(self)) |
| parts.append('"') |
| else: |
| parts.append('EMPTY') |
| if self.anchor: |
| parts.append(', anchor={}'.format('-'.join(self.anchor))) |
| parts.append('>') |
| return ''.join(parts) |