Merge topic 'genex-list-transform-apply'

0f6e8ded1d GenEx: add $<LIST:TRANSFORM,...,APPLY,body> action
c0a0b7fdd9 GenEx: add bound-operand binding mechanism and $<_0>
caa51f5689 GenEx: factor TRANSFORM selector parsing and selection

Acked-by: Kitware Robot <kwrobot@kitware.com>
Tested-by: buildbot <buildbot@kitware.com>
Merge-request: !12203
This commit is contained in:
Brad King
2026-06-23 14:17:25 +00:00
committed by Kitware Robot
33 changed files with 702 additions and 128 deletions

View File

@@ -933,6 +933,23 @@ List Transformations
element instead of the beginning of each repeated search.
See policy :policy:`CMP0186`.
``APPLY``
Transform each selected element by evaluating an arbitrary generator
expression ``<body>`` once per element. Within ``<body>``, the bound
operand ``$<_0>`` expands to the current element. Unlike the
configure-time ``list(TRANSFORM ... APPLY <function>)`` command, the
genex form returns the body's value directly and has no side effects. A
list-valued body result expands into multiple elements, like any other
list-valued generator expression.
.. code-block:: cmake
$<LIST:TRANSFORM,list,APPLY,body[,SELECTOR]>
See ``$<_0>`` below for the bound operand.
.. versionadded:: 4.5
``SELECTOR`` determines which items of the list will be transformed.
Only one type of selector can be specified at a time. When given,
``SELECTOR`` must be one of the following:
@@ -1048,6 +1065,25 @@ List Ordering
$<LIST:SORT,list,CASE:SENSITIVE,COMPARE:STRING,ORDER:DESCENDING>
.. _GenEx Bound Operands:
Bound Operands
^^^^^^^^^^^^^^
.. genex:: $<_0>
.. versionadded:: 4.5
``$<_0>`` is the *bound operand* of the enclosing *binding operation*: a
generator expression that evaluates a ``<body>`` once for each value it
supplies, with ``$<_0>`` expanding to that value.
For example, ``$<LIST:TRANSFORM,...,APPLY,body>`` evaluates ``body`` once per
list element with ``$<_0>`` bound to the current element.
``$<_0>`` is only valid inside the body of a binding operation. Using it
anywhere else is an error.
Path Expressions
----------------

View File

@@ -0,0 +1,6 @@
genex-list-transform-apply
--------------------------
* The :genex:`LIST` generator expression's ``TRANSFORM`` operation gained an
``APPLY`` action that evaluates an arbitrary generator expression for each
element, with ``$<_0>`` referring to the current element.

View File

@@ -3,6 +3,7 @@
#pragma once
#include <string>
#include <utility>
#include <cm/optional>
@@ -25,9 +26,26 @@ struct Context final
void SetCMP0189(cmPolicies::PolicyStatus cmp0189);
cmPolicies::PolicyStatus GetCMP0189() const;
void SetBoundOperand(std::string value);
bool HasBoundOperand() const;
std::string const& GetBoundOperand() const;
private:
cm::optional<cmPolicies::PolicyStatus> CMP0189;
cm::optional<std::string> BoundOperand;
};
inline void Context::SetBoundOperand(std::string value)
{
this->BoundOperand = std::move(value);
}
inline bool Context::HasBoundOperand() const
{
return this->BoundOperand.has_value();
}
inline std::string const& Context::GetBoundOperand() const
{
return *this->BoundOperand;
}
}
}

View File

@@ -97,6 +97,12 @@ struct GeneratorExpressionContent : public cmGeneratorExpressionEvaluator
std::string GetOriginalExpression() const;
std::vector<cmGeneratorExpressionEvaluatorVector> const& GetParamChildren()
const
{
return this->ParamChildren;
}
~GeneratorExpressionContent() override;
private:

View File

