1#!/usr/bin/python3 -i 2# 3# Copyright 2013-2021 The Khronos Group Inc. 4# 5# SPDX-License-Identifier: Apache-2.0 6 7import re 8 9 10_A_VS_AN_RE = re.compile(r' a ([a-z]+:)?([aAeEiIoOxX]\w+\b)(?!:)') 11 12_STARTS_WITH_MACRO_RE = re.compile(r'^[a-z]+:.*') 13 14 15def _checkAnchorComponents(anchor): 16 """Raise an exception if any component of a VUID anchor name is illegal.""" 17 if anchor: 18 # Any other invalid things in an anchor name should be detected here. 19 if any((' ' in anchor_part for anchor_part in anchor)): 20 raise RuntimeError("Illegal component of a VUID anchor name!") 21 22 23def _fix_a_vs_an(s): 24 """Fix usage (often generated) of the indefinite article 'a' when 'an' is appropriate. 25 26 Explicitly excludes the markup macros.""" 27 return _A_VS_AN_RE.sub(r' an \1\2', s) 28 29 30class ValidityCollection: 31 """Combines validity for a single entity.""" 32 33 def __init__(self, entity_name=None, conventions=None, strict=True): 34 self.entity_name = entity_name 35 self.conventions = conventions 36 self.lines = [] 37 self.strict = strict 38 39 def possiblyAddExtensionRequirement(self, extension_name, entity_preface): 40 """Add an extension-related validity statement if required. 41 42 entity_preface is a string that goes between "must be enabled prior to " 43 and the name of the entity, and normally ends in a macro. 44 For instance, might be "calling flink:" for a function. 45 """ 46 if extension_name and not extension_name.startswith(self.conventions.api_version_prefix): 47 msg = 'The {} extension must: be enabled prior to {}{}'.format( 48 self.conventions.formatExtension(extension_name), entity_preface, self.entity_name) 49 self.addValidityEntry(msg, anchor=('extension', 'notenabled')) 50 51 def addValidityEntry(self, msg, anchor=None): 52 """Add a validity entry, optionally with a VUID anchor. 53 54 If any trailing arguments are supplied, 55 an anchor is generated by concatenating them with dashes 56 at the end of the VUID anchor name. 57 """ 58 if not msg: 59 raise RuntimeError("Tried to add a blank validity line!") 60 parts = ['*'] 61 _checkAnchorComponents(anchor) 62 if anchor: 63 if not self.entity_name: 64 raise RuntimeError('Cannot add a validity entry with an anchor to a collection that does not know its entity name.') 65 parts.append('[[{}]]'.format( 66 '-'.join(['VUID', self.entity_name] + list(anchor)))) 67 parts.append(msg) 68 combined = _fix_a_vs_an(' '.join(parts)) 69 if combined in self.lines: 70 raise RuntimeError("Duplicate validity added!") 71 self.lines.append(combined) 72 73 def addText(self, msg): 74 """Add already formatted validity text.""" 75 if self.strict: 76 raise RuntimeError('addText called when collection in strict mode') 77 if not msg: 78 return 79 msg = msg.rstrip() 80 if not msg: 81 return 82 self.lines.append(msg) 83 84 def _extend(self, lines): 85 lines = list(lines) 86 dupes = set(lines).intersection(self.lines) 87 if dupes: 88 raise RuntimeError("The two sets contain some shared entries! " + str(dupes)) 89 self.lines.extend(lines) 90 91 def __iadd__(self, other): 92 """Perform += with a string, iterable, or ValidityCollection.""" 93 if other is None: 94 pass 95 elif isinstance(other, str): 96 if self.strict: 97 raise RuntimeError( 98 'Collection += a string when collection in strict mode') 99 if not other: 100 # empty string 101 pass 102 elif other.startswith('*'): 103 # Handle already-formatted 104 self.addText(other) 105 else: 106 # Do the formatting ourselves. 107 self.addValidityEntry(other) 108 elif isinstance(other, ValidityEntry): 109 if other: 110 if other.verbose: 111 print(self.entity_name, 'Appending', str(other)) 112 self.addValidityEntry(str(other), anchor=other.anchor) 113 elif isinstance(other, ValidityCollection): 114 if not self.entity_name == other.entity_name: 115 raise RuntimeError( 116 "Trying to combine two ValidityCollections for different entities!") 117 self._extend(other.lines) 118 else: 119 # Deal with other iterables. 120 self._extend(other) 121 122 return self 123 124 def __bool__(self): 125 """Is the collection non-empty?""" 126 empty = not self.lines 127 return not empty 128 129 @property 130 def text(self): 131 """Access validity statements as a single string or None.""" 132 if not self.lines: 133 return None 134 return '\n'.join(self.lines) + '\n' 135 136 def __str__(self): 137 """Access validity statements as a single string or empty string.""" 138 if not self: 139 return '' 140 return self.text 141 142 def __repr__(self): 143 return '<ValidityCollection: {}>'.format(self.lines) 144 145 146class ValidityEntry: 147 """A single validity line in progress.""" 148 149 def __init__(self, text=None, anchor=None): 150 """Prepare to add a validity entry, optionally with a VUID anchor. 151 152 An anchor is generated by concatenating the elements of the anchor tuple with dashes 153 at the end of the VUID anchor name. 154 """ 155 _checkAnchorComponents(anchor) 156 if isinstance(anchor, str): 157 # anchor needs to be a tuple 158 anchor = (anchor,) 159 160 # VUID does not allow special chars except ":" 161 if anchor is not None: 162 anchor = [(anchor_value.replace('->', '::').replace('.', '::')) for anchor_value in anchor] 163 164 self.anchor = anchor 165 self.parts = [] 166 self.verbose = False 167 if text: 168 self.append(text) 169 170 def append(self, part): 171 """Append a part of a string. 172 173 If this is the first entry part and the part doesn't start 174 with a markup macro, the first character will be capitalized.""" 175 if not self.parts and not _STARTS_WITH_MACRO_RE.match(part): 176 self.parts.append(part[:1].upper()) 177 self.parts.append(part[1:]) 178 else: 179 self.parts.append(part) 180 if self.verbose: 181 print('ValidityEntry', id(self), 'after append:', str(self)) 182 183 def drop_end(self, n): 184 """Remove up to n trailing characters from the string.""" 185 temp = str(self) 186 n = min(len(temp), n) 187 self.parts = [temp[:-n]] 188 189 def __iadd__(self, other): 190 """Perform += with a string,""" 191 self.append(other) 192 return self 193 194 def __bool__(self): 195 """Return true if we have something more than just an anchor.""" 196 empty = not self.parts 197 return not empty 198 199 def __str__(self): 200 """Access validity statement as a single string or empty string.""" 201 if not self: 202 raise RuntimeError("No parts added?") 203 return ''.join(self.parts).strip() 204 205 def __repr__(self): 206 parts = ['<ValidityEntry: '] 207 if self: 208 parts.append('"') 209 parts.append(str(self)) 210 parts.append('"') 211 else: 212 parts.append('EMPTY') 213 if self.anchor: 214 parts.append(', anchor={}'.format('-'.join(self.anchor))) 215 parts.append('>') 216 return ''.join(parts) 217