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:
Alexandru Croitor 2019-09-30 18:11:15 +02:00 committed by Tobias Hunger
parent 447c868a5d
commit 590213e531
3 changed files with 239 additions and 206 deletions

View 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"

View File

@ -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:

View File

@ -27,7 +27,7 @@
##
#############################################################################
from pro2cmake import simplify_condition
from condition_simplifier import simplify_condition
def validate_simplify(input: str, expected: str) -> None: