Skip to content

Commit

Permalink
depgen.py: preprocessor: improve expression evaluation.
Browse files Browse the repository at this point in the history
  • Loading branch information
skosukhin committed Apr 25, 2024
1 parent feece6e commit ce6ccae
Showing 1 changed file with 81 additions and 24 deletions.
105 changes: 81 additions & 24 deletions mkhelper/depgen/preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand All @@ -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

0 comments on commit ce6ccae

Please sign in to comment.