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