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