Add parser and evaluator for rust cfg expressions

This commit is contained in:
Zbigniew Jędrzejewski-Szmek 2022-02-28 00:01:57 +01:00
parent 3fdc1b24cb
commit 61e7b5fc7a
2 changed files with 173 additions and 0 deletions

113
rust2rpm/cfg.py Normal file
View file

@ -0,0 +1,113 @@
import ast
import ctypes
import functools
import platform
import sys
import pyparsing as pp
pp.ParserElement.enable_packrat()
# ConfigurationPredicate :
# ConfigurationOption
# | ConfigurationAll
# | ConfigurationAny
# | ConfigurationNot
# ConfigurationOption :
# IDENTIFIER (= (STRING_LITERAL | RAW_STRING_LITERAL))?
# ConfigurationAll
# all ( ConfigurationPredicateList? )
# ConfigurationAny
# any ( ConfigurationPredicateList? )
# ConfigurationNot
# not ( ConfigurationPredicate )
# ConfigurationPredicateList
# ConfigurationPredicate (, ConfigurationPredicate)* ,?
# cfg(target_os = "macos")
# cfg(any(foo, bar))
# cfg(all(unix, target_pointer_width = "32"))
# cfg(not(foo))
def _call(word, arg):
return pp.Group(pp.Literal(word) + pp.Suppress('(') + arg + pp.Suppress(')'), aslist=True)
@functools.cache
def cfg_grammar():
pred = pp.Forward()
ident = pp.Word(pp.alphas + '_', pp.alphanums + '_')
option = pp.Group(ident + pp.Optional(pp.Suppress('=') + pp.quotedString), aslist=True)
not_ = _call('not', pred)
# pp.pyparsing_common.comma_separated_list?
# any_ = _call('any', pp.pyparsing_common.comma_separated_list(pred))
# all_ = _call('all', pp.pyparsing_common.comma_separated_list(pred))
# all_ = _call('all', pp.delimited_list(pred))
any_ = _call('any', pred + pp.ZeroOrMore(pp.Suppress(',') + pred))
all_ = _call('all', pred + pp.ZeroOrMore(pp.Suppress(',') + pred))
pred <<= not_ | any_ | all_ | option
grammar = _call('cfg', pred)
return grammar
@functools.cache
def evaluate_variable(name):
# print(f'evaluate_variable: {expr}')
match name:
case 'target_arch':
return platform.machine()
case 'target_os':
return 'linux'
case 'target_family':
return 'unix'
case 'unix':
return evaluate_variable('target_family') == 'unix'
case 'windows':
return evaluate_variable('target_family') == 'windows'
case 'target_env':
# Key-value option set with further disambiguating information about the
# target platform with information about the ABI or libc used
return ...
case 'target_endian':
return sys.byteorder
case 'target_pointer_width':
return str(ctypes.sizeof(ctypes.c_voidp) * 8)
case 'target_vendor':
return 'unknown'
case _:
print(f'Unknown variable {name}, assuming False')
return False
def evaluate(expr, nested=False):
# print(f'evaluate: {expr}')
match expr:
case ['cfg', expr] if not nested:
return evaluate(expr, True)
case ['not', expr] if nested:
return not evaluate(expr, True)
case ['all', *args] if nested:
return all(evaluate(arg, True) for arg in args)
case ['any', *args] if nested:
return any(evaluate(arg, True) for arg in args)
case [variable, value] if nested:
v = ast.literal_eval(value)
x = evaluate_variable(variable)
return x == v
case [variable] if nested:
x = evaluate_variable(variable)
return x
case _:
raise ValueError

View file

@ -0,0 +1,60 @@
import pytest
from rust2rpm import cfg
def test_pyparsing_run_tests():
g = cfg.cfg_grammar()
g.run_tests("""\
cfg(target_os = "macos")
cfg(any(foo, bar))
cfg(all(unix, target_pointer_width = "32"))
cfg(not(foo))
""")
@pytest.mark.parametrize('expr, expected', [
('cfg(target_os = "macos")',
False),
('cfg(any(foo, bar))',
False),
('cfg(all(unix, target_pointer_width = "16"))',
False),
('cfg(not(foo))',
True),
('cfg(unix)',
True),
('cfg(not(unix))',
False),
('cfg(windows)',
False),
('cfg(linux)', # not defined
False),
('cfg(not(windows))',
True),
('cfg(any(unix, windows))',
True),
('cfg(any(windows, unix))',
True),
('cfg(any(windows, windows, windows))',
False),
('cfg(target_os = "linux")',
True),
('cfg(any(target_os = "linux"))',
True),
('cfg(all(target_os = "linux"))',
True),
('cfg(any(target_os = "linux", target_os = "macos"))',
True),
('cfg(any(target_pointer_width = "16", target_pointer_width = "32", target_pointer_width = "64"))',
True),
('cfg(all(target_pointer_width = "16", target_pointer_width = "32", target_pointer_width = "64"))',
False),
])
def test_expressions(expr, expected):
parsed = cfg.cfg_grammar().parse_string(expr)
value = cfg.evaluate(parsed[0])
assert value == expected