diff --git a/Help/manual/cmake-generator-expressions.7.rst b/Help/manual/cmake-generator-expressions.7.rst index 2626d6a89e..1661fe868d 100644 --- a/Help/manual/cmake-generator-expressions.7.rst +++ b/Help/manual/cmake-generator-expressions.7.rst @@ -977,6 +977,22 @@ List Transformations $ + ``PREDICATE`` + Specify a generator expression ``body`` evaluated once per item with the + bound operand :genex:`$<_0>` expanding to the current item. Only items + whose body evaluates to ``1`` are transformed; the body must evaluate to + exactly ``0`` or ``1``. ``PREDICATE`` may be combined with any action, + including ``APPLY`` (in which case both bodies bind :genex:`$<_0>` + independently). + Like all selectors, only one selector may be given; ``PREDICATE`` cannot + be combined with ``AT``, ``FOR``, or ``REGEX``. + + .. code-block:: cmake + + $ + + .. versionadded:: 4.5 + .. genex:: $ Joins the ``list`` with the content of the ``glue`` string inserted between diff --git a/Help/release/dev/genex-list-filter-transform-predicate.rst b/Help/release/dev/genex-list-filter-transform-predicate.rst new file mode 100644 index 0000000000..53ca41026d --- /dev/null +++ b/Help/release/dev/genex-list-filter-transform-predicate.rst @@ -0,0 +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 + arbitrary generator expression once per item, with ``$<_0>`` referring to the + current item. diff --git a/Source/cmGeneratorExpressionNode.cxx b/Source/cmGeneratorExpressionNode.cxx index 7dfdc52c81..d194f855b1 100644 --- a/Source/cmGeneratorExpressionNode.cxx +++ b/Source/cmGeneratorExpressionNode.cxx @@ -149,6 +149,41 @@ static std::string EvaluateBodyWithBoundOperand( return result; } +// Evaluate `predicateBody` once per element of `list`, binding `$<_0>` to the +// element (reusing EvaluateBodyWithBoundOperand). Each result must be exactly +// "0" or "1". Returns the per-element boolean mask, or cm::nullopt after +// reporting an error (non-boolean result, or a failure inside the body). +static cm::optional> EvaluatePredicateMask( + cmGeneratorExpressionEvaluatorVector const& predicateBody, + cmList const& list, cm::string_view subCommand, cm::GenEx::Evaluation* eval, + GeneratorExpressionContent const* content, + cmGeneratorExpressionDAGChecker* dagChecker) +{ + std::vector mask; + mask.reserve(list.size()); + for (auto const& element : list) { + std::string r = + EvaluateBodyWithBoundOperand(predicateBody, element, eval, dagChecker); + if (eval->HadError) { + return cm::nullopt; + } + if (r == "1") { + mask.push_back(true); + } else if (r == "0") { + mask.push_back(false); + } else { + reportError( + eval, content->GetOriginalExpression(), + cmStrCat("sub-command ", subCommand, + ", PREDICATE body must evaluate to \"0\" or \"1\", but " + "evaluated to \"", + r, "\".")); + return cm::nullopt; + } + } + return mask; +} + static const struct ZeroNode : public cmGeneratorExpressionNode { ZeroNode() {} // NOLINT(modernize-use-equals-default) @@ -1948,6 +1983,123 @@ cm::optional FindTransformActionDescriptor( return it->second; } +// Index in `parameters` at which a TRANSFORM action's selector region begins, +// or nullopt if this is not a TRANSFORM or the action is unknown. For the +// APPLY action the occupies slot 3, so the selector starts at slot 4. +cm::optional TransformSelectorStart( + std::vector const& parameters) +{ + if (parameters.size() < 3 || parameters[0] != "TRANSFORM") { + return cm::nullopt; + } + std::string const& action = parameters[2]; + if (action == "APPLY") { + return std::size_t{ 4 }; + } + if (auto d = FindTransformActionDescriptor(action)) { + return std::size_t{ 3 } + static_cast(d->Arity); + } + return cm::nullopt; +} + +// Handle $>. `predIndex` is the index of +// the PREDICATE token in `parameters`; the follows it. PREDICATE is +// the sole selector: only elements whose predicate is "1" are transformed. +std::string EvaluateTransformPredicate( + std::vector const& parameters, std::size_t predIndex, + cm::GenEx::Evaluation* eval, GeneratorExpressionContent const* content, + cmGeneratorExpressionDAGChecker* dagChecker) +{ + // PREDICATE must take exactly one and not be combined with another + // selector (AT/FOR/REGEX) or trailing tokens. + if (parameters.size() < predIndex + 2) { + reportError(eval, content->GetOriginalExpression(), + "sub-command TRANSFORM, selector PREDICATE expects a " + "argument."); + return std::string(); + } + if (parameters.size() > predIndex + 2) { + reportError(eval, content->GetOriginalExpression(), + "sub-command TRANSFORM, selector PREDICATE expects a single " + " argument and cannot be combined with another " + "selector."); + return std::string(); + } + + cmList list = GetList(parameters[1]); + if (list.empty()) { + return std::string(); + } + + cmGeneratorExpressionEvaluatorVector const& predicateBody = + content->GetParamChildren()[predIndex + 1]; + auto mask = EvaluatePredicateMask(predicateBody, list, "TRANSFORM"_s, eval, + content, dagChecker); + if (!mask) { + return std::string(); + } + + if (parameters[2] == "APPLY") { + cmGeneratorExpressionEvaluatorVector const& applyBody = + content->GetParamChildren()[3]; + std::vector out; + out.reserve(list.size()); + std::size_t i = 0; + for (auto const& element : list) { + if ((*mask)[i]) { + out.push_back( + EvaluateBodyWithBoundOperand(applyBody, element, eval, dagChecker)); + if (eval->HadError) { + return std::string(); + } + } else { + out.push_back(element); + } + ++i; + } + return cmList{ out.begin(), out.end(), cmList::ExpandElements::No, + cmList::EmptyElements::Yes } + .to_string(); + } + + std::string const& action = parameters[2]; + auto descriptor = FindTransformActionDescriptor(action); + if (!descriptor) { + reportError( + eval, content->GetOriginalExpression(), + cmStrCat(" sub-command TRANSFORM, ", action, " invalid action.")); + return std::string(); + } + + // Action arguments occupy parameters[3 .. predIndex); TransformSelectorStart + // guarantees there are exactly descriptor->Arity of them. + std::vector arguments(parameters.begin() + 3, + parameters.begin() + predIndex); + + std::vector indices; + for (std::size_t i = 0; i < mask->size(); ++i) { + if ((*mask)[i]) { + indices.push_back(static_cast(i)); + } + } + if (indices.empty()) { + // No element selected: TRANSFORM is a no-op. + return list.to_string(); + } + + auto selector = + cmList::TransformSelector::New( + std::move(indices)); + selector->Makefile = eval->Context.LG->GetMakefile(); + try { + return list.transform(descriptor->Action, arguments, std::move(selector)) + .to_string(); + } catch (cmList::transform_error& e) { + reportError(eval, content->GetOriginalExpression(), e.what()); + return std::string(); + } +} + // Parse the optional trailing selector of a $ action // (AT ... / FOR [] / REGEX ) into a // cmList::TransformSelector. Returns nullptr (after reporting via `eval`) on @@ -2076,6 +2228,15 @@ static const struct ListNode : public cmGeneratorExpressionNode bool ShouldEvaluateNextParameter(std::vector const& parameters, std::string&) const override { + // 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). + if (auto start = TransformSelectorStart(parameters)) { + if (parameters.size() == *start + 1 && + parameters.back() == "PREDICATE") { + return false; + } + } // Skip the APPLY (4th parameter) so $<_0> is not evaluated unbound; // selector args (5th+) evaluate normally. return !(parameters.size() == 3 && parameters[0] == "TRANSFORM" && @@ -2087,6 +2248,18 @@ static const struct ListNode : public cmGeneratorExpressionNode GeneratorExpressionContent const* content, cmGeneratorExpressionDAGChecker* dagChecker) const override { + // TRANSFORM ... PREDICATE : genex-native predicate selector, usable + // with any action (canned or APPLY). Handled here (not in the + // listCommands lambda) because the predicate needs the DAG checker. + if (parameters.size() >= 3 && parameters[0] == "TRANSFORM") { + if (auto start = TransformSelectorStart(parameters)) { + if (*start < parameters.size() && parameters[*start] == "PREDICATE") { + return EvaluateTransformPredicate(parameters, *start, eval, content, + dagChecker); + } + } + } + if (parameters.size() >= 3 && parameters[0] == "TRANSFORM" && parameters[2] == "APPLY") { if (parameters.size() < 4) { diff --git a/Tests/CMakeLib/CMakeLists.txt b/Tests/CMakeLib/CMakeLists.txt index e14898da0a..d6063a2ac6 100644 --- a/Tests/CMakeLib/CMakeLists.txt +++ b/Tests/CMakeLib/CMakeLists.txt @@ -16,6 +16,7 @@ set(CMakeLib_TESTS testGeneratedFileStream.cxx testGenExBoundOperand.cxx testGenExTransformApply.cxx + testGenExListPredicate.cxx testJSONHelpers.cxx testRST.cxx testRange.cxx diff --git a/Tests/CMakeLib/testGenExListPredicate.cxx b/Tests/CMakeLib/testGenExListPredicate.cxx new file mode 100644 index 0000000000..77fb57570c --- /dev/null +++ b/Tests/CMakeLib/testGenExListPredicate.cxx @@ -0,0 +1,130 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file LICENSE.rst or https://cmake.org/licensing for details. */ +#include +#include + +#include + +#include "cmGeneratorExpression.h" +#include "cmGlobalGenerator.h" +#include "cmLocalGenerator.h" +#include "cmMakefile.h" +#include "cmState.h" +#include "cmStateDirectory.h" +#include "cmStateSnapshot.h" +#include "cmake.h" + +namespace { +struct GenExFixture +{ + cmake CMake{ cmState::Role::Project }; + std::unique_ptr GG; + std::unique_ptr MF; + std::unique_ptr LG; + + GenExFixture() + { + this->GG = cm::make_unique(&this->CMake); + cmStateSnapshot snapshot = this->CMake.GetCurrentSnapshot(); + snapshot.GetDirectory().SetCurrentBinary("."); + snapshot.GetDirectory().SetCurrentSource("."); + this->MF = cm::make_unique(this->GG.get(), snapshot); + this->LG = this->GG->CreateLocalGenerator(this->MF.get()); + } + + std::string Eval(std::string const& expr) + { + return cmGeneratorExpression::Evaluate(expr, this->LG.get(), "Debug"); + } +}; + +bool expectEq(char const* name, std::string const& got, + std::string const& want) +{ + if (got != want) { + std::cerr << name << ": expected '" << want << "', got '" << got << "'\n"; + return false; + } + return true; +} +} + +static bool testCannedTransformStillWorks() +{ + GenExFixture fx; + // Existing canned action + REGEX selector must be unaffected by the + // refactor. + return expectEq("testCannedTransformStillWorks", + fx.Eval("$"), + "foo;BAR;BAZ"); +} + +static bool testTransformPredicateCanned() +{ + GenExFixture fx; + // PREPEND "X-" only to elements equal to "a"; others pass through. + return expectEq( + "testTransformPredicateCanned", + fx.Eval( + "$,a>>"), + "X-a;b;X-a"); +} + +static bool testTransformPredicateNoneSelected() +{ + GenExFixture fx; + // No element matches: the list is returned unchanged. + return expectEq("testTransformPredicateNoneSelected", + fx.Eval("$"), + "a;b;c"); +} + +static bool testTransformPredicateApply() +{ + GenExFixture fx; + // Upper-case only elements equal to "a"; "b" passes through unchanged. + return expectEq("testTransformPredicateApply", + fx.Eval("$>," + "PREDICATE,$,a>>"), + "A;b;A"); +} + +static bool testTransformPredicateApplyShadowing() +{ + GenExFixture fx; + // The apply body and predicate body each independently bind $<_0>. + return expectEq("testTransformPredicateApplyShadowing", + fx.Eval("$$<_0>,PREDICATE," + "$,a>>"), + "aa;bb"); +} + +static bool testTransformPredicateEmptyList() +{ + GenExFixture fx; + return expectEq("testTransformPredicateEmptyList", + fx.Eval("$"), ""); +} + +int testGenExListPredicate(int /*argc*/, char* /*argv*/[]) +{ + if (!testCannedTransformStillWorks()) { + return 1; + } + if (!testTransformPredicateCanned()) { + return 1; + } + if (!testTransformPredicateNoneSelected()) { + return 1; + } + if (!testTransformPredicateApply()) { + return 1; + } + if (!testTransformPredicateApplyShadowing()) { + return 1; + } + if (!testTransformPredicateEmptyList()) { + return 1; + } + return 0; +} diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformPredicateCombined-result.txt b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateCombined-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateCombined-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformPredicateCombined-stderr.txt b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateCombined-stderr.txt new file mode 100644 index 0000000000..9de1d9200e --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateCombined-stderr.txt @@ -0,0 +1 @@ +selector PREDICATE expects a single argument diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformPredicateCombined.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateCombined.cmake new file mode 100644 index 0000000000..bb335dc0e4 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateCombined.cmake @@ -0,0 +1,2 @@ +file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt" + CONTENT "$") diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformPredicateLinkLibraries-check.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateLinkLibraries-check.cmake new file mode 100644 index 0000000000..972f6a3a4c --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateLinkLibraries-check.cmake @@ -0,0 +1,12 @@ +file(READ "${RunCMake_TEST_BINARY_DIR}/filtered.txt" actual) +file(READ "${RunCMake_TEST_BINARY_DIR}/expected.txt" expected) +string(STRIP "${actual}" actual) +string(STRIP "${expected}" expected) +# PREDICATE over app's LINK_LIBRARIES must prefix only STATIC_LIBRARY targets +# and leave INTERFACE_LIBRARY targets unchanged. +if(NOT actual STREQUAL expected) + set(RunCMake_TEST_FAILED + "PREDICATE-over-LINK_LIBRARIES output does not match the expected list:\n" + " actual: [${actual}]\n" + " expected: [${expected}]") +endif() diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformPredicateLinkLibraries.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateLinkLibraries.cmake new file mode 100644 index 0000000000..2580743973 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateLinkLibraries.cmake @@ -0,0 +1,28 @@ +enable_language(C) + +# Evaluate LINK_LIBRARIES transitively (CMP0189, CMake 4.1+); the test dir's +# cmake_minimum_required would otherwise leave this OLD. +cmake_policy(SET CMP0189 NEW) + +# A dependency tree with mixed library types: +# app -> { netlib, plugin }; netlib -> { ssl, zlib } +# netlib, ssl, zlib are STATIC_LIBRARY; plugin is INTERFACE_LIBRARY. +add_library(ssl STATIC empty.c) +add_library(zlib STATIC empty.c) +add_library(plugin INTERFACE) +add_library(netlib STATIC empty.c) +target_link_libraries(netlib PUBLIC ssl zlib) +add_library(app STATIC empty.c) +target_link_libraries(app PRIVATE netlib plugin) + +# PREDICATE body reads each element's TYPE via TARGET_PROPERTY, demonstrating +# that the body is evaluated in a target context (context-sensitivity). Only +# STATIC_LIBRARY targets receive the PREPEND; INTERFACE_LIBRARY targets are +# left unchanged. +file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/filtered.txt" + CONTENT "$,PREPEND,lib:,PREDICATE,$,TYPE>,STATIC_LIBRARY>>\n") + +# Exact reference: direct deps first (netlib, plugin), then netlib's transitive +# deps (ssl, zlib); only the three STATIC targets gain the "lib:" prefix. +file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/expected.txt" + CONTENT "lib:netlib;plugin;lib:ssl;lib:zlib\n") diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformPredicateMissingBody-result.txt b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateMissingBody-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateMissingBody-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformPredicateMissingBody-stderr.txt b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateMissingBody-stderr.txt new file mode 100644 index 0000000000..9047c3842c --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateMissingBody-stderr.txt @@ -0,0 +1 @@ +selector PREDICATE expects a argument diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformPredicateMissingBody.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateMissingBody.cmake new file mode 100644 index 0000000000..75e0fa8136 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateMissingBody.cmake @@ -0,0 +1,2 @@ +file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt" + CONTENT "$") diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformPredicateNonBool-result.txt b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateNonBool-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateNonBool-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformPredicateNonBool-stderr.txt b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateNonBool-stderr.txt new file mode 100644 index 0000000000..550ac6d776 --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateNonBool-stderr.txt @@ -0,0 +1 @@ +PREDICATE body must evaluate to "0" or "1" diff --git a/Tests/RunCMake/GeneratorExpression/ListTransformPredicateNonBool.cmake b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateNonBool.cmake new file mode 100644 index 0000000000..22f4ea6d9a --- /dev/null +++ b/Tests/RunCMake/GeneratorExpression/ListTransformPredicateNonBool.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 d024ab7e34..ec901f6c46 100644 --- a/Tests/RunCMake/GeneratorExpression/RunCMakeTest.cmake +++ b/Tests/RunCMake/GeneratorExpression/RunCMakeTest.cmake @@ -64,6 +64,10 @@ run_cmake(ListTransformApplyNested) run_cmake(ListTransformApplyBadSelector) run_cmake(ListTransformApplyBodyError) run_cmake(ListTransformApplyMissingBody) +run_cmake(ListTransformPredicateNonBool) +run_cmake(ListTransformPredicateCombined) +run_cmake(ListTransformPredicateMissingBody) +run_cmake(ListTransformPredicateLinkLibraries) run_cmake(BoundOperandOutsideBinding) function(run_cmake_build test)