Merge topic 'genex-list-sort-comparator'

4d86ef5e86 GenEx: add $<LIST:SORT,...,COMPARATOR,body> comparator
c418a3dc85 GenEx: factor out reusable SORT comparator and option-parsing helpers
e335872625 GenEx: generalize bound operands to a frame and add $<_1>

Acked-by: Kitware Robot <kwrobot@kitware.com>
Tested-by: buildbot <buildbot@kitware.com>
Merge-request: !12228
This commit is contained in:
Brad King
2026-07-01 09:46:35 -04:00
committed by Kitware Robot
30 changed files with 570 additions and 117 deletions
@@ -1098,6 +1098,24 @@ List Ordering
$<LIST:SORT,list,CASE:SENSITIVE,COMPARE:STRING,ORDER:DESCENDING>
.. versionadded:: 4.5
A ``COMPARATOR`` option sorts using a generator-expression ``body`` instead
of a built-in ordering:
.. code-block:: cmake
$<LIST:SORT,list,COMPARATOR,body[,ORDER:ASCENDING|DESCENDING][,CASE:SENSITIVE|INSENSITIVE]>
``body`` is evaluated once per comparison with the two items being compared
bound to :genex:`$<_0>` and :genex:`$<_1>`. It must evaluate to exactly
``0`` or ``1``; ``1`` means :genex:`$<_0>` sorts before :genex:`$<_1>`.
``COMPARATOR`` is incompatible with ``COMPARE:``. ``ORDER:DESCENDING``
reverses the comparator and ``CASE:INSENSITIVE`` case-folds the values the
body sees, both as in the configure-time :command:`list(SORT)`. The body
must induce a `strict weak ordering
<https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings>`_.
.. _GenEx Bound Operands:
Bound Operands
@@ -1117,6 +1135,20 @@ Bound Operands
``$<_0>`` is only valid inside the body of a binding operation. Using it
anywhere else is an error.
.. genex:: $<_1>
.. versionadded:: 4.5
The second *bound operand* of a *binding operation* that binds at least two
operands, expanding to the second supplied value.
For example, :genex:`$<LIST:SORT,...,COMPARATOR,body>` evaluates ``body``
once per comparison with :genex:`$<_0>` and ``$<_1>`` bound to the two items
being compared.
``$<_1>`` is only valid inside the body of a binding operation that binds at
least two operands. Using it anywhere else is an error.
Path Expressions
----------------
@@ -0,0 +1,7 @@
genex-list-sort-comparator
--------------------------
* The :genex:`LIST` generator expression's ``SORT`` operation gained a
``COMPARATOR`` option that orders items using an arbitrary generator
expression evaluated once per comparison, with ``$<_0>`` and ``$<_1>``
referring to the two items being compared.
+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];
}
}
}
+242 -102
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;
@@ -2102,6 +2130,178 @@ std::string EvaluateTransformPredicate(
}
}
enum class SortOptionResult
{
NotRecognized, // `arg` is not a SORT option keyword
Parsed, // recognized and applied to `sortConfig`
Error, // recognized but malformed or duplicate (already reported)
};
// Parse one $<LIST:SORT> colon-option (COMPARE:/CASE:/ORDER:) into sortConfig.
SortOptionResult ParseSortOption(std::string const& arg,
cmList::SortConfiguration& sortConfig,
cm::GenEx::Evaluation* eval,
GeneratorExpressionContent const* content)
{
using SortConfig = cmList::SortConfiguration;
auto const COMPARE = "COMPARE:"_s;
auto const CASE = "CASE:"_s;
auto const ORDER = "ORDER:"_s;
if (cmHasPrefix(arg, COMPARE)) {
if (sortConfig.Compare != SortConfig::CompareMethod::DEFAULT) {
reportError(eval, content->GetOriginalExpression(),
"sub-command SORT, COMPARE option has been specified "
"multiple times.");
return SortOptionResult::Error;
}
auto option = cm::string_view{ arg.c_str() + COMPARE.length() };
if (option == "STRING"_s) {
sortConfig.Compare = SortConfig::CompareMethod::STRING;
} else if (option == "FILE_BASENAME"_s) {
sortConfig.Compare = SortConfig::CompareMethod::FILE_BASENAME;
} else if (option == "NATURAL"_s) {
sortConfig.Compare = SortConfig::CompareMethod::NATURAL;
} else {
reportError(eval, content->GetOriginalExpression(),
cmStrCat("sub-command SORT, an invalid COMPARE option has "
"been specified: \"",
option, "\"."));
return SortOptionResult::Error;
}
return SortOptionResult::Parsed;
}
if (cmHasPrefix(arg, CASE)) {
if (sortConfig.Case != SortConfig::CaseSensitivity::DEFAULT) {
reportError(eval, content->GetOriginalExpression(),
"sub-command SORT, CASE option has been specified multiple "
"times.");
return SortOptionResult::Error;
}
auto option = cm::string_view{ arg.c_str() + CASE.length() };
if (option == "SENSITIVE"_s) {
sortConfig.Case = SortConfig::CaseSensitivity::SENSITIVE;
} else if (option == "INSENSITIVE"_s) {
sortConfig.Case = SortConfig::CaseSensitivity::INSENSITIVE;
} else {
reportError(eval, content->GetOriginalExpression(),
cmStrCat("sub-command SORT, an invalid CASE option has been "
"specified: \"",
option, "\"."));
return SortOptionResult::Error;
}
return SortOptionResult::Parsed;
}
if (cmHasPrefix(arg, ORDER)) {
if (sortConfig.Order != SortConfig::OrderMode::DEFAULT) {
reportError(eval, content->GetOriginalExpression(),
"sub-command SORT, ORDER option has been specified multiple "
"times.");
return SortOptionResult::Error;
}
auto option = cm::string_view{ arg.c_str() + ORDER.length() };
if (option == "ASCENDING"_s) {
sortConfig.Order = SortConfig::OrderMode::ASCENDING;
} else if (option == "DESCENDING"_s) {
sortConfig.Order = SortConfig::OrderMode::DESCENDING;
} else {
reportError(
eval, content->GetOriginalExpression(),
cmStrCat("sub-command SORT, an invalid ORDER option has been "
"specified: \"",
option, "\"."));
return SortOptionResult::Error;
}
return SortOptionResult::Parsed;
}
return SortOptionResult::NotRecognized;
}
// $<LIST:SORT,...,COMPARATOR,body>: sort with a per-comparison genex body, the
// two elements bound to $<_0> and $<_1>; body must yield "0" or "1".
std::string EvaluateSortComparator(std::vector<std::string> const& parameters,
std::size_t comparatorIndex,
cm::GenEx::Evaluation* eval,
GeneratorExpressionContent const* content,
cmGeneratorExpressionDAGChecker* dagChecker)
{
if (comparatorIndex + 1 >= parameters.size()) {
reportError(eval, content->GetOriginalExpression(),
"sub-command SORT, COMPARATOR expects a <body> argument.");
return std::string();
}
cmGeneratorExpressionEvaluatorVector const& bodyExpr =
content->GetParamChildren()[comparatorIndex + 1];
using SortConfig = cmList::SortConfiguration;
SortConfig sortConfig;
sortConfig.Compare = SortConfig::CompareMethod::COMPARATOR;
for (std::size_t i = 2; i < parameters.size(); ++i) {
if (i == comparatorIndex || i == comparatorIndex + 1) {
continue; // COMPARATOR keyword + its (empty) body slot
}
std::string const& arg = parameters[i];
// COMPARATOR defines the ordering, so reject COMPARE:; CASE:/ORDER: are
// accepted as in list(SORT ... COMPARATOR) (CASE: folds the body
// operands).
if (cmHasPrefix(arg, "COMPARE:"_s)) {
reportError(eval, content->GetOriginalExpression(),
"sub-command SORT, option \"COMPARE\" is incompatible with "
"\"COMPARATOR\".");
return std::string();
}
switch (ParseSortOption(arg, sortConfig, eval, content)) {
case SortOptionResult::Parsed:
break;
case SortOptionResult::Error:
return std::string();
case SortOptionResult::NotRecognized:
reportError(
eval, content->GetOriginalExpression(),
cmStrCat("sub-command SORT, option \"", arg, "\" is invalid."));
return std::string();
}
}
cmList list = GetList(parameters[1]);
if (list.size() < 2) {
return list.to_string();
}
// The strict-weak-ordering guard in cmList::sort may call this twice per
// pair, so the body can be evaluated up to twice per comparison.
auto comparator = [&](std::string const& a, std::string const& b) -> bool {
std::string r =
EvaluateBodyWithBoundOperands(bodyExpr, { a, b }, eval, dagChecker);
if (eval->HadError) {
throw cmList::transform_error(std::string{}); // body already reported
}
if (r == "1") {
return true;
}
if (r == "0") {
return false;
}
throw cmList::transform_error(
cmStrCat("sub-command SORT, COMPARATOR body must evaluate to \"0\" or "
"\"1\", but evaluated to \"",
r, "\"."));
};
try {
list.sort(sortConfig, comparator);
} catch (std::invalid_argument& e) {
if (!eval->HadError) {
reportError(eval, content->GetOriginalExpression(), e.what());
}
return std::string();
}
return list.to_string();
}
// 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
@@ -2245,6 +2445,12 @@ static const struct ListNode : public cmGeneratorExpressionNode
return false;
}
}
// Leave the SORT COMPARATOR <body> unevaluated; a bare COMPARATOR token is
// unambiguous since SORT's other options are colon-style.
if (parameters.size() >= 3 && parameters[0] == "SORT" &&
parameters.back() == "COMPARATOR") {
return false;
}
// Skip the APPLY <body> (4th parameter) so $<_0> is not evaluated unbound;
// selector args (5th+) evaluate normally.
return !(parameters.size() == 3 && parameters[0] == "TRANSFORM" &&
@@ -2364,6 +2570,17 @@ static const struct ListNode : public cmGeneratorExpressionNode
.to_string();
}
// SORT COMPARATOR is handled here, not the listCommands SORT lambda,
// because the body needs the DAG checker.
if (parameters.size() >= 3 && parameters[0] == "SORT") {
for (std::size_t i = 2; i < parameters.size(); ++i) {
if (parameters[i] == "COMPARATOR") {
return EvaluateSortComparator(parameters, i, eval, content,
dagChecker);
}
}
}
static std::unordered_map<
cm::string_view,
std::function<std::string(cm::GenEx::Evaluation*,
@@ -2678,97 +2895,19 @@ static const struct ListNode : public cmGeneratorExpressionNode
false)) {
auto list = GetList(args.front());
args.advance(1);
auto const COMPARE = "COMPARE:"_s;
auto const CASE = "CASE:"_s;
auto const ORDER = "ORDER:"_s;
using SortConfig = cmList::SortConfiguration;
SortConfig sortConfig;
cmList::SortConfiguration sortConfig;
for (auto const& arg : args) {
if (cmHasPrefix(arg, COMPARE)) {
if (sortConfig.Compare !=
SortConfig::CompareMethod::DEFAULT) {
reportError(ev, cnt->GetOriginalExpression(),
"sub-command SORT, COMPARE option has been "
"specified multiple times.");
switch (ParseSortOption(arg, sortConfig, ev, cnt)) {
case SortOptionResult::Parsed:
break;
case SortOptionResult::Error:
return std::string{};
}
auto option =
cm::string_view{ arg.c_str() + COMPARE.length() };
if (option == "STRING"_s) {
sortConfig.Compare = SortConfig::CompareMethod::STRING;
continue;
}
if (option == "FILE_BASENAME"_s) {
sortConfig.Compare =
SortConfig::CompareMethod::FILE_BASENAME;
continue;
}
if (option == "NATURAL"_s) {
sortConfig.Compare = SortConfig::CompareMethod::NATURAL;
continue;
}
reportError(
ev, cnt->GetOriginalExpression(),
cmStrCat(
"sub-command SORT, an invalid COMPARE option has been "
"specified: \"",
option, "\"."));
return std::string{};
}
if (cmHasPrefix(arg, CASE)) {
if (sortConfig.Case !=
SortConfig::CaseSensitivity::DEFAULT) {
case SortOptionResult::NotRecognized:
reportError(ev, cnt->GetOriginalExpression(),
"sub-command SORT, CASE option has been "
"specified multiple times.");
cmStrCat("sub-command SORT, option \"", arg,
"\" is invalid."));
return std::string{};
}
auto option = cm::string_view{ arg.c_str() + CASE.length() };
if (option == "SENSITIVE"_s) {
sortConfig.Case = SortConfig::CaseSensitivity::SENSITIVE;
continue;
}
if (option == "INSENSITIVE"_s) {
sortConfig.Case = SortConfig::CaseSensitivity::INSENSITIVE;
continue;
}
reportError(
ev, cnt->GetOriginalExpression(),
cmStrCat(
"sub-command SORT, an invalid CASE option has been "
"specified: \"",
option, "\"."));
return std::string{};
}
if (cmHasPrefix(arg, ORDER)) {
if (sortConfig.Order != SortConfig::OrderMode::DEFAULT) {
reportError(ev, cnt->GetOriginalExpression(),
"sub-command SORT, ORDER option has been "
"specified multiple times.");
return std::string{};
}
auto option =
cm::string_view{ arg.c_str() + ORDER.length() };
if (option == "ASCENDING"_s) {
sortConfig.Order = SortConfig::OrderMode::ASCENDING;
continue;
}
if (option == "DESCENDING"_s) {
sortConfig.Order = SortConfig::OrderMode::DESCENDING;
continue;
}
reportError(
ev, cnt->GetOriginalExpression(),
cmStrCat(
"sub-command SORT, an invalid ORDER option has been "
"specified: \"",
option, "\"."));
return std::string{};
}
reportError(ev, cnt->GetOriginalExpression(),
cmStrCat("sub-command SORT, option \"", arg,
"\" is invalid."));
return std::string{};
}
return list.sort(sortConfig).to_string();
@@ -6285,7 +6424,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 },
+19 -5
View File
@@ -382,7 +382,9 @@ cmList& cmList::sort(SortConfiguration cfg)
return *this;
}
cmList& cmList::sort(SortConfiguration cfg, cmMakefile& makefile)
cmList& cmList::sort(
SortConfiguration cfg,
std::function<bool(std::string const&, std::string const&)> comparator)
{
SortConfiguration config{ cfg };
@@ -394,11 +396,10 @@ cmList& cmList::sort(SortConfiguration cfg, cmMakefile& makefile)
}
try {
ComparatorEvaluator evaluator(config.ComparatorFunction, makefile);
StringSorter sorter(
config, [&evaluator](std::string const& a, std::string const& b) {
bool result = evaluator(a, b);
if (result && evaluator(b, a)) {
config, [&comparator](std::string const& a, std::string const& b) {
bool result = comparator(a, b);
if (result && comparator(b, a)) {
throw cmList::transform_error(
"sub-command SORT, COMPARATOR: function does not induce a strict "
"weak ordering. The comparator returned TRUE for both (a, b) and "
@@ -414,6 +415,19 @@ cmList& cmList::sort(SortConfiguration cfg, cmMakefile& makefile)
return *this;
}
cmList& cmList::sort(SortConfiguration cfg, cmMakefile& makefile)
{
try {
ComparatorEvaluator evaluator(cfg.ComparatorFunction, makefile);
return this->sort(
cfg, [&evaluator](std::string const& a, std::string const& b) {
return evaluator(a, b);
});
} catch (transform_error& e) {
throw std::invalid_argument(e.what());
}
}
namespace {
using transform_type = std::function<std::string(std::string const&)>;
using transform_error = cmList::transform_error;
+4
View File
@@ -7,6 +7,7 @@
#include <algorithm>
#include <cstddef>
#include <functional>
#include <initializer_list>
#include <iterator>
#include <memory>
@@ -873,6 +874,9 @@ public:
};
cmList& sort(SortConfiguration config = SortConfiguration{});
cmList& sort(SortConfiguration config, cmMakefile& makefile);
cmList& sort(
SortConfiguration config,
std::function<bool(std::string const&, std::string const&)> comparator);
// exception raised on error during transform operations
class transform_error : public std::runtime_error
+1
View File
@@ -17,6 +17,7 @@ set(CMakeLib_TESTS
testGenExBoundOperand.cxx
testGenExTransformApply.cxx
testGenExListPredicate.cxx
testGenExListSortComparator.cxx
testJSONHelpers.cxx
testRST.cxx
testRange.cxx
+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,157 @@
/* 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");
}
};
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 testSortNumericAscending()
{
GenExFixture fx;
return expectEq(
"testSortNumericAscending",
fx.Eval("$<LIST:SORT,3;1;2,COMPARATOR,$<STRLESS:$<_0>,$<_1>>>"), "1;2;3");
}
static bool testSortByExtension()
{
GenExFixture fx;
return expectEq("testSortByExtension",
fx.Eval("$<LIST:SORT,c.z;a.a;b.m,COMPARATOR,"
"$<STRLESS:$<PATH:GET_EXTENSION,$<_0>>,"
"$<PATH:GET_EXTENSION,$<_1>>>>"),
"a.a;b.m;c.z");
}
static bool testSortDescending()
{
GenExFixture fx;
return expectEq(
"testSortDescending",
fx.Eval(
"$<LIST:SORT,3;1;2,COMPARATOR,$<STRLESS:$<_0>,$<_1>>,ORDER:DESCENDING>"),
"3;2;1");
}
static bool testSortNestedBinding()
{
GenExFixture fx;
// Nested binding: the inner APPLY rebinds $<_0> but the outer $<_1> is
// restored after it, so this reduces to STRLESS(a, b) ascending.
return expectEq(
"testSortNestedBinding",
fx.Eval("$<LIST:SORT,y;x,COMPARATOR,"
"$<STRLESS:$<LIST:TRANSFORM,$<_0>,APPLY,$<_0>>,$<_1>>>"),
"x;y");
}
static bool testSortEmpty()
{
GenExFixture fx;
return expectEq("testSortEmpty",
fx.Eval("$<LIST:SORT,,COMPARATOR,$<STRLESS:$<_0>,$<_1>>>"),
"");
}
static bool testSortSingle()
{
GenExFixture fx;
return expectEq("testSortSingle",
fx.Eval("$<LIST:SORT,x,COMPARATOR,$<STRLESS:$<_0>,$<_1>>>"),
"x");
}
static bool testSortEqualElements()
{
GenExFixture fx;
// Equal elements are FALSE both ways, so the strict-weak-ordering guard must
// not trip and the duplicates are preserved.
return expectEq(
"testSortEqualElements",
fx.Eval("$<LIST:SORT,b;a;b,COMPARATOR,$<STRLESS:$<_0>,$<_1>>>"), "a;b;b");
}
static bool testSortCaseInsensitive()
{
// CASE:INSENSITIVE case-folds the body's operands, so B;a;C orders as a;B;C
// (elements keep their original case).
GenExFixture fx;
return expectEq(
"testSortCaseInsensitive",
fx.Eval(
"$<LIST:SORT,B;a;C,COMPARATOR,$<STRLESS:$<_0>,$<_1>>,CASE:INSENSITIVE>"),
"a;B;C");
}
int testGenExListSortComparator(int /*argc*/, char* /*argv*/[])
{
if (!testSortNumericAscending()) {
return 1;
}
if (!testSortByExtension()) {
return 1;
}
if (!testSortDescending()) {
return 1;
}
if (!testSortNestedBinding()) {
return 1;
}
if (!testSortEmpty()) {
return 1;
}
if (!testSortSingle()) {
return 1;
}
if (!testSortEqualElements()) {
return 1;
}
if (!testSortCaseInsensitive()) {
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>")
@@ -0,0 +1 @@
option "COMPARE" is incompatible with "COMPARATOR"
@@ -0,0 +1,2 @@
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
CONTENT "$<LIST:SORT,a;b,COMPARATOR,$<STRLESS:$<_0>,$<_1>>,COMPARE:STRING>")
@@ -0,0 +1 @@
option "BOGUS:X" is invalid
@@ -0,0 +1,4 @@
# An unrecognized trailing option on the COMPARATOR path goes through the
# NotRecognized branch of the shared option parser.
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
CONTENT "$<LIST:SORT,a;b,COMPARATOR,$<STRLESS:$<_0>,$<_1>>,BOGUS:X>")
@@ -0,0 +1 @@
sub-command SORT, COMPARATOR expects a <body> argument
@@ -0,0 +1,2 @@
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
CONTENT "$<LIST:SORT,a;b,COMPARATOR>")
@@ -0,0 +1 @@
COMPARATOR body must evaluate to "0" or "1"
@@ -0,0 +1,2 @@
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
CONTENT "$<LIST:SORT,a;b,COMPARATOR,maybe>")
@@ -0,0 +1 @@
does not induce a strict weak
@@ -0,0 +1,2 @@
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
CONTENT "$<LIST:SORT,a;b;c,COMPARATOR,1>")
@@ -0,0 +1,5 @@
file(READ "${RunCMake_TEST_BINARY_DIR}/out.txt" actual)
string(STRIP "${actual}" actual)
if(NOT actual STREQUAL "b;c;a")
set(RunCMake_TEST_FAILED "unexpected output: [${actual}]")
endif()
@@ -0,0 +1,10 @@
add_custom_target(a)
add_custom_target(b)
add_custom_target(c)
set_property(TARGET a PROPERTY MY_RANK 3)
set_property(TARGET b PROPERTY MY_RANK 1)
set_property(TARGET c PROPERTY MY_RANK 2)
# Sort the target names by their MY_RANK property, ascending.
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/out.txt"
CONTENT "$<LIST:SORT,a;b;c,COMPARATOR,$<STRLESS:$<TARGET_PROPERTY:$<_0>,MY_RANK>,$<TARGET_PROPERTY:$<_1>,MY_RANK>>>\n")
@@ -72,6 +72,13 @@ run_cmake(ListTransformPredicateLinkLibraries)
run_cmake(ListFilterPredicateMissingBody)
run_cmake(ListFilterPredicateNonBool)
run_cmake(BoundOperandOutsideBinding)
run_cmake(BoundOperand1OutsideBinding)
run_cmake(ListSortComparatorNonBool)
run_cmake(ListSortComparatorNotStrictWeak)
run_cmake(ListSortComparatorCompareConflict)
run_cmake(ListSortComparatorInvalidOption)
run_cmake(ListSortComparatorMissingBody)
run_cmake(ListSortComparatorTargetProperty)
function(run_cmake_build test)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test}-build)