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