@@ -107,6 +107,48 @@ std::string cmGeneratorExpressionNode::EvaluateDependentExpression(
return result;
}
// Re-evaluate the unevaluated <body> subtree of a binding operation with
// `$<_0>` bound to the given operand. A fresh Evaluation is built from a
// copied, mutated Context so that nested binding operations can shadow `$<_0>`
// and restore it on exit.
static std::string EvaluateBodyWithBoundOperand(
cmGeneratorExpressionEvaluatorVector const& bodyExpr,
std::string const& operand, cm::GenEx::Evaluation* eval,
cmGeneratorExpressionDAGChecker* dagChecker)
{
cm::GenEx::Context elemContext = eval->Context; // copy
elemContext.SetBoundOperand(operand);
cm::GenEx::Evaluation elemEval(
elemContext, eval->Quiet, eval->HeadTarget, eval->CurrentTarget,
eval->EvaluateForBuildsystem, eval->Backtrace);
std::string result;
for (auto const& pExprEval : bodyExpr) {
result += pExprEval->Evaluate(&elemEval, dagChecker);
if (elemEval.HadError) {
eval->HadError = true;
return std::string{};
}
}
eval->HadContextSensitiveCondition |= elemEval.HadContextSensitiveCondition;
eval->HadHeadSensitiveCondition |= elemEval.HadHeadSensitiveCondition;
eval->HadLinkLanguageSensitiveCondition |=
elemEval.HadLinkLanguageSensitiveCondition;
eval->DependTargets.insert(elemEval.DependTargets.begin(),
elemEval.DependTargets.end());
eval->AllTargets.insert(elemEval.AllTargets.begin(),
elemEval.AllTargets.end());
eval->SeenTargetProperties.insert(elemEval.SeenTargetProperties.begin(),
elemEval.SeenTargetProperties.end());
eval->SourceSensitiveTargets.insert(elemEval.SourceSensitiveTargets.begin(),
elemEval.SourceSensitiveTargets.end());
for (auto const& entry : elemEval.MaxLanguageStandard) {
eval->MaxLanguageStandard[entry.first].insert(entry.second.begin(),
entry.second.end());
}
return result;
}
static const struct ZeroNode : public cmGeneratorExpressionNode
{
ZeroNode() {} // NOLINT(modernize-use-equals-default)
@@ -141,6 +183,27 @@ static const struct OneNode : public cmGeneratorExpressionNode
}
} oneNode;
static const struct BoundOperandNode : public cmGeneratorExpressionNode
{
BoundOperandNode() {} // NOLINT(modernize-use-equals-default)
int NumExpectedParameters() const override { return 0; }
std::string Evaluate(
std::vector<std::string> const& /*parameters*/,
cm::GenEx::Evaluation* eval, GeneratorExpressionContent const* content,
cmGeneratorExpressionDAGChecker* /*dagChecker*/) const override
{
if (!eval->Context.HasBoundOperand()) {
reportError(eval, content->GetOriginalExpression(),
"$<_0> may only be used inside the body of a binding "
"operation.");
return std::string();
}
return eval->Context.GetBoundOperand();
}
} boundOperandNode;
static const struct OneNode buildInterfaceNode;
static const struct ZeroNode installInterfaceNode;
@@ -1858,6 +1921,122 @@ inline cmList GetList(std::string const& list)
{
return list.empty() ? cmList{} : cmList{ list, cmList::EmptyElements::Yes };
}
// Parse the optional trailing selector of a $<LIST:TRANSFORM,...> action
// (AT <i>... / FOR <start> <stop> [<step>] / REGEX <re>) into a
// cmList::TransformSelector. Returns nullptr (after reporting via `eval`) on
// a malformed selector; empty `tokens` yields a select-all selector.
std::unique_ptr<cmList::TransformSelector> ParseTransformSelector(
std::vector<std::string> const& tokens, cm::GenEx::Evaluation* eval,
GeneratorExpressionContent const* content)
{
static std::string const REGEX{ "REGEX" };
static std::string const AT{ "AT" };
static std::string const FOR{ "FOR" };
std::unique_ptr<cmList::TransformSelector> selector;
std::size_t i = 0;
while (i < tokens.size()) {
std::string const& tok = tokens[i];
if ((tok == REGEX || tok == AT || tok == FOR) && selector) {
reportError(
eval, content->GetOriginalExpression(),
cmStrCat("sub-command TRANSFORM, selector already specified (",
selector->GetTag(), ")."));
return nullptr;
}
if (tok == REGEX) {
if (i + 1 >= tokens.size()) {
reportError(eval, content->GetOriginalExpression(),
"sub-command TRANSFORM, selector REGEX expects "
"'regular expression' argument.");
return nullptr;
}
selector =
cmList::TransformSelector::New<cmList::TransformSelector::REGEX>(
tokens[i + 1]);
i += 2;
continue;
}
if (tok == AT) {
++i;
std::vector<cmList::index_type> indexes;
for (; i < tokens.size(); ++i) {
cmList indexList{ tokens[i] };
for (auto const& index : indexList) {
cmList::index_type value;
if (!GetNumericArgument(index, value)) {
reportError(eval, content->GetOriginalExpression(),
cmStrCat("sub-command TRANSFORM, selector AT: '",
index, "': unexpected argument."));
return nullptr;
}
indexes.push_back(value);
}
}
if (indexes.empty()) {
reportError(eval, content->GetOriginalExpression(),
"sub-command TRANSFORM, selector AT expects at least one "
"numeric value.");
return nullptr;
}
selector = cmList::TransformSelector::New<cmList::TransformSelector::AT>(
std::move(indexes));
continue;
}
if (tok == FOR) {
if (i + 2 >= tokens.size()) {
reportError(eval, content->GetOriginalExpression(),
"sub-command TRANSFORM, selector FOR expects, at least, "
"two arguments.");
return nullptr;
}
cmList::index_type start = 0;
cmList::index_type stop = 0;
cmList::index_type step = 1;
if (!GetNumericArgument(tokens[i + 1], start) ||
!GetNumericArgument(tokens[i + 2], stop)) {
reportError(eval, content->GetOriginalExpression(),
"sub-command TRANSFORM, selector FOR expects, at least, "
"two numeric values.");
return nullptr;
}
i += 3;
if (i < tokens.size()) {
if (!GetNumericArgument(tokens[i], step)) {
step = -1;
}
++i;
}
if (step <= 0) {
reportError(eval, content->GetOriginalExpression(),
"sub-command TRANSFORM, selector FOR expects positive "
"numeric value for <step>.");
return nullptr;
}
selector =
cmList::TransformSelector::New<cmList::TransformSelector::FOR>(
{ start, stop, step });
continue;
}
std::vector<std::string> const rest(tokens.begin() + i, tokens.end());
reportError(eval, content->GetOriginalExpression(),
cmStrCat("sub-command TRANSFORM, '", cmJoin(rest, ", "),
"': unexpected argument(s)."));
return nullptr;
}
if (!selector) {
selector = cmList::TransformSelector::New();
}
return selector;
}
}
static const struct ListNode : public cmGeneratorExpressionNode
@@ -1868,11 +2047,74 @@ static const struct ListNode : public cmGeneratorExpressionNode
bool AcceptsArbitraryContentParameter() const override { return true; }
bool ShouldEvaluateNextParameter(std::vector<std::string> const& parameters,
std::string&) const override
{
// Skip the APPLY <body> (4th parameter) so $<_0> is not evaluated unbound;
// selector args (5th+) evaluate normally.
return !(parameters.size() == 3 && parameters[0] == "TRANSFORM" &&
parameters[2] == "APPLY");
}
std::string Evaluate(
std::vector<std::string> const& parameters, cm::GenEx::Evaluation* eval,
GeneratorExpressionContent const* content,
cmGeneratorExpressionDAGChecker* /*dagChecker*/) const override
cmGeneratorExpressionDAGChecker* dagChecker) const override
{
if (parameters.size() >= 3 && parameters[0] == "TRANSFORM" &&
parameters[2] == "APPLY") {
if (parameters.size() < 4) {
reportError(
eval, content->GetOriginalExpression(),
"sub-command TRANSFORM, action APPLY expects a <body> argument.");
return std::string();
}
cmList list = GetList(parameters[1]);
if (list.empty()) {
return std::string{};
}
cmGeneratorExpressionEvaluatorVector const& bodyExpr =
content->GetParamChildren()[3];
std::vector<std::string> const selectorTokens(parameters.begin() + 4,
parameters.end());
std::unique_ptr<cmList::TransformSelector> selector =
ParseTransformSelector(selectorTokens, eval, content);
if (!selector) {
return std::string();
}
// selector->Makefile is left unset: the REGEX/AT/FOR selectors never
// consult it, and the only one that does (list()'s PREDICATE) cannot
// reach here.
std::vector<bool> selected;
try {
selected = list.GetTransformSelection(*selector);
} catch (cmList::transform_error& e) {
reportError(eval, content->GetOriginalExpression(), e.what());
return std::string();
}
std::vector<std::string> out;
out.reserve(list.size());
std::size_t i = 0;
for (auto const& element : list) {
if (i < selected.size() && selected[i]) {
out.push_back(
EvaluateBodyWithBoundOperand(bodyExpr, element, eval, dagChecker));
if (eval->HadError) {
return std::string();
}
} else {
out.push_back(element);
}
++i;
}
// Join per-element results with ';'; keep empty elements so a body that
// yields "" is not dropped.
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*,
@@ -2177,137 +2419,14 @@ static const struct ListNode : public cmGeneratorExpressionNode
args.advance(descriptor->Arity);
}
std::string const REGEX{ "REGEX" };
std::string const AT{ "AT" };
std::string const FOR{ "FOR" };
std::unique_ptr<cmList::TransformSelector> selector;
try {
// handle optional arguments
while (!args.empty()) {
if ((args.front() == REGEX || args.front() == AT ||
args.front() == FOR) &&
selector) {
reportError(ev, cnt->GetOriginalExpression(),
cmStrCat("sub-command TRANSFORM, selector "
"already specified (",
selector->GetTag(), ")."));
return std::string{};
}
// REGEX selector
if (args.front() == REGEX) {
if (args.advance(1).empty()) {
reportError(
ev, cnt->GetOriginalExpression(),
"sub-command TRANSFORM, selector REGEX expects "
"'regular expression' argument.");
return std::string{};
}
selector = cmList::TransformSelector::New<
cmList::TransformSelector::REGEX>(args.front());
args.advance(1);
continue;
}
// AT selector
if (args.front() == AT) {
args.advance(1);
// get all specified indexes
std::vector<cmList::index_type> indexes;
while (!args.empty()) {
cmList indexList{ args.front() };
for (auto const& index : indexList) {
cmList::index_type value;
if (!GetNumericArgument(index, value)) {
// this is not a number, stop processing
reportError(
ev, cnt->GetOriginalExpression(),
cmStrCat("sub-command TRANSFORM, selector AT: '",
index, "': unexpected argument."));
return std::string{};
}
indexes.push_back(value);
}
args.advance(1);
}
if (indexes.empty()) {
reportError(ev, cnt->GetOriginalExpression(),
"sub-command TRANSFORM, selector AT "
"expects at least one "
"numeric value.");
return std::string{};
}
selector = cmList::TransformSelector::New<
cmList::TransformSelector::AT>(std::move(indexes));
continue;
}
// FOR selector
if (args.front() == FOR) {
if (args.advance(1).size() < 2) {
reportError(ev, cnt->GetOriginalExpression(),
"sub-command TRANSFORM, selector FOR "
"expects, at least,"
" two arguments.");
return std::string{};
}
cmList::index_type start = 0;
cmList::index_type stop = 0;
cmList::index_type step = 1;
bool valid = false;
if (GetNumericArgument(args.front(), start) &&
GetNumericArgument(args.advance(1).front(), stop)) {
valid = true;
}
if (!valid) {
reportError(
ev, cnt->GetOriginalExpression(),
"sub-command TRANSFORM, selector FOR expects, "
"at least, two numeric values.");
return std::string{};
}
// try to read a third numeric value for step
if (!args.advance(1).empty()) {
if (!GetNumericArgument(args.front(), step)) {
// this is not a number
step = -1;
}
args.advance(1);
}
if (step <= 0) {
reportError(
ev, cnt->GetOriginalExpression(),
"sub-command TRANSFORM, selector FOR expects "
"positive numeric value for <step>.");
return std::string{};
}
selector = cmList::TransformSelector::New<
cmList::TransformSelector::FOR>({ start, stop, step });
continue;
}
reportError(ev, cnt->GetOriginalExpression(),
cmStrCat("sub-command TRANSFORM, '",
cmJoin(args, ", "),
"': unexpected argument(s)."));
return std::string{};
}
std::vector<std::string> const tokens(args.begin(),
args.end());
selector = ParseTransformSelector(tokens, ev, cnt);
if (!selector) {
selector = cmList::TransformSelector::New();
return std::string{};
}
selector->Makefile = ev->Context.LG->GetMakefile();
@@ -5939,6 +6058,7 @@ cmGeneratorExpressionNode const* cmGeneratorExpressionNode::GetNode(
{ "PATH_EQUAL", &pathEqualNode },
{ "MAKE_C_IDENTIFIER", &makeCIdentifierNode },
{ "BOOL", &boolNode },
{ "_0", &boundOperandNode },
{ "IF", &ifNode },
{ "ANGLE-R", &angle_rNode },
{ "COMMA", &commaNode },

View File

@@ -437,6 +437,17 @@ public:
std::transform(list.begin(), list.end(), list.begin(), transform);
}
// Return, for each element, whether the selector selects it via InSelection.
virtual std::vector<bool> Selection(cmList::container_type const& list)
{
std::vector<bool> selected;
selected.reserve(list.size());
for (auto const& value : list) {
selected.push_back(this->InSelection(value));
}
return selected;
}
protected:
TransformSelector(std::string&& tag)
: Tag(std::move(tag))
@@ -516,6 +527,19 @@ public:
}
}
// Select the computed Indexes; Validate throws transform_error on an
// out-of-range index.
std::vector<bool> Selection(cmList::container_type const& list) override
{
this->Validate(list.size());
std::vector<bool> selected(list.size(), false);
for (auto index : this->Indexes) {
selected[index] = true;
}
return selected;
}
protected:
TransformSelectorIndexes(std::string&& tag)
: TransformSelector(std::move(tag))
@@ -1148,6 +1172,12 @@ cmList& cmList::transform(TransformAction action, std::string const& arg,
return *this;
}
std::vector<bool> cmList::GetTransformSelection(
cmList::TransformSelector& selector) const
{
return static_cast<::TransformSelector&>(selector).Selection(this->Values);
}
std::string& cmList::append(std::string& list, std::string&& value)
{
if (list.empty()) {

View File

@@ -975,6 +975,11 @@ public:
cmMakefile& makefile,
std::unique_ptr<TransformSelector> = {});
// Return, for each element of this list, whether `selector` selects it.
// Throws transform_error on a malformed selector (e.g. an out-of-range
// index), like transform().
std::vector<bool> GetTransformSelection(TransformSelector& selector) const;
std::string join(cm::string_view glue) const
{
return cmList::Join(this->Values, glue);

View File

@@ -14,6 +14,8 @@ set(CMakeLib_TESTS
testDocumentationFormatter.cxx
testGccDepfileReader.cxx
testGeneratedFileStream.cxx
testGenExBoundOperand.cxx
testGenExTransformApply.cxx
testJSONHelpers.cxx
testRST.cxx
testRange.cxx

View File

@@ -0,0 +1,30 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#include <iostream>
#include <string>
#include "cmGenExContext.h"
static bool testContextBinding()
{
cm::GenEx::Context ctx(nullptr, "Debug");
bool ok = true;
if (ctx.HasBoundOperand()) {
std::cerr << "binding should start unset\n";
ok = false;
}
ctx.SetBoundOperand("net");
if (!ctx.HasBoundOperand() || ctx.GetBoundOperand() != "net") {
std::cerr << "binding did not round-trip\n";
ok = false;
}
return ok;
}
int testGenExBoundOperand(int /*argc*/, char* /*argv*/[])
{
if (!testContextBinding()) {
return 1;
}
return 0;
}

View File

@@ -0,0 +1,174 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#include <iostream>
#include <string>
#include <cm/memory>
#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<cmGlobalGenerator> GG;
std::unique_ptr<cmMakefile> MF;
std::unique_ptr<cmLocalGenerator> LG;
GenExFixture()
{
this->GG = cm::make_unique<cmGlobalGenerator>(&this->CMake);
cmStateSnapshot snapshot = this->CMake.GetCurrentSnapshot();
snapshot.GetDirectory().SetCurrentBinary(".");
snapshot.GetDirectory().SetCurrentSource(".");
this->MF = cm::make_unique<cmMakefile>(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");
}
};
}
static bool testApplyBasic()
{
GenExFixture fx;
return fx.Eval("$<LIST:TRANSFORM,net;audio,APPLY,$<UPPER_CASE:$<_0>>>") ==
"NET;AUDIO";
}
static bool testApplyConstantBody()
{
// The body need not reference the operand at all.
GenExFixture fx;
return fx.Eval("$<LIST:TRANSFORM,a;b;c,APPLY,X>") == "X;X;X";
}
static bool testApplyOperandUsedTwice()
{
// The operand may be substituted more than once in a single body.
GenExFixture fx;
return fx.Eval("$<LIST:TRANSFORM,a;b,APPLY,$<_0>$<_0>>") == "aa;bb";
}
static bool testApplyFlatMapExpands()
{
GenExFixture fx;
// A list-valued body expands: a -> "x;y", b -> "z" => 3 items.
std::string out =
fx.Eval("$<LIST:TRANSFORM,a;b,APPLY,$<IF:$<STREQUAL:$<_0>,a>,x;y,z>>");
std::string len = fx.Eval("$<LIST:LENGTH,$<LIST:TRANSFORM,a;b,APPLY,"
"$<IF:$<STREQUAL:$<_0>,a>,x;y,z>>>");
return out == "x;y;z" && len == "3";
}
static bool testApplySelector()
{
GenExFixture fx;
return fx.Eval(
"$<LIST:TRANSFORM,a;b;c;d,APPLY,$<UPPER_CASE:$<_0>>,AT,0,2>") ==
"A;b;C;d";
}
static bool testApplyNestedShadowing()
{
GenExFixture fx;
return fx.Eval(
"$<LIST:TRANSFORM,a;b,APPLY,"
"$<LIST:TRANSFORM,$<_0>1;$<_0>2,APPLY,$<UPPER_CASE:$<_0>>>>") ==
"A1;A2;B1;B2";
}
static bool testApplyEmptyList()
{
GenExFixture fx;
// An empty list produces an empty string without error (matches canned
// TRANSFORM behavior).
std::string got = fx.Eval("$<LIST:TRANSFORM,,APPLY,x$<_0>y>");
if (!got.empty()) {
std::cerr << "testApplyEmptyList: expected empty string, got '" << got
<< "'\n";
return false;
}
return true;
}
static bool testApplyEmptyElement()
{
GenExFixture fx;
// Every element is selected, including the leading empty one, so the body
// wraps each: "" -> "xy", "b" -> "xby".
return fx.Eval("$<LIST:TRANSFORM,;b,APPLY,x$<_0>y>") == "xy;xby";
}
static bool testApplyEmptyPassthrough()
{
GenExFixture fx;
// The list ";b" has two elements: "" (index 0, unselected) and "b"
// (index 1, selected). The unselected empty element must pass through
// verbatim, not be silently dropped.
std::string got = fx.Eval("$<LIST:TRANSFORM,;b,APPLY,X$<_0>Y,AT,1>");
if (got != ";XbY") {
std::cerr << "testApplyEmptyPassthrough: expected ';XbY', got '" << got
<< "'\n";
return false;
}
return true;
}
static bool testApplyEmptyResult()
{
GenExFixture fx;
// Body yields "" for "a" and "z" for "b". The empty element produced by
// the body must not be dropped.
std::string got =
fx.Eval("$<LIST:TRANSFORM,a;b,APPLY,$<IF:$<STREQUAL:$<_0>,a>,,z>>");
if (got != ";z") {
std::cerr << "testApplyEmptyResult: expected ';z', got '" << got << "'\n";
return false;
}
return true;
}
int testGenExTransformApply(int /*argc*/, char* /*argv*/[])
{
if (!testApplyBasic()) {
return 1;
}
if (!testApplyConstantBody()) {
return 1;
}
if (!testApplyOperandUsedTwice()) {
return 1;
}
if (!testApplyFlatMapExpands()) {
return 1;
}
if (!testApplySelector()) {
return 1;
}
if (!testApplyNestedShadowing()) {
return 1;
}
if (!testApplyEmptyList()) {
return 1;
}
if (!testApplyEmptyElement()) {
return 1;
}
if (!testApplyEmptyPassthrough()) {
return 1;
}
if (!testApplyEmptyResult()) {
return 1;
}
return 0;
}

View File

@@ -0,0 +1 @@
\$<_0> may only be used inside the body of a binding operation

View File

@@ -0,0 +1 @@
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/bad.txt" CONTENT "$<_0>")

View File

@@ -0,0 +1,5 @@
file(READ "${RunCMake_TEST_BINARY_DIR}/out.txt" actual)
string(STRIP "${actual}" actual)
if(NOT actual STREQUAL "NET;AUDIO;VIDEO")
set(RunCMake_TEST_FAILED "unexpected APPLY output: [${actual}]")
endif()

View File

@@ -0,0 +1,3 @@
set(input "net;audio;video")
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/out.txt"
CONTENT "$<LIST:TRANSFORM,${input},APPLY,$<UPPER_CASE:$<_0>>>\n")

View File

@@ -0,0 +1 @@
sub-command TRANSFORM, selector AT expects at least one numeric value

View File

@@ -0,0 +1,4 @@
# 'AT' with no following index is a malformed selector; APPLY must report the
# same diagnostic the canned TRANSFORM actions do.
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
CONTENT "$<LIST:TRANSFORM,a;b,APPLY,$<_0>,AT>")

