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