xref: /aosp_15_r20/build/bazel/utils/config_setting_boolean_algebra.bzl (revision 7594170e27e0732bc44b93d1440d87a54b6ffe7c)
1# Copyright (C) 2023 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15load("@bazel_skylib//lib:selects.bzl", "selects")
16
17def _not(name, config_setting):
18    native.alias(
19        name = name,
20        actual = select({
21            config_setting: "//build/bazel/utils:always_off_config_setting",
22            "//conditions:default": "//build/bazel/utils:always_on_config_setting",
23        }),
24    )
25
26def _or(name, match_any):
27    if not match_any:
28        native.alias(
29            name = name,
30            actual = "//build/bazel/utils:always_off_config_setting",
31        )
32    else:
33        selects.config_setting_group(
34            name = name,
35            match_any = match_any,
36        )
37
38def _and(name, match_all):
39    if not match_all:
40        native.alias(
41            name = name,
42            actual = "//build/bazel/utils:always_on_config_setting",
43        )
44    else:
45        selects.config_setting_group(
46            name = name,
47            match_all = match_all,
48        )
49
50def config_setting_boolean_algebra(*, name, expr):
51    """
52    Computes the given boolean expression of config settings.
53
54    The format of the expr argument is a dictionary with a single key/value pair.
55    The key can be AND, OR, or NOT. The value or AND/OR keys must be a list
56    of strings or more expression dictionaries, where the strings are labels of config settings.
57    The value of NOT keys must be a string or an expression dictionary.
58
59    The result will be a new config setting which is the evaluation of the expression.
60
61    A bunch of internal config settings will also be created, but they should be treated
62    as an implementation detail and not relied on. They could change in future updates to
63    this method.
64
65    Example:
66    config_setting_boolean_algebra(
67        name = "my_config_setting",
68        expr = {"AND": [
69            ":config_setting_1",
70            {"NOT": ":config_setting_2"},
71            {"OR": [
72                ":config_setting_3",
73                {"NOT": "config_setting_4"},
74            ]}
75        ]}
76    )
77    """
78
79    # The implementation of this function is modeled after a recursive function,
80    # but due to the special nature of the problem it's simplified quite a bit from
81    # a full recursion-to-iteration algorithm. (no need for return values, no need to return
82    # to prior stack frames once we start executing a new one)
83    stack = [struct(
84        expr = expr,
85        name = name,
86    )]
87
88    # Starlark doesn't support infinite loops, so just make a large loop
89    for _ in range(1000):
90        if not stack:
91            break
92
93        frame = stack.pop()
94        name = frame.name
95        expr = frame.expr
96        expr_type = type(expr)
97        if expr_type == "string":
98            native.alias(
99                name = name,
100                actual = expr,
101            )
102            continue
103        elif expr_type == "dict":
104            if len(expr) != 1:
105                fail("Dictionaries should have exactly 1 key/value")
106            op, operands = expr.items()[0]
107            if op == "NOT":
108                # traditionally this would come after the recursive call, but because it's a rule
109                # definition it doesn't matter if name + ".0" exists yet or not. Having the
110                # recursive call come last makes the non-recursive version easier to implement
111                _not(name, name + ".0")
112                stack.append(struct(
113                    name = name + ".0",
114                    expr = operands,
115                ))
116                continue
117
118            if type(operands) != "list":
119                fail("Operand to AND/OR must be a list, got %s" % type(operands))
120
121            operand_names = [name + "." + str(i) for i, elem in enumerate(operands)]
122
123            if op == "AND":
124                _and(name, operand_names)
125            elif op == "OR":
126                _or(name, operand_names)
127            else:
128                fail("Operator must be AND, OR, or NOT, got %s" % op)
129
130            for elem_name, elem in zip(operand_names, operands):
131                # because we don't need a return value from these recursive calls,
132                # we can queue them all up at once without returning to the current stack frame.
133                stack.append(struct(
134                    name = elem_name,
135                    expr = elem,
136                ))
137        else:
138            fail("Expression must be string or dict, got %s" % expr_type)
139    if stack:
140        fail("Recursion took too many iterations!")
141