View File

@@ -0,0 +1 @@
index: 9 out of range

View File

@@ -0,0 +1,5 @@
# A body that errors for an element must fail the whole expression, not be
# silently dropped. $<LIST:GET,...,9> is out of range for a single-element
# operand.
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
CONTENT "$<LIST:TRANSFORM,a;b,APPLY,$<LIST:GET,$<_0>,9>>")

View File

@@ -0,0 +1,6 @@
/* A real symbol so the static archive is non-empty (some ranlib versions warn
on an archive with no global symbols). */
int cmListTransformApplyProvider(void)
{
return 0;
}

View File

@@ -0,0 +1,18 @@
enable_language(C)
# `provider` is EXCLUDE_FROM_ALL, so it builds only when another target declares
# a build-order dependency on it.
add_library(provider STATIC ListTransformApplyDependTarget.c)
set_property(TARGET provider PROPERTY EXCLUDE_FROM_ALL 1)
# `consumer`'s only reference to `provider` is the $<TARGET_FILE:$<_0>> in the
# APPLY body. CMake derives the build-order dependency from that reference only
# if APPLY propagates the target back out (DependTargets). If that regresses,
# `provider` is never built and the copy below fails for lack of the archive, so
# a green build of `consumer` alone is the regression guard.
add_custom_target(consumer
COMMAND "${CMAKE_COMMAND}" -E copy
"$<LIST:TRANSFORM,provider,APPLY,$<TARGET_FILE:$<_0>>>"
"${CMAKE_CURRENT_BINARY_DIR}/copied.out"
VERBATIM
)

