GenEx: generalize bound operands to a frame and add $<_1>

Allow a binding operation to bind more than one operand at once, exposed as
$<_0>, $<_1>, ....  A single value remains the common case, but the upcoming
SORT COMPARATOR must bind the two elements being compared, so the binding
becomes an indexed frame and $<_1> is added.  Referencing an index the active
binding does not provide is reported as an error.

Issue: #27892
This commit is contained in:
Mickaël Germain
2026-06-30 08:19:41 -07:00
parent 4933a2de10
commit e335872625
7 changed files with 110 additions and 26 deletions
+20 -8
View File
@@ -2,8 +2,10 @@
file LICENSE.rst or https://cmake.org/licensing for details. */
#pragma once
#include <cstddef>
#include <string>
#include <utility>
#include <vector>
#include <cm/optional>
@@ -26,26 +28,36 @@ struct Context final
void SetCMP0189(cmPolicies::PolicyStatus cmp0189);
cmPolicies::PolicyStatus GetCMP0189() const;
void SetBoundOperands(std::vector<std::string> operands);
void SetBoundOperand(std::string value);
bool HasBoundOperand() const;
std::string const& GetBoundOperand() const;
std::size_t BoundOperandCount() const;
bool HasBoundOperand(std::size_t index = 0) const;
std::string const& GetBoundOperand(std::size_t index = 0) const;
private:
cm::optional<cmPolicies::PolicyStatus> CMP0189;
cm::optional<std::string> BoundOperand;
std::vector<std::string> BoundOperands;
};
inline void Context::SetBoundOperands(std::vector<std::string> operands)
{
this->BoundOperands = std::move(operands);
}
inline void Context::SetBoundOperand(std::string value)
{
this->BoundOperand = std::move(value);
this->SetBoundOperands({ std::move(value) });
}
inline bool Context::HasBoundOperand() const
inline std::size_t Context::BoundOperandCount() const
{
return this->BoundOperand.has_value();
return this->BoundOperands.size();
}
inline std::string const& Context::GetBoundOperand() const
inline bool Context::HasBoundOperand(std::size_t index) const
{
return *this->BoundOperand;
return index < this->BoundOperandCount();
}
inline std::string const& Context::GetBoundOperand(std::size_t index) const
{
return this->BoundOperands[index];
}
}
}
+45 -16
View File
@@ -107,17 +107,17 @@ 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(
// Re-evaluate the unevaluated <body> subtree of a binding operation with the
// given operands bound (accessible as $<_0>, $<_1>, ...). A fresh Evaluation
// is built from a copied, mutated Context so that nested binding operations
// can shadow the operands and restore them on exit.
static std::string EvaluateBodyWithBoundOperands(
cmGeneratorExpressionEvaluatorVector const& bodyExpr,
std::string const& operand, cm::GenEx::Evaluation* eval,
std::vector<std::string> operands, cm::GenEx::Evaluation* eval,
cmGeneratorExpressionDAGChecker* dagChecker)
{
cm::GenEx::Context elemContext = eval->Context; // copy
elemContext.SetBoundOperand(operand);
elemContext.SetBoundOperands(std::move(operands));
cm::GenEx::Evaluation elemEval(
elemContext, eval->Quiet, eval->HeadTarget, eval->CurrentTarget,
eval->EvaluateForBuildsystem, eval->Backtrace);
@@ -149,6 +149,15 @@ static std::string EvaluateBodyWithBoundOperand(
return result;
}
static std::string EvaluateBodyWithBoundOperand(
cmGeneratorExpressionEvaluatorVector const& bodyExpr,
std::string const& operand, cm::GenEx::Evaluation* eval,
cmGeneratorExpressionDAGChecker* dagChecker)
{
return EvaluateBodyWithBoundOperands(bodyExpr, { operand }, eval,
dagChecker);
}
// 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
@@ -220,9 +229,12 @@ static const struct OneNode : public cmGeneratorExpressionNode
}
} oneNode;
static const struct BoundOperandNode : public cmGeneratorExpressionNode
struct BoundOperandNode : public cmGeneratorExpressionNode
{
BoundOperandNode() {} // NOLINT(modernize-use-equals-default)
explicit BoundOperandNode(std::size_t index)
: Index(index)
{
}
int NumExpectedParameters() const override { return 0; }
@@ -231,15 +243,31 @@ static const struct BoundOperandNode : public cmGeneratorExpressionNode
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.");
if (!eval->Context.HasBoundOperand(this->Index)) {
std::size_t const count = eval->Context.BoundOperandCount();
if (count == 0) {
reportError(eval, content->GetOriginalExpression(),
cmStrCat("$<_", this->Index,
"> may only be used inside the body of a binding "
"operation."));
} else {
reportError(
eval, content->GetOriginalExpression(),
cmStrCat(
"$<_", this->Index,
"> is out of range for the current binding operation, which "
"binds only ",
count, " operand(s) (maximum $<_", count - 1, ">)."));
}
return std::string();
}
return eval->Context.GetBoundOperand();
return eval->Context.GetBoundOperand(this->Index);
}
} boundOperandNode;
std::size_t Index;
};
static BoundOperandNode const boundOperandNode0{ 0 };
static BoundOperandNode const boundOperandNode1{ 1 };
static const struct OneNode buildInterfaceNode;
@@ -6279,7 +6307,8 @@ cmGeneratorExpressionNode const* cmGeneratorExpressionNode::GetNode(
{ "PATH_EQUAL", &pathEqualNode },
{ "MAKE_C_IDENTIFIER", &makeCIdentifierNode },
{ "BOOL", &boolNode },
{ "_0", &boundOperandNode },
{ "_0", &boundOperandNode0 },
{ "_1", &boundOperandNode1 },
{ "IF", &ifNode },
{ "ANGLE-R", &angle_rNode },
{ "COMMA", &commaNode },
+38 -2
View File
@@ -2,6 +2,7 @@
file LICENSE.rst or https://cmake.org/licensing for details. */
#include <iostream>
#include <string>
#include <vector>
#include "cmGenExContext.h"
@@ -9,22 +10,57 @@ static bool testContextBinding()
{
cm::GenEx::Context ctx(nullptr, "Debug");
bool ok = true;
if (ctx.HasBoundOperand()) {
if (ctx.HasBoundOperand() || ctx.BoundOperandCount() != 0) {
std::cerr << "binding should start unset\n";
ok = false;
}
ctx.SetBoundOperand("net");
if (!ctx.HasBoundOperand() || ctx.GetBoundOperand() != "net") {
if (!ctx.HasBoundOperand() || ctx.BoundOperandCount() != 1 ||
ctx.GetBoundOperand() != "net") {
std::cerr << "binding did not round-trip\n";
ok = false;
}
return ok;
}
static bool testContextMultipleOperands()
{
cm::GenEx::Context ctx(nullptr, "Debug");
bool ok = true;
ctx.SetBoundOperands({ "a", "b" });
if (ctx.BoundOperandCount() != 2 || !ctx.HasBoundOperand(0) ||
!ctx.HasBoundOperand(1) || ctx.GetBoundOperand(0) != "a" ||
ctx.GetBoundOperand(1) != "b") {
std::cerr << "two-operand binding did not round-trip\n";
ok = false;
}
if (ctx.HasBoundOperand(2)) {
std::cerr << "index past the frame should be out of range\n";
ok = false;
}
// Re-binding replaces the whole frame, which the shadow/restore of nested
// bindings relies on.
ctx.SetBoundOperand("x");
if (ctx.BoundOperandCount() != 1 || ctx.HasBoundOperand(1) ||
ctx.GetBoundOperand(0) != "x") {
std::cerr << "re-binding did not replace the frame\n";
ok = false;
}
ctx.SetBoundOperands({});
if (ctx.BoundOperandCount() != 0 || ctx.HasBoundOperand(0)) {
std::cerr << "empty frame should clear the binding\n";
ok = false;
}
return ok;
}
int testGenExBoundOperand(int /*argc*/, char* /*argv*/[])
{
if (!testContextBinding()) {
return 1;
}
if (!testContextMultipleOperands()) {
return 1;
}
return 0;
}
@@ -0,0 +1 @@
is out of range for the current binding operation
@@ -0,0 +1,4 @@
# $<_1> requires a binary binding (e.g. SORT COMPARATOR); using it in a unary
# APPLY body, which binds only $<_0>, is an error.
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
CONTENT "$<LIST:TRANSFORM,a;b,APPLY,X$<_1>Y>")
@@ -72,6 +72,7 @@ run_cmake(ListTransformPredicateLinkLibraries)
run_cmake(ListFilterPredicateMissingBody)
run_cmake(ListFilterPredicateNonBool)
run_cmake(BoundOperandOutsideBinding)
run_cmake(BoundOperand1OutsideBinding)
function(run_cmake_build test)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test}-build)