From ce6ccae1546ecdca2b974f224e8a69c9989eb0e6 Mon Sep 17 00:00:00 2001 From: Sergey Kosukhin Date: Thu, 25 Apr 2024 19:15:33 +0200 Subject: [PATCH] depgen.py: preprocessor: improve expression evaluation. --- mkhelper/depgen/preprocessor.py | 105 ++++++++++++++++++++++++-------- 1 file changed, 81 insertions(+), 24 deletions(-) diff --git a/mkhelper/depgen/preprocessor.py b/mkhelper/depgen/preprocessor.py index d369990..de0debe 100644 --- a/mkhelper/depgen/preprocessor.py +++ b/mkhelper/depgen/preprocessor.py @@ -337,11 +337,23 @@ class MacroHandler(object): r"(defined\s*(\(\s*)?([a-zA-Z_]\w*)(?(2)\s*\)))" ) + # an invalid identifier (used as a marker) + _invalid_identifier = " 0choke_me0 " + + # a valid name for a variable + _valid_python_name = "choke_me" + # matches object-like and function-like macro identifiers _re_identifier = re.compile( - r"(([a-zA-Z_]\w*)(\s*\(\s*(\w+(?:\s*,\s*\w+)*)?\s*\))?)" + "({0}|{1})".format( + _invalid_identifier, + r"([a-zA-Z_]\w*)\s*(\(\s*(?:\w+(?:\s*,\s*\w+)*\s*)?\))?", + ) ) + # matches empty identifier arguments + _re_empty_args = re.compile(r"^\(\s*\)$") + __slots__ = ["_macros"] def __init__(self, predefined_macros=None): @@ -358,7 +370,9 @@ def eval_defined(self, macro_name, negate=False): return 1 if bool(macro_name in self._macros) ^ negate else -1 def eval_expression(self, expr): - prev_expr = expr + prev_expr = None + + invalid_is_injected = False while 1: # replace calls to function "defined" defined_calls = re.findall(MacroHandler._re_defined_call, expr) @@ -369,45 +383,88 @@ def eval_expression(self, expr): identifiers = re.findall(MacroHandler._re_identifier, expr) - for identifier in identifiers: - if identifier[1] == "defined": + for identifier, identifier_name, identifier_args in identifiers: + if identifier == MacroHandler._invalid_identifier: + continue + + if identifier_name == "defined": + # this should never happen return 0 - macro = self._macros.get(identifier[1], None) - if identifier[2]: - # potential call to a function + macro = self._macros.get(identifier_name, None) + + if identifier_args: + # expansion of a function-like identifier + if macro is None: - # call to undefined function - return 0 - elif macro[0] is not None: - # we can't evaluate function-like macros + # the respective macro is not defined, which normally + # results in a preprocessor error return 0 + + if macro[0] is None: + # the respective macro is an object-like one: replace + # the name of the function-like identifier with the body + # (value) of the macro + identifier_repl = macro[1] + identifier_args + elif MacroHandler._re_empty_args.match(identifier_args): + # the argument list of the function-like identifier is + # empty: replace the identifier with the body of the + # function-like macro + identifier_repl = macro[1] else: - # identifier is defined as object-like macro - expr = expr.replace( - identifier[0], macro[1] + identifier[2] - ) + # we cannot expand function-like macros but we might not + # have to (e.g. when the first operand of the logical + # "and" is evaluated to False and the identifier belongs + # to the second operand): replace the identifier with + # the marker, which will not be replaced during the next + # iterations + identifier_repl = MacroHandler._invalid_identifier + invalid_is_injected = True else: - # no function call + # expansion of an object-like identifier + if macro is None or macro[0] is not None: - # macro is not defined or - # defined as function-like macro - expr = expr.replace(identifier[0], "0") + # the respective macro is not defined or defined as a + # function-like one: the identifier evaluates to False + identifier_repl = "0" else: - # identifier is defined as object-like macro - expr = expr.replace(identifier[0], macro[1]) + # the respective macro is defined as an object-like one: + # replace the identifier with the body (value) of the + # macro + identifier_repl = macro[1] + + expr = expr.replace(identifier, identifier_repl) if prev_expr == expr: break - else: - prev_expr = expr + + prev_expr = expr expr = expr.replace("||", " or ") expr = expr.replace("&&", " and ") expr = expr.replace("!", "not ") + if invalid_is_injected: + # replace the invalid identifier with a valid Python name + expr = expr.replace( + MacroHandler._invalid_identifier, + MacroHandler._valid_python_name, + ) + try: - result = bool(eval(expr, {})) + eval_globals = {} + if invalid_is_injected: + # if the result does not depend on the values of the + # function-like identifiers, return it + eval_locals = {MacroHandler._valid_python_name: True} + possible_result = eval(expr, eval_globals, eval_locals) + eval_locals[MacroHandler._valid_python_name] = False + if possible_result != eval(expr, eval_globals, eval_locals): + return 0 + else: + result = possible_result + else: + result = eval(expr, eval_globals) return 1 if result else -1 except Exception: return 0