View File

@@ -0,0 +1,12 @@
file(READ "${RunCMake_TEST_BINARY_DIR}/ll.txt" actual)
file(READ "${RunCMake_TEST_BINARY_DIR}/expected.txt" expected)
string(STRIP "${actual}" actual)
string(STRIP "${expected}" expected)
# APPLY over app's LINK_LIBRARIES must equal the explicit per-library
# $<TARGET_FILE_NAME> expansion exactly.
if(NOT actual STREQUAL expected)
set(RunCMake_TEST_FAILED
"APPLY-over-LINK_LIBRARIES output does not match the explicit expansion:\n"
" actual: [${actual}]\n"
" expected: [${expected}]")
endif()

View File

@@ -0,0 +1,30 @@
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, linked PUBLIC so each library's dependencies propagate
# through INTERFACE_LINK_LIBRARIES:
# app -> { net, audio }; net -> { ssl, zlib }; audio -> codec
add_library(ssl STATIC empty.c)
add_library(zlib STATIC empty.c)
add_library(codec STATIC empty.c)
add_library(net STATIC empty.c)
target_link_libraries(net PUBLIC ssl zlib)
add_library(audio STATIC empty.c)
target_link_libraries(audio PUBLIC codec)
add_library(app STATIC empty.c)
target_link_libraries(app PRIVATE net audio)
# app's LINK_LIBRARIES now evaluates to the transitive closure
# (net;audio;ssl;zlib;codec); map each linked library to its on-disk file name.
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/ll.txt"
CONTENT "$<LIST:TRANSFORM,$<TARGET_PROPERTY:app,LINK_LIBRARIES>,APPLY,$<TARGET_FILE_NAME:$<_0>>>\n")
# Exact, platform-independent reference: the explicit per-library expansion in
# the transitive-closure order. The APPLY output must be byte-identical.
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/expected.txt"
CONTENT "$<TARGET_FILE_NAME:net>;$<TARGET_FILE_NAME:audio>;$<TARGET_FILE_NAME:ssl>;$<TARGET_FILE_NAME:zlib>;$<TARGET_FILE_NAME:codec>\n")

