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