|
1501
|
1 #!/usr/bin/env python3
|
|
|
2
|
|
|
3 # Copyright 2022-2025 The Khronos Group Inc.
|
|
|
4 # Copyright 2003-2019 Paul McGuire
|
|
|
5 # SPDX-License-Identifier: MIT
|
|
|
6
|
|
|
7 # apirequirements.py - parse 'depends' expressions in API XML
|
|
|
8 # Supported methods:
|
|
|
9 # dependency - the expression string
|
|
|
10 #
|
|
|
11 # evaluateDependency(dependency, isSupported) evaluates the expression,
|
|
|
12 # returning a boolean result. isSupported takes an extension or version name
|
|
|
13 # string and returns a boolean.
|
|
|
14 #
|
|
|
15 # dependencyLanguage(dependency) returns an English string equivalent
|
|
|
16 # to the expression, suitable for header file comments.
|
|
|
17 #
|
|
|
18 # dependencyNames(dependency) returns a set of the extension and
|
|
|
19 # version names in the expression.
|
|
|
20 #
|
|
|
21 # dependencyMarkup(dependency) returns a string containing asciidoctor
|
|
|
22 # markup for English equivalent to the expression, suitable for extension
|
|
|
23 # appendices.
|
|
|
24 #
|
|
|
25 # All may throw a ParseException if the expression cannot be parsed or is
|
|
|
26 # not completely consumed by parsing.
|
|
|
27
|
|
|
28 # Supported expressions at present:
|
|
|
29 # - extension names
|
|
|
30 # - '+' as AND connector
|
|
|
31 # - ',' as OR connector
|
|
|
32 # - parenthesization for grouping
|
|
|
33
|
|
|
34 # Based on `examples/fourFn.py` from the
|
|
|
35 # https://github.com/pyparsing/pyparsing/ repository.
|
|
|
36
|
|
|
37 from pyparsing import (
|
|
|
38 Literal,
|
|
|
39 Word,
|
|
|
40 Group,
|
|
|
41 Forward,
|
|
|
42 alphas,
|
|
|
43 alphanums,
|
|
|
44 Regex,
|
|
|
45 ParseException,
|
|
|
46 CaselessKeyword,
|
|
|
47 Suppress,
|
|
|
48 delimitedList,
|
|
|
49 infixNotation,
|
|
|
50 )
|
|
|
51 import math
|
|
|
52 import operator
|
|
|
53 import pyparsing as pp
|
|
|
54 import re
|
|
|
55
|
|
|
56 from apiconventions import APIConventions as APIConventions
|
|
|
57 conventions = APIConventions()
|
|
|
58
|
|
|
59 def markupPassthrough(name):
|
|
|
60 """Pass a name (leaf or operator) through without applying markup"""
|
|
|
61 return name
|
|
|
62
|
|
|
63 def leafMarkupAsciidoc(name):
|
|
|
64 """Markup a leaf name as an asciidoc link to an API version or extension
|
|
|
65 anchor.
|
|
|
66
|
|
|
67 - name - version or extension name"""
|
|
|
68
|
|
|
69 return conventions.formatVersionOrExtension(name)
|
|
|
70
|
|
|
71 def leafMarkupC(name):
|
|
|
72 """Markup a leaf name as a C expression, using conventions of the
|
|
|
73 Vulkan Validation Layers
|
|
|
74
|
|
|
75 - name - version or extension name"""
|
|
|
76
|
|
|
77 (apivariant, major, minor) = apiVersionNameMatch(name)
|
|
|
78
|
|
|
79 if apivariant is not None:
|
|
|
80 return name
|
|
|
81 else:
|
|
|
82 return f'ext.{name}'
|
|
|
83
|
|
|
84 opMarkupAsciidocMap = { '+' : 'and', ',' : 'or' }
|
|
|
85
|
|
|
86 def opMarkupAsciidoc(op):
|
|
|
87 """Markup an operator as an asciidoc spec markup equivalent
|
|
|
88
|
|
|
89 - op - operator ('+' or ',')"""
|
|
|
90
|
|
|
91 return opMarkupAsciidocMap[op]
|
|
|
92
|
|
|
93 opMarkupCMap = { '+' : '&&', ',' : '||' }
|
|
|
94
|
|
|
95 def opMarkupC(op):
|
|
|
96 """Markup an operator as a C language equivalent
|
|
|
97
|
|
|
98 - op - operator ('+' or ',')"""
|
|
|
99
|
|
|
100 return opMarkupCMap[op]
|
|
|
101
|
|
|
102
|
|
|
103 # Unfortunately global to be used in pyparsing
|
|
|
104 exprStack = []
|
|
|
105
|
|
|
106 def push_first(toks):
|
|
|
107 """Push a token on the global stack
|
|
|
108
|
|
|
109 - toks - first element is the token to push"""
|
|
|
110
|
|
|
111 exprStack.append(toks[0])
|
|
|
112
|
|
|
113 # An identifier (version, feature boolean, or extension name)
|
|
|
114 dependencyIdent = Word(f"{alphanums}_:")
|
|
|
115
|
|
|
116 # Infix expression for depends expressions
|
|
|
117 dependencyExpr = pp.infixNotation(dependencyIdent,
|
|
|
118 [ (pp.oneOf(', +'), 2, pp.opAssoc.LEFT), ])
|
|
|
119
|
|
|
120 # BNF grammar for depends expressions
|
|
|
121 _bnf = None
|
|
|
122 def dependencyBNF():
|
|
|
123 """
|
|
|
124 boolop :: '+' | ','
|
|
|
125 extname :: Char(alphas)
|
|
|
126 atom :: extname | '(' expr ')'
|
|
|
127 expr :: atom [ boolop atom ]*
|
|
|
128 """
|
|
|
129 global _bnf
|
|
|
130 if _bnf is None:
|
|
|
131 and_, or_ = map(Literal, '+,')
|
|
|
132 lpar, rpar = map(Suppress, '()')
|
|
|
133 boolop = and_ | or_
|
|
|
134
|
|
|
135 expr = Forward()
|
|
|
136 expr_list = delimitedList(Group(expr))
|
|
|
137 atom = (
|
|
|
138 boolop[...]
|
|
|
139 + (
|
|
|
140 (dependencyIdent).setParseAction(push_first)
|
|
|
141 | Group(lpar + expr + rpar)
|
|
|
142 )
|
|
|
143 )
|
|
|
144
|
|
|
145 expr <<= atom + (boolop + atom).setParseAction(push_first)[...]
|
|
|
146 _bnf = expr
|
|
|
147 return _bnf
|
|
|
148
|
|
|
149
|
|
|
150 # map operator symbols to corresponding arithmetic operations
|
|
|
151 _opn = {
|
|
|
152 '+': operator.and_,
|
|
|
153 ',': operator.or_,
|
|
|
154 }
|
|
|
155
|
|
|
156 def evaluateStack(stack, isSupported):
|
|
|
157 """Evaluate an expression stack, returning a boolean result.
|
|
|
158
|
|
|
159 - stack - the stack
|
|
|
160 - isSupported - function taking a version or extension name string and
|
|
|
161 returning True or False if that name is supported or not."""
|
|
|
162
|
|
|
163 op, num_args = stack.pop(), 0
|
|
|
164 if isinstance(op, tuple):
|
|
|
165 op, num_args = op
|
|
|
166
|
|
|
167 if op in '+,':
|
|
|
168 # Note: operands are pushed onto the stack in reverse order
|
|
|
169 op2 = evaluateStack(stack, isSupported)
|
|
|
170 op1 = evaluateStack(stack, isSupported)
|
|
|
171 return _opn[op](op1, op2)
|
|
|
172 elif op[0].isalpha():
|
|
|
173 return isSupported(op)
|
|
|
174 else:
|
|
|
175 raise Exception(f'invalid op: {op}')
|
|
|
176
|
|
|
177 def evaluateDependency(dependency, isSupported):
|
|
|
178 """Evaluate a dependency expression, returning a boolean result.
|
|
|
179
|
|
|
180 - dependency - the expression
|
|
|
181 - isSupported - function taking a version or extension name string and
|
|
|
182 returning True or False if that name is supported or not."""
|
|
|
183
|
|
|
184 global exprStack
|
|
|
185 exprStack = []
|
|
|
186 results = dependencyBNF().parseString(dependency, parseAll=True)
|
|
|
187 val = evaluateStack(exprStack[:], isSupported)
|
|
|
188 return val
|
|
|
189
|
|
|
190 def evalDependencyLanguage(stack, leafMarkup, opMarkup, parenthesize, root):
|
|
|
191 """Evaluate an expression stack, returning an English equivalent
|
|
|
192
|
|
|
193 - stack - the stack
|
|
|
194 - leafMarkup, opMarkup, parenthesize - same as dependencyLanguage
|
|
|
195 - root - True only if this is the outer (root) expression level"""
|
|
|
196
|
|
|
197 op, num_args = stack.pop(), 0
|
|
|
198 if isinstance(op, tuple):
|
|
|
199 op, num_args = op
|
|
|
200 if op in '+,':
|
|
|
201 # Could parenthesize, not needed yet
|
|
|
202 rhs = evalDependencyLanguage(stack, leafMarkup, opMarkup, parenthesize, root = False)
|
|
|
203 opname = opMarkup(op)
|
|
|
204 lhs = evalDependencyLanguage(stack, leafMarkup, opMarkup, parenthesize, root = False)
|
|
|
205 if parenthesize and not root:
|
|
|
206 return f'({lhs} {opname} {rhs})'
|
|
|
207 else:
|
|
|
208 return f'{lhs} {opname} {rhs}'
|
|
|
209 elif op[0].isalpha():
|
|
|
210 # This is an extension or feature name
|
|
|
211 return leafMarkup(op)
|
|
|
212 else:
|
|
|
213 raise Exception(f'invalid op: {op}')
|
|
|
214
|
|
|
215 def dependencyLanguage(dependency, leafMarkup, opMarkup, parenthesize):
|
|
|
216 """Return an API dependency expression translated to a form suitable for
|
|
|
217 asciidoctor conditionals or header file comments.
|
|
|
218
|
|
|
219 - dependency - the expression
|
|
|
220 - leafMarkup - function taking an extension / version name and
|
|
|
221 returning an equivalent marked up version
|
|
|
222 - opMarkup - function taking an operator ('+' / ',') name name and
|
|
|
223 returning an equivalent marked up version
|
|
|
224 - parenthesize - True if parentheses should be used in the resulting
|
|
|
225 expression, False otherwise"""
|
|
|
226
|
|
|
227 global exprStack
|
|
|
228 exprStack = []
|
|
|
229 results = dependencyBNF().parseString(dependency, parseAll=True)
|
|
|
230 return evalDependencyLanguage(exprStack, leafMarkup, opMarkup, parenthesize, root = True)
|
|
|
231
|
|
|
232 # aka specmacros = False
|
|
|
233 def dependencyLanguageComment(dependency):
|
|
|
234 """Return dependency expression translated to a form suitable for
|
|
|
235 comments in headers of emitted C code, as used by the
|
|
|
236 docgenerator."""
|
|
|
237 return dependencyLanguage(dependency, leafMarkup = markupPassthrough, opMarkup = opMarkupAsciidoc, parenthesize = True)
|
|
|
238
|
|
|
239 # aka specmacros = True
|
|
|
240 def dependencyLanguageSpecMacros(dependency):
|
|
|
241 """Return dependency expression translated to a form suitable for
|
|
|
242 comments in headers of emitted C code, as used by the
|
|
|
243 interfacegenerator."""
|
|
|
244 return dependencyLanguage(dependency, leafMarkup = leafMarkupAsciidoc, opMarkup = opMarkupAsciidoc, parenthesize = False)
|
|
|
245
|
|
|
246 def dependencyLanguageC(dependency):
|
|
|
247 """Return dependency expression translated to a form suitable for
|
|
|
248 use in C expressions"""
|
|
|
249 return dependencyLanguage(dependency, leafMarkup = leafMarkupC, opMarkup = opMarkupC, parenthesize = True)
|
|
|
250
|
|
|
251 def evalDependencyNames(stack):
|
|
|
252 """Evaluate an expression stack, returning the set of extension and
|
|
|
253 feature names used in the expression.
|
|
|
254
|
|
|
255 - stack - the stack"""
|
|
|
256
|
|
|
257 op, num_args = stack.pop(), 0
|
|
|
258 if isinstance(op, tuple):
|
|
|
259 op, num_args = op
|
|
|
260 if op in '+,':
|
|
|
261 # Do not evaluate the operation. We only care about the names.
|
|
|
262 return evalDependencyNames(stack) | evalDependencyNames(stack)
|
|
|
263 elif op[0].isalpha():
|
|
|
264 return { op }
|
|
|
265 else:
|
|
|
266 raise Exception(f'invalid op: {op}')
|
|
|
267
|
|
|
268 def dependencyNames(dependency):
|
|
|
269 """Return a set of the extension and version names in an API dependency
|
|
|
270 expression. Used when determining transitive dependencies for spec
|
|
|
271 generation with specific extensions included.
|
|
|
272
|
|
|
273 - dependency - the expression"""
|
|
|
274
|
|
|
275 global exprStack
|
|
|
276 exprStack = []
|
|
|
277 results = dependencyBNF().parseString(dependency, parseAll=True)
|
|
|
278 # print(f'names(): stack = {exprStack}')
|
|
|
279 return evalDependencyNames(exprStack)
|
|
|
280
|
|
|
281 def markupTraverse(expr, level = 0, root = True):
|
|
|
282 """Recursively process a dependency in infix form, transforming it into
|
|
|
283 asciidoctor markup with expression nesting indicated by indentation
|
|
|
284 level.
|
|
|
285
|
|
|
286 - expr - expression to process
|
|
|
287 - level - indentation level to render expression at
|
|
|
288 - root - True only on initial call"""
|
|
|
289
|
|
|
290 if level > 0:
|
|
|
291 prefix = f"{'{nbsp}{nbsp}' * level * 2} "
|
|
|
292 else:
|
|
|
293 prefix = ''
|
|
|
294 str = ''
|
|
|
295
|
|
|
296 for elem in expr:
|
|
|
297 if isinstance(elem, pp.ParseResults):
|
|
|
298 if not root:
|
|
|
299 nextlevel = level + 1
|
|
|
300 else:
|
|
|
301 # Do not indent the outer expression
|
|
|
302 nextlevel = level
|
|
|
303
|
|
|
304 str = str + markupTraverse(elem, level = nextlevel, root = False)
|
|
|
305 elif elem in ('+', ','):
|
|
|
306 str = f"{str}{prefix}{opMarkupAsciidoc(elem)} +\n"
|
|
|
307 else:
|
|
|
308 str = f"{str}{prefix}{leafMarkupAsciidoc(elem)} +\n"
|
|
|
309
|
|
|
310 return str
|
|
|
311
|
|
|
312 def dependencyMarkup(dependency):
|
|
|
313 """Return asciidoctor markup for a human-readable equivalent of an API
|
|
|
314 dependency expression, suitable for use in extension appendix
|
|
|
315 metadata.
|
|
|
316
|
|
|
317 - dependency - the expression"""
|
|
|
318
|
|
|
319 parsed = dependencyExpr.parseString(dependency)
|
|
|
320 return markupTraverse(parsed)
|
|
|
321
|
|
|
322 if __name__ == "__main__":
|
|
|
323 for str in [ 'VK_VERSION_1_0', 'cl_khr_extension_name', 'XR_VERSION_3_2', 'CL_VERSION_1_0' ]:
|
|
|
324 print(f'{str} -> {conventions.formatVersionOrExtension(str)}')
|
|
|
325 import sys
|
|
|
326 sys.exit(0)
|
|
|
327
|
|
|
328 termdict = {
|
|
|
329 'VK_VERSION_1_1' : True,
|
|
|
330 'false' : False,
|
|
|
331 'true' : True,
|
|
|
332 }
|
|
|
333 termSupported = lambda name: name in termdict and termdict[name]
|
|
|
334
|
|
|
335 def test(dependency, expected):
|
|
|
336 val = False
|
|
|
337 try:
|
|
|
338 val = evaluateDependency(dependency, termSupported)
|
|
|
339 except ParseException as pe:
|
|
|
340 print(dependency, f'failed parse: {dependency}')
|
|
|
341 except Exception as e:
|
|
|
342 print(dependency, f'failed eval: {dependency}')
|
|
|
343
|
|
|
344 if val == expected:
|
|
|
345 True
|
|
|
346 # print(f'{dependency} = {val} (as expected)')
|
|
|
347 else:
|
|
|
348 print(f'{dependency} ERROR: {val} != {expected}')
|
|
|
349
|
|
|
350 # Verify expressions are evaluated left-to-right
|
|
|
351
|
|
|
352 test('false,false+false', False)
|
|
|
353 test('false,false+true', False)
|
|
|
354 test('false,true+false', False)
|
|
|
355 test('false,true+true', True)
|
|
|
356 test('true,false+false', False)
|
|
|
357 test('true,false+true', True)
|
|
|
358 test('true,true+false', False)
|
|
|
359 test('true,true+true', True)
|
|
|
360
|
|
|
361 test('false,(false+false)', False)
|
|
|
362 test('false,(false+true)', False)
|
|
|
363 test('false,(true+false)', False)
|
|
|
364 test('false,(true+true)', True)
|
|
|
365 test('true,(false+false)', True)
|
|
|
366 test('true,(false+true)', True)
|
|
|
367 test('true,(true+false)', True)
|
|
|
368 test('true,(true+true)', True)
|
|
|
369
|
|
|
370
|
|
|
371 test('false+false,false', False)
|
|
|
372 test('false+false,true', True)
|
|
|
373 test('false+true,false', False)
|
|
|
374 test('false+true,true', True)
|
|
|
375 test('true+false,false', False)
|
|
|
376 test('true+false,true', True)
|
|
|
377 test('true+true,false', True)
|
|
|
378 test('true+true,true', True)
|
|
|
379
|
|
|
380 test('false+(false,false)', False)
|
|
|
381 test('false+(false,true)', False)
|
|
|
382 test('false+(true,false)', False)
|
|
|
383 test('false+(true,true)', False)
|
|
|
384 test('true+(false,false)', False)
|
|
|
385 test('true+(false,true)', True)
|
|
|
386 test('true+(true,false)', True)
|
|
|
387 test('true+(true,true)', True)
|
|
|
388
|
|
|
389 # Check formatting
|
|
|
390 for dependency in [
|
|
|
391 #'true',
|
|
|
392 #'true+true+false',
|
|
|
393 'true+false',
|
|
|
394 'true+(true+false),(false,true)',
|
|
|
395 #'true+((true+false),(false,true))',
|
|
|
396 'VK_VERSION_1_0+VK_KHR_display',
|
|
|
397 #'VK_VERSION_1_1+(true,false)',
|
|
|
398 ]:
|
|
|
399 print(f'expr = {dependency}\n{dependencyMarkup(dependency)}')
|
|
|
400 print(f' spec language = {dependencyLanguageSpecMacros(dependency)}')
|
|
|
401 print(f' comment language = {dependencyLanguageComment(dependency)}')
|
|
|
402 print(f' C language = {dependencyLanguageC(dependency)}')
|
|
|
403 print(f' names = {dependencyNames(dependency)}')
|
|
|
404 print(f' value = {evaluateDependency(dependency, termSupported)}')
|