• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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