Skip to content

Instantly share code, notes, and snippets.

@rrbutani
Last active June 3, 2024 09:40
Show Gist options
  • Save rrbutani/cc3fdc5bae38b5cd18733fb6bef15fdd to your computer and use it in GitHub Desktop.
Save rrbutani/cc3fdc5bae38b5cd18733fb6bef15fdd to your computer and use it in GitHub Desktop.

bazel query 'filter("^_version_is_at_.*$", ...) + true + false' --output=graph

flowchart TD
  classDef true stroke:#1F0
  classDef false stroke:#F00
  classDef m stroke:#F90
  classDef l stroke:#C0F

  _version_is_at_most_17__14:::m
  _version_is_at_most_17__14 -->|//conditions:default| _version_is_at_most_16__13
  _version_is_at_most_17__14 -->|//:_version_is_17__14| true
  _version_is_at_least_1__0.1:::l
  _version_is_at_least_1__0.1 -->|//conditions:default| _version_is_at_least_2__1
  _version_is_at_least_1__0.1 -->|//:_version_is_1__0.1| true
  _version_is_at_most_18__latest:::m
  _version_is_at_most_18__latest --> true
  _version_is_at_least_0__0:::l
  _version_is_at_least_0__0 --> true
  _version_is_at_most_16__13:::m
  _version_is_at_most_16__13 -->|//conditions:default| _version_is_at_most_15__12
  _version_is_at_most_16__13 -->|//:_version_is_16__13| true
  _version_is_at_least_2__1:::l
  _version_is_at_least_2__1 -->|//conditions:default| _version_is_at_least_3__2
  _version_is_at_least_2__1 -->|//:_version_is_2__1| true
  _version_is_at_least_3__2:::l
  _version_is_at_least_3__2 -->|//conditions:default| _version_is_at_least_4__3
  _version_is_at_least_3__2 -->|//:_version_is_3__2| true
  _version_is_at_least_4__3:::l
  _version_is_at_least_4__3 -->|//conditions:default| _version_is_at_least_5__3.1
  _version_is_at_least_4__3 -->|//:_version_is_4__3| true
  _version_is_at_least_5__3.1:::l
  _version_is_at_least_5__3.1 -->|//conditions:default| _version_is_at_least_6__3.1.1
  _version_is_at_least_5__3.1 -->|//:_version_is_5__3.1| true
  _version_is_at_least_6__3.1.1:::l
  _version_is_at_least_6__3.1.1 -->|//conditions:default| _version_is_at_least_7__4
  _version_is_at_least_6__3.1.1 -->|//:_version_is_6__3.1.1| true
  _version_is_at_least_7__4:::l
  _version_is_at_least_7__4 -->|//conditions:default| _version_is_at_least_8__5
  _version_is_at_least_7__4 -->|//:_version_is_7__4| true
  _version_is_at_least_8__5:::l
  _version_is_at_least_8__5 -->|//conditions:default| _version_is_at_least_9__6
  _version_is_at_least_8__5 -->|//:_version_is_8__5| true
  _version_is_at_least_9__6:::l
  _version_is_at_least_9__6 -->|//conditions:default| _version_is_at_least_10__7
  _version_is_at_least_9__6 -->|//:_version_is_9__6| true
  _version_is_at_least_10__7:::l
  _version_is_at_least_10__7 -->|//conditions:default| _version_is_at_least_11__8
  _version_is_at_least_10__7 -->|//:_version_is_10__7| true
  _version_is_at_least_11__8:::l
  _version_is_at_least_11__8 -->|//conditions:default| _version_is_at_least_12__9
  _version_is_at_least_11__8 -->|//:_version_is_11__8| true
  _version_is_at_least_12__9:::l
  _version_is_at_least_12__9 -->|//conditions:default| _version_is_at_least_13__10
  _version_is_at_least_12__9 -->|//:_version_is_12__9| true
  _version_is_at_least_13__10:::l
  _version_is_at_least_13__10 -->|//conditions:default| _version_is_at_least_14__11
  _version_is_at_least_13__10 -->|//:_version_is_13__10| true
  _version_is_at_least_14__11:::l
  _version_is_at_least_14__11 -->|//conditions:default| _version_is_at_least_15__12
  _version_is_at_least_14__11 -->|//:_version_is_14__11| true
  _version_is_at_least_15__12:::l
  _version_is_at_least_15__12 -->|//conditions:default| _version_is_at_least_16__13
  _version_is_at_least_15__12 -->|//:_version_is_15__12| true
  _version_is_at_least_16__13:::l
  _version_is_at_least_16__13 -->|//conditions:default| _version_is_at_least_17__14
  _version_is_at_least_16__13 -->|//:_version_is_16__13| true
  _version_is_at_least_17__14:::l
  _version_is_at_least_17__14 -->|//conditions:default| _version_is_at_least_18__latest
  _version_is_at_least_17__14 -->|//:_version_is_17__14| true
  _version_is_at_least_18__latest:::l
  _version_is_at_least_18__latest -->|//conditions:default| false
  _version_is_at_least_18__latest -->|//:_version_is_18__latest| true
  _version_is_at_most_15__12:::m
  _version_is_at_most_15__12 -->|//conditions:default| _version_is_at_most_14__11
  _version_is_at_most_15__12 -->|//:_version_is_15__12| true
  _version_is_at_most_14__11:::m
  _version_is_at_most_14__11 -->|//conditions:default| _version_is_at_most_13__10
  _version_is_at_most_14__11 -->|//:_version_is_14__11| true
  _version_is_at_most_13__10:::m
  _version_is_at_most_13__10 -->|//conditions:default| _version_is_at_most_12__9
  _version_is_at_most_13__10 -->|//:_version_is_13__10| true
  _version_is_at_most_12__9:::m
  _version_is_at_most_12__9 -->|//conditions:default| _version_is_at_most_11__8
  _version_is_at_most_12__9 -->|//:_version_is_12__9| true
  _version_is_at_most_11__8:::m
  _version_is_at_most_11__8 -->|//conditions:default| _version_is_at_most_10__7
  _version_is_at_most_11__8 -->|//:_version_is_11__8| true
  _version_is_at_most_10__7:::m
  _version_is_at_most_10__7 -->|//conditions:default| _version_is_at_most_9__6
  _version_is_at_most_10__7 -->|//:_version_is_10__7| true
  _version_is_at_most_9__6:::m
  _version_is_at_most_9__6 -->|//conditions:default| _version_is_at_most_8__5
  _version_is_at_most_9__6 -->|//:_version_is_9__6| true
  _version_is_at_most_8__5:::m
  _version_is_at_most_8__5 -->|//conditions:default| _version_is_at_most_7__4
  _version_is_at_most_8__5 -->|//:_version_is_8__5| true
  _version_is_at_most_7__4:::m
  _version_is_at_most_7__4 -->|//conditions:default| _version_is_at_most_6__3.1.1
  _version_is_at_most_7__4 -->|//:_version_is_7__4| true
  _version_is_at_most_6__3.1.1:::m
  _version_is_at_most_6__3.1.1 -->|//conditions:default| _version_is_at_most_5__3.1
  _version_is_at_most_6__3.1.1 -->|//:_version_is_6__3.1.1| true
  _version_is_at_most_5__3.1:::m
  _version_is_at_most_5__3.1 -->|//conditions:default| _version_is_at_most_4__3
  _version_is_at_most_5__3.1 -->|//:_version_is_5__3.1| true
  _version_is_at_most_4__3:::m
  _version_is_at_most_4__3 -->|//conditions:default| _version_is_at_most_3__2
  _version_is_at_most_4__3 -->|//:_version_is_4__3| true
  _version_is_at_most_3__2:::m
  _version_is_at_most_3__2 -->|//conditions:default| _version_is_at_most_2__1
  _version_is_at_most_3__2 -->|//:_version_is_3__2| true
  _version_is_at_most_2__1:::m
  _version_is_at_most_2__1 -->|//conditions:default| _version_is_at_most_1__0.1
  _version_is_at_most_2__1 -->|//:_version_is_2__1| true
  _version_is_at_most_1__0.1:::m
  _version_is_at_most_1__0.1 -->|//conditions:default| _version_is_at_most_0__0
  _version_is_at_most_1__0.1 -->|//:_version_is_1__0.1| true
  _version_is_at_most_0__0:::m
  _version_is_at_most_0__0 -->|//conditions:default| false
  _version_is_at_most_0__0 -->|//:_version_is_0__0| true
  true:::true
  false:::false
