Move sympy condition simplification code into separate file
Change-Id: I3f062bf939b452bb41b7a27508a83cbf93abff8c Reviewed-by: Simon Hausmann <simon.hausmann@qt.io> Reviewed-by: Qt CMake Build Bot
This commit is contained in:
parent
447c868a5d
commit
590213e531
236
util/cmake/condition_simplifier.py
Normal file
236
util/cmake/condition_simplifier.py
Normal file
@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
#############################################################################
|
||||
##
|
||||
## Copyright (C) 2019 The Qt Company Ltd.
|
||||
## Contact: https://www.qt.io/licensing/
|
||||
##
|
||||
## This file is part of the plugins of the Qt Toolkit.
|
||||
##
|
||||
## $QT_BEGIN_LICENSE:GPL-EXCEPT$
|
||||
## Commercial License Usage
|
||||
## Licensees holding valid commercial Qt licenses may use this file in
|
||||
## accordance with the commercial license agreement provided with the
|
||||
## Software or, alternatively, in accordance with the terms contained in
|
||||
## a written agreement between you and The Qt Company. For licensing terms
|
||||
## and conditions see https://www.qt.io/terms-conditions. For further
|
||||
## information use the contact form at https://www.qt.io/contact-us.
|
||||
##
|
||||
## GNU General Public License Usage
|
||||
## Alternatively, this file may be used under the terms of the GNU
|
||||
## General Public License version 3 as published by the Free Software
|
||||
## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
||||
## included in the packaging of this file. Please review the following
|
||||
## information to ensure the GNU General Public License requirements will
|
||||
## be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
##
|
||||
## $QT_END_LICENSE$
|
||||
##
|
||||
#############################################################################
|
||||
|
||||
|
||||
import re
|
||||
|
||||
from sympy import simplify_logic, And, Or, Not, SympifyError
|
||||
|
||||
|
||||
def _iterate_expr_tree(expr, op, matches):
|
||||
assert expr.func == op
|
||||
keepers = ()
|
||||
for arg in expr.args:
|
||||
if arg in matches:
|
||||
matches = tuple(x for x in matches if x != arg)
|
||||
elif arg == op:
|
||||
(matches, extra_keepers) = _iterate_expr_tree(arg, op, matches)
|
||||
keepers = (*keepers, *extra_keepers)
|
||||
else:
|
||||
keepers = (*keepers, arg)
|
||||
return matches, keepers
|
||||
|
||||
|
||||
def _simplify_expressions(expr, op, matches, replacement):
|
||||
for arg in expr.args:
|
||||
expr = expr.subs(arg, _simplify_expressions(arg, op, matches, replacement))
|
||||
|
||||
if expr.func == op:
|
||||
(to_match, keepers) = tuple(_iterate_expr_tree(expr, op, matches))
|
||||
if len(to_match) == 0:
|
||||
# build expression with keepers and replacement:
|
||||
if keepers:
|
||||
start = replacement
|
||||
current_expr = None
|
||||
last_expr = keepers[-1]
|
||||
for repl_arg in keepers[:-1]:
|
||||
current_expr = op(start, repl_arg)
|
||||
start = current_expr
|
||||
top_expr = op(start, last_expr)
|
||||
else:
|
||||
top_expr = replacement
|
||||
|
||||
expr = expr.subs(expr, top_expr)
|
||||
|
||||
return expr
|
||||
|
||||
|
||||
def _simplify_flavors_in_condition(base: str, flavors, expr):
|
||||
""" Simplify conditions based on the knowledge of which flavors
|
||||
belong to which OS. """
|
||||
base_expr = simplify_logic(base)
|
||||
false_expr = simplify_logic("false")
|
||||
for flavor in flavors:
|
||||
flavor_expr = simplify_logic(flavor)
|
||||
expr = _simplify_expressions(expr, And, (base_expr, flavor_expr), flavor_expr)
|
||||
expr = _simplify_expressions(expr, Or, (base_expr, flavor_expr), base_expr)
|
||||
expr = _simplify_expressions(expr, And, (Not(base_expr), flavor_expr), false_expr)
|
||||
return expr
|
||||
|
||||
|
||||
def _simplify_os_families(expr, family_members, other_family_members):
|
||||
for family in family_members:
|
||||
for other in other_family_members:
|
||||
if other in family_members:
|
||||
continue # skip those in the sub-family
|
||||
|
||||
f_expr = simplify_logic(family)
|
||||
o_expr = simplify_logic(other)
|
||||
|
||||
expr = _simplify_expressions(expr, And, (f_expr, Not(o_expr)), f_expr)
|
||||
expr = _simplify_expressions(expr, And, (Not(f_expr), o_expr), o_expr)
|
||||
expr = _simplify_expressions(expr, And, (f_expr, o_expr), simplify_logic("false"))
|
||||
return expr
|
||||
|
||||
|
||||
def _recursive_simplify(expr):
|
||||
""" Simplify the expression as much as possible based on
|
||||
domain knowledge. """
|
||||
input_expr = expr
|
||||
|
||||
# Simplify even further, based on domain knowledge:
|
||||
# windowses = ('WIN32', 'WINRT')
|
||||
apples = ("APPLE_OSX", "APPLE_UIKIT", "APPLE_IOS", "APPLE_TVOS", "APPLE_WATCHOS")
|
||||
bsds = ("FREEBSD", "OPENBSD", "NETBSD")
|
||||
androids = ("ANDROID", "ANDROID_EMBEDDED")
|
||||
unixes = (
|
||||
"APPLE",
|
||||
*apples,
|
||||
"BSD",
|
||||
*bsds,
|
||||
"LINUX",
|
||||
*androids,
|
||||
"HAIKU",
|
||||
"INTEGRITY",
|
||||
"VXWORKS",
|
||||
"QNX",
|
||||
"WASM",
|
||||
)
|
||||
|
||||
unix_expr = simplify_logic("UNIX")
|
||||
win_expr = simplify_logic("WIN32")
|
||||
false_expr = simplify_logic("false")
|
||||
true_expr = simplify_logic("true")
|
||||
|
||||
expr = expr.subs(Not(unix_expr), win_expr) # NOT UNIX -> WIN32
|
||||
expr = expr.subs(Not(win_expr), unix_expr) # NOT WIN32 -> UNIX
|
||||
|
||||
# UNIX [OR foo ]OR WIN32 -> ON [OR foo]
|
||||
expr = _simplify_expressions(expr, Or, (unix_expr, win_expr), true_expr)
|
||||
# UNIX [AND foo ]AND WIN32 -> OFF [AND foo]
|
||||
expr = _simplify_expressions(expr, And, (unix_expr, win_expr), false_expr)
|
||||
|
||||
expr = _simplify_flavors_in_condition("WIN32", ("WINRT",), expr)
|
||||
expr = _simplify_flavors_in_condition("APPLE", apples, expr)
|
||||
expr = _simplify_flavors_in_condition("BSD", bsds, expr)
|
||||
expr = _simplify_flavors_in_condition("UNIX", unixes, expr)
|
||||
expr = _simplify_flavors_in_condition("ANDROID", ("ANDROID_EMBEDDED",), expr)
|
||||
|
||||
# Simplify families of OSes against other families:
|
||||
expr = _simplify_os_families(expr, ("WIN32", "WINRT"), unixes)
|
||||
expr = _simplify_os_families(expr, androids, unixes)
|
||||
expr = _simplify_os_families(expr, ("BSD", *bsds), unixes)
|
||||
|
||||
for family in ("HAIKU", "QNX", "INTEGRITY", "LINUX", "VXWORKS"):
|
||||
expr = _simplify_os_families(expr, (family,), unixes)
|
||||
|
||||
# Now simplify further:
|
||||
expr = simplify_logic(expr)
|
||||
|
||||
while expr != input_expr:
|
||||
input_expr = expr
|
||||
expr = _recursive_simplify(expr)
|
||||
|
||||
return expr
|
||||
|
||||
|
||||
def simplify_condition(condition: str) -> str:
|
||||
input_condition = condition.strip()
|
||||
|
||||
# Map to sympy syntax:
|
||||
condition = " " + input_condition + " "
|
||||
condition = condition.replace("(", " ( ")
|
||||
condition = condition.replace(")", " ) ")
|
||||
|
||||
tmp = ""
|
||||
while tmp != condition:
|
||||
tmp = condition
|
||||
|
||||
condition = condition.replace(" NOT ", " ~ ")
|
||||
condition = condition.replace(" AND ", " & ")
|
||||
condition = condition.replace(" OR ", " | ")
|
||||
condition = condition.replace(" ON ", " true ")
|
||||
condition = condition.replace(" OFF ", " false ")
|
||||
# Replace dashes with a token
|
||||
condition = condition.replace("-", "_dash_")
|
||||
|
||||
# SymPy chokes on expressions that contain two tokens one next to
|
||||
# the other delimited by a space, which are not an operation.
|
||||
# So a CMake condition like "TARGET Foo::Bar" fails the whole
|
||||
# expression simplifying process.
|
||||
# Turn these conditions into a single token so that SymPy can parse
|
||||
# the expression, and thus simplify it.
|
||||
# Do this by replacing and keeping a map of conditions to single
|
||||
# token symbols.
|
||||
# Support both target names without double colons, and with double
|
||||
# colons.
|
||||
pattern = re.compile(r"(TARGET [a-zA-Z]+(?:::[a-zA-Z]+)?)")
|
||||
target_symbol_mapping = {}
|
||||
all_target_conditions = re.findall(pattern, condition)
|
||||
for target_condition in all_target_conditions:
|
||||
# Replace spaces and colons with underscores.
|
||||
target_condition_symbol_name = re.sub("[ :]", "_", target_condition)
|
||||
target_symbol_mapping[target_condition_symbol_name] = target_condition
|
||||
condition = re.sub(target_condition, target_condition_symbol_name, condition)
|
||||
|
||||
# Do similar token mapping for comparison operators.
|
||||
pattern = re.compile(r"([a-zA-Z_0-9]+ (?:STRLESS|STREQUAL|STRGREATER) [a-zA-Z_0-9]+)")
|
||||
comparison_symbol_mapping = {}
|
||||
all_comparisons = re.findall(pattern, condition)
|
||||
for comparison in all_comparisons:
|
||||
# Replace spaces and colons with underscores.
|
||||
comparison_symbol_name = re.sub("[ ]", "_", comparison)
|
||||
comparison_symbol_mapping[comparison_symbol_name] = comparison
|
||||
condition = re.sub(comparison, comparison_symbol_name, condition)
|
||||
|
||||
try:
|
||||
# Generate and simplify condition using sympy:
|
||||
condition_expr = simplify_logic(condition)
|
||||
condition = str(_recursive_simplify(condition_expr))
|
||||
|
||||
# Restore the target conditions.
|
||||
for symbol_name in target_symbol_mapping:
|
||||
condition = re.sub(symbol_name, target_symbol_mapping[symbol_name], condition)
|
||||
|
||||
# Restore comparisons.
|
||||
for comparison in comparison_symbol_mapping:
|
||||
condition = re.sub(comparison, comparison_symbol_mapping[comparison], condition)
|
||||
|
||||
# Map back to CMake syntax:
|
||||
condition = condition.replace("~", "NOT ")
|
||||
condition = condition.replace("&", "AND")
|
||||
condition = condition.replace("|", "OR")
|
||||
condition = condition.replace("True", "ON")
|
||||
condition = condition.replace("False", "OFF")
|
||||
condition = condition.replace("_dash_", "-")
|
||||
except (SympifyError, TypeError, AttributeError):
|
||||
# sympy did not like our input, so leave this condition alone:
|
||||
condition = input_condition
|
||||
|
||||
return condition or "ON"
|
@ -39,6 +39,8 @@ import io
|
||||
import glob
|
||||
import collections
|
||||
|
||||
from condition_simplifier import simplify_condition
|
||||
|
||||
try:
|
||||
collectionsAbc = collections.abc
|
||||
except AttributeError:
|
||||
@ -53,8 +55,6 @@ from textwrap import indent as textwrap_indent
|
||||
from itertools import chain
|
||||
from functools import lru_cache
|
||||
from shutil import copyfile
|
||||
from sympy.logic import simplify_logic, And, Or, Not
|
||||
from sympy.core.sympify import SympifyError
|
||||
from typing import (
|
||||
List,
|
||||
Optional,
|
||||
@ -2054,209 +2054,6 @@ def write_ignored_keys(scope: Scope, indent: str) -> str:
|
||||
return result
|
||||
|
||||
|
||||
def _iterate_expr_tree(expr, op, matches):
|
||||
assert expr.func == op
|
||||
keepers = ()
|
||||
for arg in expr.args:
|
||||
if arg in matches:
|
||||
matches = tuple(x for x in matches if x != arg)
|
||||
elif arg == op:
|
||||
(matches, extra_keepers) = _iterate_expr_tree(arg, op, matches)
|
||||
keepers = (*keepers, *extra_keepers)
|
||||
else:
|
||||
keepers = (*keepers, arg)
|
||||
return matches, keepers
|
||||
|
||||
|
||||
def _simplify_expressions(expr, op, matches, replacement):
|
||||
for arg in expr.args:
|
||||
expr = expr.subs(arg, _simplify_expressions(arg, op, matches, replacement))
|
||||
|
||||
if expr.func == op:
|
||||
(to_match, keepers) = tuple(_iterate_expr_tree(expr, op, matches))
|
||||
if len(to_match) == 0:
|
||||
# build expression with keepers and replacement:
|
||||
if keepers:
|
||||
start = replacement
|
||||
current_expr = None
|
||||
last_expr = keepers[-1]
|
||||
for repl_arg in keepers[:-1]:
|
||||
current_expr = op(start, repl_arg)
|
||||
start = current_expr
|
||||
top_expr = op(start, last_expr)
|
||||
else:
|
||||
top_expr = replacement
|
||||
|
||||
expr = expr.subs(expr, top_expr)
|
||||
|
||||
return expr
|
||||
|
||||
|
||||
def _simplify_flavors_in_condition(base: str, flavors, expr):
|
||||
""" Simplify conditions based on the knownledge of which flavors
|
||||
belong to which OS. """
|
||||
base_expr = simplify_logic(base)
|
||||
false_expr = simplify_logic("false")
|
||||
for flavor in flavors:
|
||||
flavor_expr = simplify_logic(flavor)
|
||||
expr = _simplify_expressions(expr, And, (base_expr, flavor_expr), flavor_expr)
|
||||
expr = _simplify_expressions(expr, Or, (base_expr, flavor_expr), base_expr)
|
||||
expr = _simplify_expressions(expr, And, (Not(base_expr), flavor_expr), false_expr)
|
||||
return expr
|
||||
|
||||
|
||||
def _simplify_os_families(expr, family_members, other_family_members):
|
||||
for family in family_members:
|
||||
for other in other_family_members:
|
||||
if other in family_members:
|
||||
continue # skip those in the sub-family
|
||||
|
||||
f_expr = simplify_logic(family)
|
||||
o_expr = simplify_logic(other)
|
||||
|
||||
expr = _simplify_expressions(expr, And, (f_expr, Not(o_expr)), f_expr)
|
||||
expr = _simplify_expressions(expr, And, (Not(f_expr), o_expr), o_expr)
|
||||
expr = _simplify_expressions(expr, And, (f_expr, o_expr), simplify_logic("false"))
|
||||
return expr
|
||||
|
||||
|
||||
def _recursive_simplify(expr):
|
||||
""" Simplify the expression as much as possible based on
|
||||
domain knowledge. """
|
||||
input_expr = expr
|
||||
|
||||
# Simplify even further, based on domain knowledge:
|
||||
# windowses = ('WIN32', 'WINRT')
|
||||
apples = ("APPLE_OSX", "APPLE_UIKIT", "APPLE_IOS", "APPLE_TVOS", "APPLE_WATCHOS")
|
||||
bsds = ("FREEBSD", "OPENBSD", "NETBSD")
|
||||
androids = ("ANDROID", "ANDROID_EMBEDDED")
|
||||
unixes = (
|
||||
"APPLE",
|
||||
*apples,
|
||||
"BSD",
|
||||
*bsds,
|
||||
"LINUX",
|
||||
*androids,
|
||||
"HAIKU",
|
||||
"INTEGRITY",
|
||||
"VXWORKS",
|
||||
"QNX",
|
||||
"WASM",
|
||||
)
|
||||
|
||||
unix_expr = simplify_logic("UNIX")
|
||||
win_expr = simplify_logic("WIN32")
|
||||
false_expr = simplify_logic("false")
|
||||
true_expr = simplify_logic("true")
|
||||
|
||||
expr = expr.subs(Not(unix_expr), win_expr) # NOT UNIX -> WIN32
|
||||
expr = expr.subs(Not(win_expr), unix_expr) # NOT WIN32 -> UNIX
|
||||
|
||||
# UNIX [OR foo ]OR WIN32 -> ON [OR foo]
|
||||
expr = _simplify_expressions(expr, Or, (unix_expr, win_expr), true_expr)
|
||||
# UNIX [AND foo ]AND WIN32 -> OFF [AND foo]
|
||||
expr = _simplify_expressions(expr, And, (unix_expr, win_expr), false_expr)
|
||||
|
||||
expr = _simplify_flavors_in_condition("WIN32", ("WINRT",), expr)
|
||||
expr = _simplify_flavors_in_condition("APPLE", apples, expr)
|
||||
expr = _simplify_flavors_in_condition("BSD", bsds, expr)
|
||||
expr = _simplify_flavors_in_condition("UNIX", unixes, expr)
|
||||
expr = _simplify_flavors_in_condition("ANDROID", ("ANDROID_EMBEDDED",), expr)
|
||||
|
||||
# Simplify families of OSes against other families:
|
||||
expr = _simplify_os_families(expr, ("WIN32", "WINRT"), unixes)
|
||||
expr = _simplify_os_families(expr, androids, unixes)
|
||||
expr = _simplify_os_families(expr, ("BSD", *bsds), unixes)
|
||||
|
||||
for family in ("HAIKU", "QNX", "INTEGRITY", "LINUX", "VXWORKS"):
|
||||
expr = _simplify_os_families(expr, (family,), unixes)
|
||||
|
||||
# Now simplify further:
|
||||
expr = simplify_logic(expr)
|
||||
|
||||
while expr != input_expr:
|
||||
input_expr = expr
|
||||
expr = _recursive_simplify(expr)
|
||||
|
||||
return expr
|
||||
|
||||
|
||||
def simplify_condition(condition: str) -> str:
|
||||
input_condition = condition.strip()
|
||||
|
||||
# Map to sympy syntax:
|
||||
condition = " " + input_condition + " "
|
||||
condition = condition.replace("(", " ( ")
|
||||
condition = condition.replace(")", " ) ")
|
||||
|
||||
tmp = ""
|
||||
while tmp != condition:
|
||||
tmp = condition
|
||||
|
||||
condition = condition.replace(" NOT ", " ~ ")
|
||||
condition = condition.replace(" AND ", " & ")
|
||||
condition = condition.replace(" OR ", " | ")
|
||||
condition = condition.replace(" ON ", " true ")
|
||||
condition = condition.replace(" OFF ", " false ")
|
||||
# Replace dashes with a token
|
||||
condition = condition.replace("-", "_dash_")
|
||||
|
||||
# SymPy chokes on expressions that contain two tokens one next to
|
||||
# the other delimited by a space, which are not an operation.
|
||||
# So a CMake condition like "TARGET Foo::Bar" fails the whole
|
||||
# expression simplifying process.
|
||||
# Turn these conditions into a single token so that SymPy can parse
|
||||
# the expression, and thus simplify it.
|
||||
# Do this by replacing and keeping a map of conditions to single
|
||||
# token symbols.
|
||||
# Support both target names without double colons, and with double
|
||||
# colons.
|
||||
pattern = re.compile(r"(TARGET [a-zA-Z]+(?:::[a-zA-Z]+)?)")
|
||||
target_symbol_mapping = {}
|
||||
all_target_conditions = re.findall(pattern, condition)
|
||||
for target_condition in all_target_conditions:
|
||||
# Replace spaces and colons with underscores.
|
||||
target_condition_symbol_name = re.sub("[ :]", "_", target_condition)
|
||||
target_symbol_mapping[target_condition_symbol_name] = target_condition
|
||||
condition = re.sub(target_condition, target_condition_symbol_name, condition)
|
||||
|
||||
# Do similar token mapping for comparison operators.
|
||||
pattern = re.compile(r"([a-zA-Z_0-9]+ (?:STRLESS|STREQUAL|STRGREATER) [a-zA-Z_0-9]+)")
|
||||
comparison_symbol_mapping = {}
|
||||
all_comparisons = re.findall(pattern, condition)
|
||||
for comparison in all_comparisons:
|
||||
# Replace spaces and colons with underscores.
|
||||
comparison_symbol_name = re.sub("[ ]", "_", comparison)
|
||||
comparison_symbol_mapping[comparison_symbol_name] = comparison
|
||||
condition = re.sub(comparison, comparison_symbol_name, condition)
|
||||
|
||||
try:
|
||||
# Generate and simplify condition using sympy:
|
||||
condition_expr = simplify_logic(condition)
|
||||
condition = str(_recursive_simplify(condition_expr))
|
||||
|
||||
# Restore the target conditions.
|
||||
for symbol_name in target_symbol_mapping:
|
||||
condition = re.sub(symbol_name, target_symbol_mapping[symbol_name], condition)
|
||||
|
||||
# Restore comparisons.
|
||||
for comparison in comparison_symbol_mapping:
|
||||
condition = re.sub(comparison, comparison_symbol_mapping[comparison], condition)
|
||||
|
||||
# Map back to CMake syntax:
|
||||
condition = condition.replace("~", "NOT ")
|
||||
condition = condition.replace("&", "AND")
|
||||
condition = condition.replace("|", "OR")
|
||||
condition = condition.replace("True", "ON")
|
||||
condition = condition.replace("False", "OFF")
|
||||
condition = condition.replace("_dash_", "-")
|
||||
except (SympifyError, TypeError, AttributeError):
|
||||
# sympy did not like our input, so leave this condition alone:
|
||||
condition = input_condition
|
||||
|
||||
return condition or "ON"
|
||||
|
||||
|
||||
def recursive_evaluate_scope(
|
||||
scope: Scope, parent_condition: str = "", previous_condition: str = ""
|
||||
) -> str:
|
||||
|
@ -27,7 +27,7 @@
|
||||
##
|
||||
#############################################################################
|
||||
|
||||
from pro2cmake import simplify_condition
|
||||
from condition_simplifier import simplify_condition
|
||||
|
||||
|
||||
def validate_simplify(input: str, expected: str) -> None:
|
||||
|
Loading…
Reference in New Issue
Block a user