GenEx: add $<LIST:FILTER> PREDICATE filter and REGEX keyword

Let $<LIST:FILTER> select elements by an arbitrary condition: PREDICATE keeps or
drops each element according to a <body> evaluated with $<_0> bound,
complementing the existing regular-expression form.  Also accept an explicit
REGEX keyword before the pattern so the regex and predicate forms read
symmetrically; the bare pattern keeps working.

Issue: #27892
This commit is contained in:
Mickaël Germain
2026-06-23 21:31:49 -07:00
parent d2ad140ef9
commit 092edb6860
13 changed files with 158 additions and 6 deletions
@@ -882,6 +882,25 @@ List Transformations
(``EXCLUDE``) the regular expression ``regex``. The result is the same as
:genex:`$<FILTER:list,INCLUDE|EXCLUDE,regex>`.
.. versionadded:: 4.5
The regular expression may be introduced explicitly with a ``REGEX``
keyword, and a ``PREDICATE`` keyword selects items using a generator
expression instead:
.. code-block:: cmake
$<LIST:FILTER,list,INCLUDE|EXCLUDE,REGEX,regex>
$<LIST:FILTER,list,INCLUDE|EXCLUDE,PREDICATE,body>
With ``PREDICATE``, ``body`` is evaluated once per item with the bound
operand :genex:`$<_0>` expanding to the current item. The body must
evaluate to exactly ``0`` or ``1``; ``INCLUDE`` keeps items whose body
yields ``1`` and ``EXCLUDE`` removes them. Use ``$<BOOL:...>`` to
coerce other values. Because ``REGEX`` and ``PREDICATE`` are now keywords,
a bare regular expression equal to ``REGEX`` or ``PREDICATE`` must use the
explicit ``REGEX`` form.
.. genex:: $<LIST:TRANSFORM,list,ACTION[,SELECTOR]>
.. versionadded:: 3.27
@@ -1,7 +1,7 @@
genex-list-filter-transform-predicate
-------------------------------------
* The :genex:`LIST` generator expression's ``TRANSFORM`` operation gained a
``PREDICATE`` selector that chooses the items to transform by evaluating an
* The :genex:`LIST` generator expression's ``FILTER`` and ``TRANSFORM``
operations gained a ``PREDICATE`` keyword that selects items by evaluating an
arbitrary generator expression once per item, with ``$<_0>`` referring to the
current item.
current item. ``FILTER`` also gained an explicit ``REGEX`` keyword.
+58 -3
View File
@@ -2228,6 +2228,12 @@ static const struct ListNode : public cmGeneratorExpressionNode
bool ShouldEvaluateNextParameter(std::vector<std::string> const& parameters,
std::string&) const override
{
// Leave the FILTER PREDICATE <body> (slot 4) unevaluated so $<_0> is never
// evaluated unbound.
if (parameters.size() == 4 && parameters[0] == "FILTER" &&
parameters[3] == "PREDICATE") {
return false;
}
// Leave a TRANSFORM PREDICATE selector's <body> unevaluated. PREDICATE is
// the selector keyword only when it sits exactly at the selector position
// (not when it is a literal action argument such as APPEND PREDICATE).
@@ -2314,6 +2320,48 @@ static const struct ListNode : public cmGeneratorExpressionNode
.to_string();
}
// FILTER ... PREDICATE <body>: genex-native predicate filter.
if (parameters.size() >= 4 && parameters[0] == "FILTER" &&
parameters[3] == "PREDICATE") {
if (parameters.size() != 5) {
reportError(eval, content->GetOriginalExpression(),
"sub-command FILTER, PREDICATE expects a single <body> "
"argument.");
return std::string();
}
std::string const& op = parameters[2];
if (op != "INCLUDE" && op != "EXCLUDE") {
reportError(
eval, content->GetOriginalExpression(),
cmStrCat("sub-command FILTER does not recognize operator \"", op,
"\". It must be either INCLUDE or EXCLUDE."));
return std::string();
}
cmList list = GetList(parameters[1]);
if (list.empty()) {
return std::string();
}
cmGeneratorExpressionEvaluatorVector const& predicateBody =
content->GetParamChildren()[4];
auto mask = EvaluatePredicateMask(predicateBody, list, "FILTER"_s, eval,
content, dagChecker);
if (!mask) {
return std::string();
}
bool const keepWhenTrue = (op == "INCLUDE");
std::vector<std::string> out;
std::size_t i = 0;
for (auto const& element : list) {
if ((*mask)[i] == keepWhenTrue) {
out.push_back(element);
}
++i;
}
return cmList{ out.begin(), out.end(), cmList::ExpandElements::No,
cmList::EmptyElements::Yes }
.to_string();
}
static std::unordered_map<
cm::string_view,
std::function<std::string(cm::GenEx::Evaluation*,
@@ -2526,7 +2574,13 @@ static const struct ListNode : public cmGeneratorExpressionNode
{ "FILTER"_s,
[](cm::GenEx::Evaluation* ev, GeneratorExpressionContent const* cnt,
Arguments& args) -> std::string {
if (CheckListParameters(ev, cnt, "FILTER"_s, args, 3)) {
// args = [list, INCLUDE|EXCLUDE, <regex> | REGEX <regex>].
// (PREDICATE is handled up-front in Evaluate and never reaches
// here.)
bool const explicitRegex =
args.size() >= 3 && args[2] == "REGEX"_s;
int const required = explicitRegex ? 4 : 3;
if (CheckListParameters(ev, cnt, "FILTER"_s, args, required)) {
auto const& op = args[1];
if (op != "INCLUDE"_s && op != "EXCLUDE"_s) {
reportError(
@@ -2535,9 +2589,10 @@ static const struct ListNode : public cmGeneratorExpressionNode
op, "\". It must be either INCLUDE or EXCLUDE."));
return std::string{};
}
auto const& regex = explicitRegex ? args[3] : args[2];
try {
return GetList(args.front())
.filter(args[2],
.filter(regex,
op == "INCLUDE"_s ? cmList::FilterMode::INCLUDE
: cmList::FilterMode::EXCLUDE)
.to_string();
@@ -2545,7 +2600,7 @@ static const struct ListNode : public cmGeneratorExpressionNode
reportError(
ev, cnt->GetOriginalExpression(),
cmStrCat("sub-command FILTER, failed to compile regex \"",
args[2], "\"."));
regex, "\"."));
return std::string{};
}
}
+59
View File
@@ -49,6 +49,43 @@ bool expectEq(char const* name, std::string const& got,
}
}
static bool testFilterPredicateInclude()
{
GenExFixture fx;
// Keep only elements equal to "a".
return expectEq(
"testFilterPredicateInclude",
fx.Eval("$<LIST:FILTER,a;b;a;c,INCLUDE,PREDICATE,$<STREQUAL:$<_0>,a>>"),
"a;a");
}
static bool testFilterPredicateExclude()
{
GenExFixture fx;
// Drop elements equal to "a".
return expectEq(
"testFilterPredicateExclude",
fx.Eval("$<LIST:FILTER,a;b;a;c,EXCLUDE,PREDICATE,$<STREQUAL:$<_0>,a>>"),
"b;c");
}
static bool testFilterRegexKeyword()
{
GenExFixture fx;
// Explicit REGEX keyword behaves like the bare form.
return expectEq("testFilterRegexKeyword",
fx.Eval("$<LIST:FILTER,foo;bar;baz,INCLUDE,REGEX,^ba>"),
"bar;baz");
}
static bool testFilterBareRegexUnchanged()
{
GenExFixture fx;
// The legacy bare form still works.
return expectEq("testFilterBareRegexUnchanged",
fx.Eval("$<LIST:FILTER,foo;bar;baz,EXCLUDE,^ba>"), "foo");
}
static bool testCannedTransformStillWorks()
{
GenExFixture fx;
@@ -99,6 +136,13 @@ static bool testTransformPredicateApplyShadowing()
"aa;bb");
}
static bool testFilterPredicateEmptyList()
{
GenExFixture fx;
return expectEq("testFilterPredicateEmptyList",
fx.Eval("$<LIST:FILTER,,INCLUDE,PREDICATE,1>"), "");
}
static bool testTransformPredicateEmptyList()
{
GenExFixture fx;
@@ -108,6 +152,18 @@ static bool testTransformPredicateEmptyList()
int testGenExListPredicate(int /*argc*/, char* /*argv*/[])
{
if (!testFilterPredicateInclude()) {
return 1;
}
if (!testFilterPredicateExclude()) {
return 1;
}
if (!testFilterRegexKeyword()) {
return 1;
}
if (!testFilterBareRegexUnchanged()) {
return 1;
}
if (!testCannedTransformStillWorks()) {
return 1;
}
@@ -123,6 +179,9 @@ int testGenExListPredicate(int /*argc*/, char* /*argv*/[])
if (!testTransformPredicateApplyShadowing()) {
return 1;
}
if (!testFilterPredicateEmptyList()) {
return 1;
}
if (!testTransformPredicateEmptyList()) {
return 1;
}
@@ -0,0 +1,5 @@
file(READ "${RunCMake_TEST_BINARY_DIR}/out.txt" actual)
string(STRIP "${actual}" actual)
if(NOT actual STREQUAL "beta")
set(RunCMake_TEST_FAILED "unexpected FILTER PREDICATE output: [${actual}]")
endif()
@@ -0,0 +1,3 @@
set(input "alpha;beta;gamma")
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/out.txt"
CONTENT "$<LIST:FILTER,${input},INCLUDE,PREDICATE,$<STREQUAL:$<_0>,beta>>\n")
@@ -0,0 +1 @@
sub-command FILTER, PREDICATE expects a single <body> argument
@@ -0,0 +1,2 @@
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
CONTENT "$<LIST:FILTER,a;b,INCLUDE,PREDICATE>")
@@ -0,0 +1 @@
PREDICATE body must evaluate to "0" or "1"
@@ -0,0 +1,2 @@
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
CONTENT "$<LIST:FILTER,a;b,INCLUDE,PREDICATE,maybe>")
@@ -64,10 +64,13 @@ run_cmake(ListTransformApplyNested)
run_cmake(ListTransformApplyBadSelector)
run_cmake(ListTransformApplyBodyError)
run_cmake(ListTransformApplyMissingBody)
run_cmake(ListFilterPredicate)
run_cmake(ListTransformPredicateNonBool)
run_cmake(ListTransformPredicateCombined)
run_cmake(ListTransformPredicateMissingBody)
run_cmake(ListTransformPredicateLinkLibraries)
run_cmake(ListFilterPredicateMissingBody)
run_cmake(ListFilterPredicateNonBool)
run_cmake(BoundOperandOutsideBinding)
function(run_cmake_build test)