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