View File

@@ -0,0 +1 @@
sub-command TRANSFORM, action APPLY expects a <body> argument

View File

@@ -0,0 +1,3 @@
# APPLY with no body argument is an error.
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
CONTENT "$<LIST:TRANSFORM,a;b,APPLY>")

View File

@@ -0,0 +1,12 @@
file(READ "${RunCMake_TEST_BINARY_DIR}/nested.txt" actual)
file(READ "${RunCMake_TEST_BINARY_DIR}/expected.txt" expected)
string(STRIP "${actual}" actual)
string(STRIP "${expected}" expected)
# The nested APPLY (outer binds each target, inner uppercases each include dir)
# must equal the explicit per-target expansion exactly.
if(NOT actual STREQUAL expected)
set(RunCMake_TEST_FAILED
"nested APPLY output does not match the explicit expansion:\n"
" actual: [${actual}]\n"
" expected: [${expected}]")
endif()

View File

@@ -0,0 +1,17 @@
add_library(libA INTERFACE)
target_include_directories(libA INTERFACE /a/one /a/two)
add_library(libB INTERFACE)
target_include_directories(libB INTERFACE /b/one)
# Nested APPLY: the outer APPLY runs over targets; its body is an APPLY over
# each target's include directories that uppercases them. The outer $<_0> (a
# target) feeds the inner list arg $<TARGET_PROPERTY:...>; the inner $<_0> (a
# directory) feeds $<UPPER_CASE> under the inner binding, while the outer
# binding stays intact.
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/nested.txt"
CONTENT "$<LIST:TRANSFORM,libA;libB,APPLY,$<LIST:TRANSFORM,$<TARGET_PROPERTY:$<_0>,INTERFACE_INCLUDE_DIRECTORIES>,APPLY,$<UPPER_CASE:$<_0>>>>\n")
# Exact reference: the same result without the inner APPLY (uppercase each
# target's include dirs directly). The nested APPLY must match it.
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/expected.txt"
CONTENT "$<UPPER_CASE:$<TARGET_PROPERTY:libA,INTERFACE_INCLUDE_DIRECTORIES>>;$<UPPER_CASE:$<TARGET_PROPERTY:libB,INTERFACE_INCLUDE_DIRECTORIES>>\n")

