#!/usr/bin/python3 # Copyright 2022-2023 The Khronos Group Inc. # Copyright 2003-2019 Paul McGuire # SPDX-License-Identifier: MIT # apirequirements.py - parse 'depends' expressions in API XML # Supported methods: # dependency - the expression string # # evaluateDependency(dependency, isSupported) evaluates the expression, # returning a boolean result. isSupported takes an extension or version name # string and returns a boolean. # # dependencyLanguage(dependency) returns an English string equivalent # to the expression, suitable for header file comments. # # dependencyNames(dependency) returns a set of the extension and # version names in the expression. # # dependencyMarkup(dependency) returns a string containing asciidoctor # markup for English equivalent to the expression, suitable for extension # appendices. # # All may throw a ParseException if the expression cannot be parsed or is # not completely consumed by parsing. # Supported expressions at present: # - extension names # - '+' as AND connector # - ',' as OR connector # - parenthesization for grouping # Based on https://github.com/pyparsing/pyparsing/blob/master/examples/fourFn.py from pyparsing import ( Literal, Word, Group, Forward, alphas, alphanums, Regex, ParseException, CaselessKeyword, Suppress, delimitedList, infixNotation, ) import math import operator import pyparsing as pp import re def nameMarkup(name): """Returns asciidoc markup to generate a link to an API version or extension anchor. - name - version or extension name""" # Could use ApiConventions.is_api_version_name, but that does not split # out the major/minor version numbers. match = re.search("[A-Z]+_VERSION_([0-9]+)_([0-9]+)", name) if match is not None: major = match.group(1) minor = match.group(2) version = major + '.' + minor return f'<>' else: return 'apiext:' + name exprStack = [] def push_first(toks): """Push a token on the global stack - toks - first element is the token to push""" exprStack.append(toks[0]) # An identifier (version or extension name) dependencyIdent = Word(alphanums + '_') # Infix expression for depends expressions dependencyExpr = pp.infixNotation(dependencyIdent, [ (pp.oneOf(', +'), 2, pp.opAssoc.LEFT), ]) # BNF grammar for depends expressions _bnf = None def dependencyBNF(): """ boolop :: '+' | ',' extname :: Char(alphas) atom :: extname | '(' expr ')' expr :: atom [ boolop atom ]* """ global _bnf if _bnf is None: and_, or_ = map(Literal, '+,') lpar, rpar = map(Suppress, '()') boolop = and_ | or_ expr = Forward() expr_list = delimitedList(Group(expr)) atom = ( boolop[...] + ( (dependencyIdent).setParseAction(push_first) | Group(lpar + expr + rpar) ) ) expr <<= atom + (boolop + atom).setParseAction(push_first)[...] _bnf = expr return _bnf # map operator symbols to corresponding arithmetic operations _opn = { '+': operator.and_, ',': operator.or_, } # map operator symbols to corresponding words _opname = { '+': 'and', ',': 'or', } def evaluateStack(stack, isSupported): """Evaluate an expression stack, returning a boolean result. - stack - the stack - isSupported - function taking a version or extension name string and returning True or False if that name is supported or not.""" op, num_args = stack.pop(), 0 if isinstance(op, tuple): op, num_args = op if op in '+,': # Note: operands are pushed onto the stack in reverse order op2 = evaluateStack(stack, isSupported) op1 = evaluateStack(stack, isSupported) return _opn[op](op1, op2) elif op[0].isalpha(): return isSupported(op) else: raise Exception(f'invalid op: {op}') def evaluateDependency(dependency, isSupported): """Evaluate a dependency expression, returning a boolean result. - dependency - the expression - isSupported - function taking a version or extension name string and returning True or False if that name is supported or not.""" global exprStack exprStack = [] results = dependencyBNF().parseString(dependency, parseAll=True) val = evaluateStack(exprStack[:], isSupported) return val def evalDependencyLanguage(stack, specmacros): """Evaluate an expression stack, returning an English equivalent - stack - the stack - specmacros - if True, prepare the language for spec inclusion""" op, num_args = stack.pop(), 0 if isinstance(op, tuple): op, num_args = op if op in '+,': # Could parenthesize, not needed yet rhs = evalDependencyLanguage(stack, specmacros) return evalDependencyLanguage(stack, specmacros) + f' {_opname[op]} ' + rhs elif op[0].isalpha(): # This is an extension or feature name if specmacros: return nameMarkup(op) else: return op else: raise Exception(f'invalid op: {op}') def dependencyLanguage(dependency, specmacros = False): """Return an API dependency expression translated to a form suitable for asciidoctor conditionals or header file comments. - dependency - the expression - specmacros - if False, return a string that can be used as an asciidoctor conditional. If True, return a string suitable for spec inclusion with macros and xrefs included.""" global exprStack exprStack = [] results = dependencyBNF().parseString(dependency, parseAll=True) return evalDependencyLanguage(exprStack, specmacros) def evalDependencyNames(stack): """Evaluate an expression stack, returning the set of extension and feature names used in the expression. - stack - the stack""" op, num_args = stack.pop(), 0 if isinstance(op, tuple): op, num_args = op if op in '+,': # Do not evaluate the operation. We only care about the names. return evalDependencyNames(stack) | evalDependencyNames(stack) elif op[0].isalpha(): return { op } else: raise Exception(f'invalid op: {op}') def dependencyNames(dependency): """Return a set of the extension and version names in an API dependency expression. Used when determining transitive dependencies for spec generation with specific extensions included. - dependency - the expression""" global exprStack exprStack = [] results = dependencyBNF().parseString(dependency, parseAll=True) # print(f'names(): stack = {exprStack}') return evalDependencyNames(exprStack) def markupTraverse(expr, level = 0, root = True): """Recursively process a dependency in infix form, transforming it into asciidoctor markup with expression nesting indicated by indentation level. - expr - expression to process - level - indentation level to render expression at - root - True only on initial call""" if level > 0: prefix = '{nbsp}{nbsp}' * level * 2 + ' ' else: prefix = '' str = '' for elem in expr: if isinstance(elem, pp.ParseResults): if not root: nextlevel = level + 1 else: # Do not indent the outer expression nextlevel = level str = str + markupTraverse(elem, level = nextlevel, root = False) elif elem in ('+', ','): str = str + f'{prefix}{_opname[elem]} +\n' else: str = str + f'{prefix}{nameMarkup(elem)} +\n' return str def dependencyMarkup(dependency): """Return asciidoctor markup for a human-readable equivalent of an API dependency expression, suitable for use in extension appendix metadata. - dependency - the expression""" parsed = dependencyExpr.parseString(dependency) return markupTraverse(parsed) if __name__ == "__main__": termdict = { 'VK_VERSION_1_1' : True, 'false' : False, 'true' : True, } termSupported = lambda name: name in termdict and termdict[name] def test(dependency, expected): val = False try: val = evaluateDependency(dependency, termSupported) except ParseException as pe: print(dependency, f'failed parse: {dependency}') except Exception as e: print(dependency, f'failed eval: {dependency}') if val == expected: print(f'{dependency} = {val} (as expected)') else: print(f'{dependency} ERROR: {val} != {expected}') # Verify expressions are evaluated left-to-right test('false,false+false', False) test('false,false+true', False) test('false,true+false', False) test('false,true+true', True) test('true,false+false', False) test('true,false+true', True) test('true,true+false', False) test('true,true+true', True) test('false,(false+false)', False) test('false,(false+true)', False) test('false,(true+false)', False) test('false,(true+true)', True) test('true,(false+false)', True) test('true,(false+true)', True) test('true,(true+false)', True) test('true,(true+true)', True) test('false+false,false', False) test('false+false,true', True) test('false+true,false', False) test('false+true,true', True) test('true+false,false', False) test('true+false,true', True) test('true+true,false', True) test('true+true,true', True) test('false+(false,false)', False) test('false+(false,true)', False) test('false+(true,false)', False) test('false+(true,true)', False) test('true+(false,false)', False) test('true+(false,true)', True) test('true+(true,false)', True) test('true+(true,true)', True) #test('VK_VERSION_1_1+(false,true)', True) #test('true', True) #test('(true)', True) #test('false,false', False) #test('false,true', True) #test('false+true', False) #test('true+true', True) # Check formatting for dependency in [ #'true', #'true+true+false', 'true+(true+false),(false,true)', 'true+((true+false),(false,true))', #'VK_VERSION_1_1+(true,false)', ]: print(f'expr = {dependency}\n{dependencyMarkup(dependency)}') print(f' language = {dependencyLanguage(dependency)}') print(f' names = {dependencyNames(dependency)}') print(f' value = {evaluateDependency(dependency, termSupported)}')