From 092edb686035fbcf4b621387a50603b088f2bbcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Germain?= Date: Thu, 18 Jun 2026 11:27:51 -0700 Subject: [PATCH] GenEx: add $ PREDICATE filter and REGEX keyword Let $ select elements by an arbitrary condition: PREDICATE keeps or drops each element according to a 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 --- Help/manual/cmake-generator-expressions.7.rst | 19 ++++++ .../genex-list-filter-transform-predicate.rst | 6 +- Source/cmGeneratorExpressionNode.cxx | 61 ++++++++++++++++++- Tests/CMakeLib/testGenExListPredicate.cxx | 59 ++++++++++++++++++ .../ListFilterPredicate-check.cmake | 5 ++ .../ListFilterPredicate.cmake | 3 + .../ListFilterPredicateMissingBody-result.txt | 1 + .../ListFilterPredicateMissingBody-stderr.txt | 1 + .../ListFilterPredicateMissingBody.cmake | 2 + .../ListFilterPredicateNonBool-result.txt | 1 + .../ListFilterPredicateNonBool-stderr.txt | 1 + .../ListFilterPredicateNonBool.cmake | 2 + .../GeneratorExpression/RunCMakeTest.cmake | 3 + 13 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 Tests/RunCMake/GeneratorExpression/ListFilterPredicate-check.cmake create mode 100644 Tests/RunCMake/GeneratorExpression/ListFilterPredicate.cmake create mode 100644 Tests/RunCMake/GeneratorExpression/ListFilterPredicateMissingBody-result.txt create mode 100644 Tests/RunCMake/GeneratorExpression/ListFilterPredicateMissingBody-stderr.txt create mode 100644 Tests/RunCMake/GeneratorExpression/ListFilterPredicateMissingBody.cmake create mode 100644 Tests/RunCMake/GeneratorExpression/ListFilterPredicateNonBool-result.txt create mode 100644 Tests/RunCMake/GeneratorExpression/ListFilterPredicateNonBool-stderr.txt create mode 100644 Tests/RunCMake/GeneratorExpression/ListFilterPredicateNonBool.cmake diff --git a/Help/manual/cmake-generator-expressions.7.rst b/Help/manual/cmake-generator-expressions.7.rst index 1661fe868d..3d81782ed8 100644 --- a/Help/manual/cmake-generator-expressions.7.rst +++ b/Help/manual/cmake-generator-expressions.7.rst @@ -882,6 +882,25 @@ List Transformations (``EXCLUDE``) the regular expression ``regex``. The result is the same as :genex:`$`. + .. 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 + + $ + $ + + 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 ``$`` 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:: $ .. versionadded:: 3.27 diff --git a/Help/release/dev/genex-list-filter-transform-predicate.rst b/Help/release/dev/genex-list-filter-transform-predicate.rst index 53ca41026d..aa212bedbc 100644 --- a/Help/release/dev/genex-list-filter-transform-predicate.rst +++ b/Help/release/dev/genex-list-filter-transform-predicate.rst @@ -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. diff --git a/Source/cmGeneratorExpressionNode.cxx b/Source/cmGeneratorExpressionNode.cxx index d194f855b1..604bc7b1b1 100644 --- a/Source/cmGeneratorExpressionNode.cxx +++ b/Source/cmGeneratorExpressionNode.cxx @@ -2228,6 +2228,12 @@ static const struct ListNode : public cmGeneratorExpressionNode bool ShouldEvaluateNextParameter(std::vector const& parameters, std::string&) const override { + // Leave the FILTER PREDICATE (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 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 : 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 " + "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 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 { - if (CheckListParameters(ev, cnt, "FILTER"_s, args, 3)) { + // args = [list, INCLUDE|EXCLUDE, | 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{}; } } diff --git a/Tests/CMakeLib/testGenExListPredicate.cxx b/Tests/CMakeLib/testGenExListPredicate.cxx index 77fb57570c..afb2e34034 100644 --- a/Tests/CMakeLib/testGenExListPredicate.cxx +++ b/Tests/CMakeLib/testGenExListPredicate.cxx @@ -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("$,a>>"), + "a;a"); +} + +static bool testFilterPredicateExclude() +{ + GenExFixture fx; + // Drop elements equal to "a". + return expectEq( + "testFilterPredicateExclude", + fx.Eval("$,a>>"), + "b;c"); +} + +static bool testFilterRegexKeyword() +{ + GenExFixture fx; + // Explicit REGEX keyword behaves like the bare form. + return expectEq("testFilterRegexKeyword", + fx.Eval("$"), + "bar;baz"); +} + +static bool testFilterBareRegexUnchanged() +{ + GenExFixture fx; + // The legacy bare form still works. + return expectEq("testFilterBareRegexUnchanged", + fx.Eval("$"), "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("$"), ""); +} + 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; } diff --git a/Tests/RunCMake/GeneratorExpression/ListFilterPredicate-check.cmake b/Tests/RunCMake/GeneratorExpression/ListFilterPredicate-check.cmake new file mode 100644 index 0000000000..7005290e88 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListFilterPredicate-check.cmake @@ -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() diff --git a/Tests/RunCMake/GeneratorExpression/ListFilterPredicate.cmake b/Tests/RunCMake/GeneratorExpression/ListFilterPredicate.cmake new file mode 100644 index 0000000000..3bd445bdf3 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListFilterPredicate.cmake @@ -0,0 +1,3 @@ +set(input "alpha;beta;gamma") +file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/out.txt" + CONTENT "$,beta>>\n") diff --git a/Tests/RunCMake/GeneratorExpression/ListFilterPredicateMissingBody-result.txt b/Tests/RunCMake/GeneratorExpression/ListFilterPredicateMissingBody-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListFilterPredicateMissingBody-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/GeneratorExpression/ListFilterPredicateMissingBody-stderr.txt b/Tests/RunCMake/GeneratorExpression/ListFilterPredicateMissingBody-stderr.txt new file mode 100644 index 0000000000..5ae414cd96 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListFilterPredicateMissingBody-stderr.txt @@ -0,0 +1 @@ +sub-command FILTER, PREDICATE expects a single argument diff --git a/Tests/RunCMake/GeneratorExpression/ListFilterPredicateMissingBody.cmake b/Tests/RunCMake/GeneratorExpression/ListFilterPredicateMissingBody.cmake new file mode 100644 index 0000000000..60b4138f7f --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListFilterPredicateMissingBody.cmake @@ -0,0 +1,2 @@ +file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt" + CONTENT "$") diff --git a/Tests/RunCMake/GeneratorExpression/ListFilterPredicateNonBool-result.txt b/Tests/RunCMake/GeneratorExpression/ListFilterPredicateNonBool-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListFilterPredicateNonBool-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/GeneratorExpression/ListFilterPredicateNonBool-stderr.txt b/Tests/RunCMake/GeneratorExpression/ListFilterPredicateNonBool-stderr.txt new file mode 100644 index 0000000000..550ac6d776 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListFilterPredicateNonBool-stderr.txt @@ -0,0 +1 @@ +PREDICATE body must evaluate to "0" or "1" diff --git a/Tests/RunCMake/GeneratorExpression/ListFilterPredicateNonBool.cmake b/Tests/RunCMake/GeneratorExpression/ListFilterPredicateNonBool.cmake new file mode 100644 index 0000000000..ea1fdd14f0 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListFilterPredicateNonBool.cmake @@ -0,0 +1,2 @@ +file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt" + CONTENT "$") diff --git a/Tests/RunCMake/GeneratorExpression/RunCMakeTest.cmake b/Tests/RunCMake/GeneratorExpression/RunCMakeTest.cmake index ec901f6c46..db24f953ce 100644 --- a/Tests/RunCMake/GeneratorExpression/RunCMakeTest.cmake +++ b/Tests/RunCMake/GeneratorExpression/RunCMakeTest.cmake @@ -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)