Loading
use nix -p bazel_7
/bazel-*
.direnv
MODULE.bazel.lock
load("@bazel_skylib//rules:common_settings.bzl", "string_setting")
string_setting(
name = "_monostate",
values = ["monostate"],
build_setting_default = "monostate",
visibility = ["//visibility:private"],
)
config_setting(
name = "true",
flag_values = { ":_monostate": "monostate" },
)
config_setting(
name = "false",
flag_values = { ":_monostate": "false" },
)
################################################################################
# Test:
load("@range_setting.bzl", "mk_range_setting")
load(":defs.bzl", "expect_val", "mk_test_utils")
vals_before, mid, vals_after = (
[0, 0.1, 1, 2, 3, 3.1],
"3.1.1",
list(range(4, 15)) + ["latest"]
)
vals = vals_before + [mid] + vals_after
version = mk_range_setting(
name = "version",
elements = vals,
)
#-------------------------------------------------------------------------------
t = mk_test_utils(version)
[
(
t.expect(mid, ">=", below_mid),
t.expect(mid, ">", below_mid),
t.expect_not(mid, "<=", below_mid),
t.expect_not(mid, "<", below_mid),
)
for below_mid in vals_before
]
t.expect(mid, ">=", mid)
t.expect_not(mid, ">", mid)
t.expect(mid, "<=", mid)
t.expect_not(mid, "<", mid)
t.expect(mid, "==", mid)
t.expect_not(mid, "==", vals[-1])
t.expect_not(mid, "==", vals[0])
[
(
t.expect_not(mid, ">=", above_mid),
t.expect_not(mid, ">", above_mid),
t.expect(mid, "<=", above_mid),
t.expect(mid, "<", above_mid),
)
for above_mid in vals_after
]
#-------------------------------------------------------------------------------
# smoke tests for the user interface:
## `struct.<cond op>(...):
expect_val(name = "op_1", ver = "4", val = version.le(4, "true", ""))
expect_val(name = "op_2", ver = "4", val = version.lt(5, "true", ""))
expect_val(name = "op_3", ver = "4", val = version.eq(4, "true", ""))
expect_val(name = "op_4", ver = "4", val = version.ge(1, "true", ""))
expect_val(name = "op_5", ver = "4", val = version.gt(3, "true", ""))
expect_val(name = "op_6", ver = "4", val = version.gt(4, "", else_ = "true"))
expect_val(name = "op_7", ver = "4", val = version.ge(4, "true", ""))
expect_val(name = "op_8", ver = "4", val = version.ge(5, "", else_ = "true"))
## `struct.cond(<cond op>, ...):
expect_val(name = "cond_1", ver = "4", val = version.cond("<=", 4, "true", ""))
expect_val(name = "cond_2", ver = "4", val = version.cond("<", 5, "true", ""))
expect_val(name = "cond_3", ver = "4", val = version.cond("==", 4, "true", ""))
expect_val(name = "cond_4", ver = "4", val = version.cond(">=", 1, "true", ""))
expect_val(name = "cond_5", ver = "4", val = version.cond(">", 3, "true", ""))
expect_val(name = "cond_6", ver = "4", val = version.cond(">", 4, "", else_ = "true"))
expect_val(name = "cond_7", ver = "4", val = version.cond(">=", 4, "true", ""))
expect_val(name = "cond_8", ver = "4", val = version.cond(">=", 5, "", else_ = "true"))
## `struct.label.<cond op>(rhs):
expect_val(name = "label_op_1", ver = "4", val = select({ version.label.le(4): "true", "//conditions:default": "" }))
expect_val(name = "label_op_2", ver = "4", val = select({ version.label.lt(5): "true", "//conditions:default": "" }))
expect_val(name = "label_op_3", ver = "4", val = select({ version.label.eq(4): "true", "//conditions:default": "" }))
expect_val(name = "label_op_4", ver = "4", val = select({ version.label.ge(1): "true", "//conditions:default": "" }))
expect_val(name = "label_op_5", ver = "4", val = select({ version.label.gt(3): "true", "//conditions:default": "" }))
## `struct.label.cond(<cond op>, rhs):
expect_val(name = "label_cond_1", ver = "4", val = select({ version.label.cond("<=", 4): "true", "//conditions:default": "" }))
expect_val(name = "label_cond_2", ver = "4", val = select({ version.label.cond("<", 5): "true", "//conditions:default": "" }))
expect_val(name = "label_cond_3", ver = "4", val = select({ version.label.cond("==", 4): "true", "//conditions:default": "" }))
expect_val(name = "label_cond_4", ver = "4", val = select({ version.label.cond(">=", 1): "true", "//conditions:default": "" }))
expect_val(name = "label_cond_5", ver = "4", val = select({ version.label.cond(">", 3): "true", "//conditions:default": "" }))
_set_version = transition(
implementation = lambda settings, attrs: {
str(Label(":version")): attrs.ver,
} if attrs.ver else {},
inputs = [],
outputs = [str(Label(":version"))],
)
expect_val = rule(
implementation = lambda ctx: (
fail(ctx.attr.val, "!=", ctx.attr.expected)
if not ctx.attr.expected == ctx.attr.val else []
),
attrs = dict(
expected = attr.string(default = "true"),
ver = attr.string(),
val = attr.string(mandatory = True),
),
cfg = _set_version,
)
def mk_test_utils(comparators, prefix = "test"):
count = [0] # wrapped in a list to capture by reference
def inc():
v = count[0]
count[0] = v + 1
return v
def expect(ver, cond, rhs, exp = "true"):
num = inc()
name = "{prefix}{num}___{}{}{}_=={}".format(ver, cond, rhs, exp, prefix = prefix, num = num)
expect_val(
name = name,
ver = ver,
val = comparators.cond(cond, rhs, then = "true", else_ = "false"),
expected = exp,
)
native.alias(
name = "test{}".format(num),
actual = ":" + name,
tags = ["manual"],
)
def expect_not(ver, cond, rhs): expect(ver, cond, rhs, exp = "false")
return struct(
expect = expect,
expect_not = expect_not,
)
module(name = "range_setting.bzl", bazel_compatibility = [
# ">=7.1.0"
])
bazel_dep(name = "bazel_skylib", version = "1.7.0")
load("@bazel_skylib//rules:common_settings.bzl", "string_flag", "string_setting")
load("@bazel_skylib//lib:structs.bzl", "structs")
_ALWAYS_TRUE = str(Label(":true"))
_ALWAYS_FALSE = str(Label(":false"))
# setting_name: str; name of the setting to make
# elements: List[stringify-able]; in increasing order
# default: elem in `elements` or None (defaults to last elem in `elements`)
# setting_rule: rule(); defaults to `string_flag`
# declare_supporting_targets_separately: bool; defaults to False
#
# returns: if `declare_supporting_targets_separately`:
# Tuple[
# fn() # callable; creates supporting BUILD-file targets
# struct(
# eq = fn(n, then, else_) => select(...)
# gt
# ge
# lt
# le
# )
# ]
# else:
# just the `struct`
def mk_range_setting(
name,
elements,
default = None,
setting_rule = string_flag,
visibility = None,
declare_supporting_targets_separately = False,
# TODO: extra attrs/tags/visibility?
):
if type(elements) != type([]): fail()
if elements == []: fail("cannot be empty")
if default != None:
if default not in elements: fail()
else:
default = str(elements[-1])
elements = [str(e) for e in elements]
elems_to_num = { n: i for i, n in enumerate(elements) }
if len(elements) != len(elems_to_num): fail(
"all elements must be unique"
)
# NOTE: must call the yielded `define_targets` function in the same package
# that `mk_range_setting` was invoked in.
# setting_label = native.package_relative_label(name) # Note: can't use in non-BUILD file contexts...
if declare_supporting_targets_separately:
if not type(name) == type(Label(":_")): fail("when using `declare_supporting_targets_separately`, please pass in a `Label` for `name`")
else:
setting_label = name
name = setting_label.name
else:
if not type(name) == type(""): fail()
setting_label = native.package_relative_label(name)
conf_setting_names = struct(
is_ = lambda elem: "_{base}_is_{num}__{name}".format(
base = name, num = elems_to_num[elem], name = elem,
),
at_least = lambda elem: "_{base}_is_at_least_{num}__{name}".format(
base = name, num = elems_to_num[elem], name = elem,
),
at_most = lambda elem: "_{base}_is_at_most_{num}__{name}".format(
base = name, num = elems_to_num[elem], name = elem,
),
)
conf_setting_labels = struct(**{
# name: lambda elem: str(setting_label.same_package_label(func(elem))) # TODO: same_package_label
# name: lambda elem: str(setting_label.relative(func(elem)))
# NOTE: double lambda here so that we capture `func` by "value":
name: (lambda f: (
lambda elem: str(setting_label.relative(f(elem)))
))(func)
for name, func in structs.to_dict(conf_setting_names).items()
})
orig_name = name
def declare_targets(name = name, setting_rule = setting_rule, default = default, visibility = visibility):
# we allow specifying `name` so that — in cases where
# `declare_supporting_targets_separately` is `True` — it's apparent that
# this macro is declaring the `setting_rule` target (this enables goto
# def to work in some primitive Bazel LSP impls)
if name != orig_name: fail("expected name = {}, got {}".format(orig_name, name))
_setting_label_during_decl = native.package_relative_label(name)
if setting_label != _setting_label_during_decl: fail("""
`mk_range_setting()` and the `declare_targets` function
it yields (when `declare_supporting_targets_separately` is True)
must be called from the same package.
mk_range_setting() was called from: {orig}
declare_targets was called from: {new}
""".format(
# TODO: same_package_label
# orig = str(setting_label.same_package_label(".")),
# new = str(_setting_label_during_decl.same_package_label(".")),
orig = str(setting_label.relative(".")),
new = str(_setting_label_during_decl.relative(".")),
))
extra_setting_rule_attrs = {}
if setting_rule == string_flag or setting_rule == string_setting:
extra_setting_rule_attrs["values"] = elements
setting_rule(
name = name,
build_setting_default = default,
visibility = visibility,
**extra_setting_rule_attrs
)
# One config setting per value:
for n, elem in enumerate(elements):
native.config_setting(
name = conf_setting_names.is_(elem),
flag_values = {
str(setting_label): elem,
},
visibility = visibility,
)
# Construct OR-chains going up and down so that we can do version range
# `select()`s.
#
# We do this instead of just using `selects.with_or` at each callsite so
# that we're creating fewer Bazel targets overall and so that calls to
# the functions below don't need to provide a `name`.
#
# See: https://github.com/bazelbuild/bazel-skylib/blob/ab604c1cd803e678753c0665a345eece1d333ea1/lib/selects.bzl#L145-L185
## Going up (is at least):
for n, elem in enumerate(elements):
# Special case optimization for `n == 0` — it is the min so the
# actual value must always be at least it.
if n == 0:
cond = _ALWAYS_TRUE
# Otherwise we need to do a comparison:
else:
# If the actual value is equal to the current value, yield true.
# Otherwise, continue down the chain.
#
# Last element in the chain is the max — if its comparison
# failed there's nothing else to compare to; yield `false`.
if n == len(elements) - 1: next = _ALWAYS_FALSE # TODO: can actually replace the bottom of the chain with the corresponding `config_setting` directly..
else:
next = conf_setting_labels.at_least(elements[n + 1])
cond = select({
conf_setting_labels.is_(elem): _ALWAYS_TRUE,
"//conditions:default": next
})
native.alias(
name = conf_setting_names.at_least(elem),
actual = cond,
visibility = visibility, # TODO?
)
## Going down (is at most):
for n, elem in reversed(enumerate(elements)):
# Special case optimization for `n == len(elements) - 1` — it is the
# max so the actual value must always be at most it.
if n == len(elements) - 1:
cond = _ALWAYS_TRUE
else:
# Same as above but now the last element in the chain is `n = 0`
# and the next element is `n - 1`.
if n == 0: next = _ALWAYS_FALSE # TODO: can actually replace the bottom of the chain with the corresponding `config_setting` directly..
else:
next = conf_setting_labels.at_most(elements[n - 1])
cond = select({
conf_setting_labels.is_(elem): _ALWAYS_TRUE,
"//conditions:default": next
})
native.alias(
name = conf_setting_names.at_most(elem),
actual = cond,
visibility = visibility, # TODO?
)
def build_comparators():
def mk_cond_label_producer(elem_to_cond_func):
def get_cond_label(elem):
elem = str(elem)
if not elem in elems_to_num: fail("Invalid value", elem)
return elem_to_cond_func(elem)
return get_cond_label
def mk_comparator(elem_to_cond_func):
def comparator(elem, then, else_):
# note: letting `select` check that then and else are of the same type..
return select({
elem_to_cond_func(elem): then,
"//conditions:default": else_
})
return comparator
eq = lambda elem: conf_setting_names.is_(elem)
ge = lambda elem: conf_setting_names.at_least(elem)
le = lambda elem: conf_setting_names.at_most(elem)
def greater_than(elem):
n = elems_to_num[elem]
if n == len(elements) - 1:
print("warning: `{} > {} (max)` will always be false!".format(str(setting_label), elem))
return _ALWAYS_FALSE
else:
return ge(elements[n + 1])
def less_than(elem):
n = elems_to_num[elem]
if n == 0:
print("warning: `{} < {} (min)` will always be false!".format(str(setting_label), elem))
return _ALWAYS_FALSE
else:
return le(elements[n - 1])
gt = greater_than
lt = less_than
comparison_ops = dict(
eq = eq,
ge = ge,
gt = gt,
le = le,
lt = lt,
# aliases:
is_ = eq,
at_least = ge,
above = gt,
at_most = le,
below = lt,
)
comparison_label_producers = {
n: (lambda f: mk_cond_label_producer(f))(func) # capture `func` by value
for n, func in comparison_ops.items()
}
comparison_funcs = {
n: (lambda f: mk_comparator(f))(func) # capture `func` by value
for n, func in comparison_label_producers.items()
}
op_map = {
"==": "eq",
">=": "ge",
">": "gt",
"<=": "le",
"<": "lt",
}
def cond_label(c, n): return comparison_label_producers[op_map[c]](n)
def cond(c, n, *a, **kw): return comparison_funcs[op_map[c]](n, *a, **kw)
return struct(
# if you want to construct the `select` yourself:
label = struct(
cond = cond_label,
**comparison_label_producers,
),
cond = cond,
**comparison_funcs,
)
comparators = build_comparators()
if declare_supporting_targets_separately:
return (declare_targets, comparators)
else:
declare_targets()
return comparators
# TODO: support having an "any"/"unconstrained" value?
# TODO: spin off into a repo?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment