xref: /aosp_15_r20/external/vulkan-headers/registry/parse_dependency.py (revision 902771965e4c6d39c75c62130a6a330c08b024db)
1#!/usr/bin/python3
2
3# Copyright 2022-2024 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
37from 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)
51import math
52import operator
53import pyparsing as pp
54import re
55
56from apiconventions import APIConventions as APIConventions
57conventions = APIConventions()
58
59def markupPassthrough(name):
60    """Pass a name (leaf or operator) through without applying markup"""
61    return name
62
63def 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
71def 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
84opMarkupAsciidocMap = { '+' : 'and', ',' : 'or' }
85
86def opMarkupAsciidoc(op):
87    """Markup an operator as an asciidoc spec markup equivalent
88
89       - op - operator ('+' or ',')"""
90
91    return opMarkupAsciidocMap[op]
92
93opMarkupCMap = { '+' : '&&', ',' : '||' }
94
95def 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
104exprStack = []
105
106def 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 or extension name)
114dependencyIdent = Word(alphanums + '_')
115
116# Infix expression for depends expressions
117dependencyExpr = pp.infixNotation(dependencyIdent,
118    [ (pp.oneOf(', +'), 2, pp.opAssoc.LEFT), ])
119
120# BNF grammar for depends expressions
121_bnf = None
122def 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
156def 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
177def 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
190def 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
215def 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
233def 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
240def 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
246def 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
251def 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
268def 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
281def 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 = '{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 = str + f'{prefix}{opMarkupAsciidoc(elem)} +\n'
307        else:
308            str = str + f'{prefix}{leafMarkupAsciidoc(elem)} +\n'
309
310    return str
311
312def 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
322if __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)}')
405