GenEx: add bound-operand binding mechanism and $<_0>

Introduce "binding operations": generator expressions that evaluate a <body>
once for each value they supply, with $<_0> expanding to that value.  This is
the foundation the $<LIST:TRANSFORM,...,APPLY> action and the predicate
selectors build on, letting a <body> refer to the element being processed.

Using $<_0> outside a binding operation is reported as an error rather than
silently expanding to nothing.

Issue: #27892
This commit is contained in:
Mickaël Germain
2026-06-20 12:32:30 -07:00
parent caa51f5689
commit c0a0b7fdd9
10 changed files with 97 additions and 0 deletions
@@ -1048,6 +1048,22 @@ 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.
``$<_0>`` is only valid inside the body of a binding operation. Using it
anywhere else is an error.
Path Expressions
----------------
+18
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;
}
}
}
+6
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:
+22
View File
@@ -141,6 +141,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;
@@ -5932,6 +5953,7 @@ cmGeneratorExpressionNode const* cmGeneratorExpressionNode::GetNode(
{ "PATH_EQUAL", &pathEqualNode },
{ "MAKE_C_IDENTIFIER", &makeCIdentifierNode },
{ "BOOL", &boolNode },
{ "_0", &boundOperandNode },
{ "IF", &ifNode },
{ "ANGLE-R", &angle_rNode },
{ "COMMA", &commaNode },
+1
View File
@@ -14,6 +14,7 @@ set(CMakeLib_TESTS
testDocumentationFormatter.cxx
testGccDepfileReader.cxx
testGeneratedFileStream.cxx
testGenExBoundOperand.cxx
testJSONHelpers.cxx
testRST.cxx
testRange.cxx
+30
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;
}
@@ -0,0 +1 @@
\$<_0> may only be used inside the body of a binding operation
@@ -0,0 +1 @@
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/bad.txt" CONTENT "$<_0>")
@@ -57,6 +57,7 @@ run_cmake(FILTER-InvalidOperator)
run_cmake(FILTER-Exclude)
run_cmake(FILTER-Include)
run_cmake(LIST-edgecases)
run_cmake(BoundOperandOutsideBinding)
function(run_cmake_build test)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test}-build)