View File

@@ -0,0 +1,7 @@
file(READ "${RunCMake_TEST_BINARY_DIR}/tp.txt" actual)
string(STRIP "${actual}" actual)
# flatMap: libA -> /a/inc (1 dir), libB -> /b/inc1;/b/inc2 (2 dirs) => 3 items.
if(NOT actual STREQUAL "/a/inc;/b/inc1;/b/inc2")
set(RunCMake_TEST_FAILED
"unexpected APPLY target-property output: [${actual}]")
endif()

View File

@@ -0,0 +1,6 @@
add_library(libA INTERFACE)
target_include_directories(libA INTERFACE /a/inc)
add_library(libB INTERFACE)
target_include_directories(libB INTERFACE /b/inc1 /b/inc2)
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/tp.txt"
CONTENT "$<LIST:TRANSFORM,libA;libB,APPLY,$<TARGET_PROPERTY:$<_0>,INTERFACE_INCLUDE_DIRECTORIES>>\n")

View File

@@ -57,6 +57,14 @@ run_cmake(FILTER-InvalidOperator)
run_cmake(FILTER-Exclude)
run_cmake(FILTER-Include)
run_cmake(LIST-edgecases)
run_cmake(ListTransformApply)
run_cmake(ListTransformApplyTargetProperty)
run_cmake(ListTransformApplyLinkLibraries)
run_cmake(ListTransformApplyNested)
run_cmake(ListTransformApplyBadSelector)
run_cmake(ListTransformApplyBodyError)
run_cmake(ListTransformApplyMissingBody)
run_cmake(BoundOperandOutsideBinding)
function(run_cmake_build test)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test}-build)
@@ -150,6 +158,8 @@ unset(RunCMake_TEST_OPTIONS)
run_cmake_build_target(COMPILE_ONLY-custom-target-deps consumer)
run_cmake_build_target(ListTransformApplyDependTarget consumer)
run_cmake(CMP0199-WARN)
run_cmake_build(CMP0199-OLD)
run_cmake_build(CMP0199-NEW)