• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2022 The Khronos Group Inc.
2# Copyright 2003-2019 Paul McGuire
3# SPDX-License-Identifier: MIT
4
5# apirequirements.py - parse 'depends' / 'extension' expressions in API XML
6# Supported expressions at present:
7#   - extension names
8#   - '+' as AND connector
9#   - ',' as OR connector
10#   - parenthesization for grouping (not used yet)
11#
12
13# Based on https://github.com/pyparsing/pyparsing/blob/master/examples/fourFn.py
14
15from pyparsing import (
16    Literal,
17    Word,
18    Group,
19    Forward,
20    alphas,
21    alphanums,
22    Regex,
23    ParseException,
24    CaselessKeyword,
25    Suppress,
26    delimitedList,
27)
28import math
29import operator
30import re
31
32exprStack = []
33
34def push_first(toks):
35    # print(f'push_first(toks = {toks}): |exprStack| = {len(exprStack)} exprStack = {exprStack}')
36
37    exprStack.append(toks[0])
38
39bnf = None
40
41def dependencyBNF():
42    """
43    boolop  :: '+' | ','
44    extname :: Char(alphas)
45    atom    :: extname | '(' expr ')'
46    expr    :: atom [ boolop atom ]*
47    """
48    global bnf
49    if not bnf:
50        ident = Word(alphanums + '_')
51
52        and_, or_ = map(Literal, '+,')
53        lpar, rpar = map(Suppress, '()')
54        boolop = and_ | or_
55
56        expr = Forward()
57        expr_list = delimitedList(Group(expr))
58        atom = (
59            boolop[...]
60            + (
61                (ident).setParseAction(push_first)
62                | Group(lpar + expr + rpar)
63            )
64        )
65
66        expr <<= atom + (boolop + atom).setParseAction(push_first)[...]
67        bnf = expr
68    return bnf
69
70
71# map operator symbols to corresponding arithmetic operations
72opn = {
73    '+': operator.and_,
74    ',': operator.or_,
75}
76
77# map operator symbols to corresponding words
78opname = {
79    '+': 'and',
80    ',': 'or',
81}
82
83def extensionIsSupported(extname):
84    return True
85
86def evaluate_stack(s):
87    op, num_args = s.pop(), 0
88    # print(f'evaluate_stack: op = {op} num_args {num_args}')
89    if isinstance(op, tuple):
90        op, num_args = op
91    if op in '+,':
92        # note: operands are pushed onto the stack in reverse order
93        op2 = evaluate_stack(s)
94        op1 = evaluate_stack(s)
95        return opn[op](op1, op2)
96    elif op[0].isalpha():
97        # print(f'extname {op} => {supported(op)}')
98        return extensionIsSupported(op)
99    else:
100        raise Exception(f'invalid op: {op}')
101
102def evalDependencyLanguage(s, specmacros):
103    """Evaluate an expression stack, returning an English equivalent
104
105     - s - the stack
106     - specmacros - if True, prepare the language for spec inclusion"""
107
108    op, num_args = s.pop(), 0
109    # print(f'evalDependencyLanguage: op = {op} num_args {num_args}')
110    if isinstance(op, tuple):
111        op, num_args = op
112    if op in '+,':
113        # @@ Should parenthesize, not needed yet
114        rhs = evalDependencyLanguage(s, specmacros)
115        return evalDependencyLanguage(s, specmacros) + f' {opname[op]} ' + rhs
116    elif op[0].isalpha():
117        # This is an extension or feature name
118        if specmacros:
119            match = re.search("[A-Z]+_VERSION_([0-9]+)_([0-9]+)", op)
120            if match is not None:
121                major = match.group(1)
122                minor = match.group(2)
123                version = major + '.' + minor
124                return f'<<versions-{major}.{minor}, Version {version}>>'
125            else:
126                return 'apiext:' + op
127        else:
128            return op
129    else:
130        raise Exception(f'invalid op: {op}')
131
132def dependencyLanguage(dependency, specmacros = False):
133    """Return an API dependency expression translated to natural language.
134
135     - dependency - the expression
136     - specmacros - if True, prepare the language for spec inclusion with
137       macros and xrefs included"""
138
139    global exprStack
140    exprStack = []
141    results = dependencyBNF().parseString(dependency, parseAll=True)
142    # print(f'language(): stack = {exprStack}')
143    return evalDependencyLanguage(exprStack, specmacros)
144
145def evalDependencyNames(s):
146    """Evaluate an expression stack, returning a set of names
147
148     - s - the stack"""
149
150    op, num_args = s.pop(), 0
151    # print(f'evalDependencyNames: op = {op} num_args {num_args}')
152    if isinstance(op, tuple):
153        op, num_args = op
154    if op in '+,':
155        # The operation itself is not evaluated, since all we care about is
156        # the names
157        return evalDependencyNames(s) | evalDependencyNames(s)
158    elif op[0].isalpha():
159        return { op }
160    else:
161        raise Exception(f'invalid op: {op}')
162
163def dependencyNames(dependency):
164    """Return a set of the extension and version names in an API dependency
165       expression
166
167     - dependency - the expression"""
168
169    global exprStack
170    exprStack = []
171    results = dependencyBNF().parseString(dependency, parseAll=True)
172    # print(f'names(): stack = {exprStack}')
173    return evalDependencyNames(exprStack)
174
175if __name__ == "__main__":
176
177    def test(dependency, expected):
178        global exprStack
179        exprStack = []
180
181        try:
182            results = dependencyBNF().parseString(dependency, parseAll=True)
183            # print('test(): stack =', exprStack)
184            val = evaluate_stack(exprStack[:])
185        except ParseException as pe:
186            print(dependency, "failed parse:", str(pe))
187        except Exception as e:
188            print(dependency, "failed eval:", str(e), exprStack)
189        else:
190            print(dependency, "failed eval:", str(e), exprStack)
191            if val == expected:
192                print(f'{dependency} = {val} {results} => {exprStack}')
193            else:
194                print(f'{dependency} !!! {val} != {expected} {results} => {exprStack}')
195
196    e = 'VK_VERSION_1_1+(bar,spam)'
197    # test(e, False)
198    print(f'{e} -> {dependencyNames(e)}')
199    print('\n------------\n')
200    print(f'{e} -> {dependencyLanguage(e, False)}')
201    print('\n------------\n')
202    print(f'{e} -> {dependencyLanguage(e, True)}')
203
204    # test('true', True)
205    # test('(True)', True)
206    # test('false,false', False)
207    # test('false,true', True)
208    # test('false+true', False)
209    # test('VK_foo_bar+true', True)
210