foreach: Allow multiple iteration variables for ZIP_LIST mode

This commit is contained in:
Alex Turbov
2019-11-09 17:50:47 +02:00
parent f3e51a2b1d
commit d30468a2f6
17 changed files with 194 additions and 31 deletions

View File

@@ -86,27 +86,42 @@ yields
.. code-block:: cmake
foreach(<loop_var> IN ZIP_LISTS <lists>)
foreach(<loop_var>... IN ZIP_LISTS <lists>)
In this variant, ``<lists>`` is a whitespace or semicolon
separated list of list-valued variables. The ``foreach``
command iterates over each list simultaneously and set the
``loop_var_N`` variables to the current item from the
corresponding list. If some list is shorter than others,
the corresponding iteration variable is not defined.
command iterates over each list simultaneously setting the
iteration variables as follows:
- if the only ``loop_var`` given, then it sets a series of
``loop_var_N`` variables to the current item from the
corresponding list;
- if multiple variable names passed, their count should match
the lists variables count;
- if any of the lists are shorter, the corresponding iteration
variable is not defined for the current iteration.
.. code-block:: cmake
list(APPEND English one two three four)
list(APPEND Bahasa satu dua tiga)
foreach(X IN ZIP_LISTS English Bahasa)
message(STATUS "X_0=${X_0}, X_1=${X_1}")
foreach(num IN ZIP_LISTS English Bahasa)
message(STATUS "num_0=${num_0}, num_1=${num_1}")
endforeach()
foreach(en ba IN ZIP_LISTS English Bahasa)
message(STATUS "en=${en}, ba=${ba}")
endforeach()
yields
::
-- X_0=one, X_1=satu
-- X_0=two, X_1=dua
-- X_0=three, X_1=tiga
-- X_0=four, X_1=
-- num_0=one, num_1=satu
-- num_0=two, num_1=dua
-- num_0=three, num_1=tiga
-- num_0=four, num_1=
-- en=one, ba=satu
-- en=two, ba=dua
-- en=three, ba=tiga
-- en=four, ba=

View File

@@ -3,6 +3,7 @@
#include "cmForEachCommand.h"
#include <algorithm>
#include <cassert>
#include <cstddef>
// NOTE The declaration of `std::abs` has moved to `cmath` since C++17
// See https://en.cppreference.com/w/cpp/numeric/math/abs
@@ -43,6 +44,10 @@ public:
bool Replay(std::vector<cmListFileFunction> functions,
cmExecutionStatus& inStatus) override;
void SetIterationVarsCount(const std::size_t varsCount)
{
this->IterationVarsCount = varsCount;
}
void SetZipLists() { this->ZipLists = true; }
std::vector<std::string> Args;
@@ -64,6 +69,7 @@ private:
cmExecutionStatus& inStatus, cmMakefile& mf);
cmMakefile* Makefile;
std::size_t IterationVarsCount = 0u;
bool ZipLists = false;
};
@@ -98,6 +104,9 @@ bool cmForEachFunctionBlocker::ReplayItems(
std::vector<cmListFileFunction> const& functions,
cmExecutionStatus& inStatus)
{
assert("Unexpected number of iteration variables" &&
this->IterationVarsCount == 1);
auto& mf = inStatus.GetMakefile();
// At end of for each execute recorded commands
@@ -130,35 +139,55 @@ bool cmForEachFunctionBlocker::ReplayZipLists(
std::vector<cmListFileFunction> const& functions,
cmExecutionStatus& inStatus)
{
assert("Unexpected number of iteration variables" &&
this->IterationVarsCount >= 1);
auto& mf = inStatus.GetMakefile();
// Expand the list of list-variables into a list of lists of strings
std::vector<std::vector<std::string>> values;
values.reserve(this->Args.size() - 1);
values.reserve(this->Args.size() - this->IterationVarsCount);
// Also track the longest list size
std::size_t max_items = 0u;
for (auto const& var : cmMakeRange(this->Args).advance(1)) {
std::size_t maxItems = 0u;
for (auto const& var :
cmMakeRange(this->Args).advance(this->IterationVarsCount)) {
std::vector<std::string> items;
auto const& value = mf.GetSafeDefinition(var);
if (!value.empty()) {
cmExpandList(value, items, true);
}
max_items = std::max(max_items, items.size());
maxItems = std::max(maxItems, items.size());
values.emplace_back(std::move(items));
}
// Form the list of iteration variables
std::vector<std::string> iterationVars;
if (this->IterationVarsCount > 1) {
// If multiple iteration variables has given,
// just copy them to the `iterationVars` list.
iterationVars.reserve(values.size());
std::copy(this->Args.begin(),
this->Args.begin() + this->IterationVarsCount,
std::back_inserter(iterationVars));
} else {
// In case of the only iteration variable,
// generate names as `var_name_N`,
// where `N` is the count of lists to zip
iterationVars.resize(values.size());
const auto iter_var_prefix = this->Args.front() + "_";
auto i = 0u;
std::generate(
iterationVars.begin(), iterationVars.end(),
[&]() -> std::string { return iter_var_prefix + std::to_string(i++); });
}
assert("Sanity check" && iterationVars.size() == values.size());
// Store old values for iteration variables
std::map<std::string, std::string> oldDefs;
// Also, form the vector of iteration variable names
std::vector<std::string> iteration_vars;
iteration_vars.reserve(values.size());
const auto iter_var_prefix = this->Args.front();
for (auto i = 0u; i < values.size(); ++i) {
auto iter_var_name = iter_var_prefix + "_" + std::to_string(i);
if (mf.GetDefinition(iter_var_name)) {
oldDefs.emplace(iter_var_name, mf.GetDefinition(iter_var_name));
if (mf.GetDefinition(iterationVars[i])) {
oldDefs.emplace(iterationVars[i], mf.GetDefinition(iterationVars[i]));
}
iteration_vars.emplace_back(std::move(iter_var_name));
}
// Form a vector of current positions in all lists (Ok, vectors) of values
@@ -168,19 +197,20 @@ bool cmForEachFunctionBlocker::ReplayZipLists(
values.begin(), values.end(), std::back_inserter(positions),
// Set the initial position to the beginning of every list
[](decltype(values)::value_type& list) { return list.begin(); });
assert("Sanity check" && positions.size() == values.size());
auto restore = false;
// Iterate over all the lists simulateneously
for (auto i = 0u; i < max_items; ++i) {
for (auto i = 0u; i < maxItems; ++i) {
// Declare iteration variables
for (auto j = 0u; j < values.size(); ++j) {
// Define (or not) the iteration variable if the current position
// still not at the end...
if (positions[j] != values[j].end()) {
mf.AddDefinition(iteration_vars[j], *positions[j]);
mf.AddDefinition(iterationVars[j], *positions[j]);
++positions[j];
} else {
mf.RemoveDefinition(iteration_vars[j]);
mf.RemoveDefinition(iterationVars[j]);
}
}
// Invoke all the functions that were collected in the block.
@@ -230,10 +260,19 @@ auto cmForEachFunctionBlocker::invoke(
return result;
}
bool HandleInMode(std::vector<std::string> const& args, cmMakefile& makefile)
bool HandleInMode(std::vector<std::string> const& args,
std::vector<std::string>::const_iterator kwInIter,
cmMakefile& makefile)
{
assert("A valid iterator expected" && kwInIter != args.end());
auto fb = cm::make_unique<cmForEachFunctionBlocker>(&makefile);
fb->Args.push_back(args.front());
// Copy iteration variable names first
std::copy(args.begin(), kwInIter, std::back_inserter(fb->Args));
// Remember the count of given iteration variable names
const auto varsCount = fb->Args.size();
fb->SetIterationVarsCount(varsCount);
enum Doing
{
@@ -243,13 +282,20 @@ bool HandleInMode(std::vector<std::string> const& args, cmMakefile& makefile)
DoingZipLists
};
Doing doing = DoingNone;
for (std::string const& arg : cmMakeRange(args).advance(2)) {
// Iterate over arguments past the "IN" keyword
for (std::string const& arg : cmMakeRange(++kwInIter, args.end())) {
if (arg == "LISTS") {
if (doing == DoingZipLists) {
makefile.IssueMessage(MessageType::FATAL_ERROR,
"ZIP_LISTS can not be used with LISTS or ITEMS");
return true;
}
if (varsCount != 1u) {
makefile.IssueMessage(
MessageType::FATAL_ERROR,
"ITEMS or LISTS require exactly one iteration variable");
return true;
}
doing = DoingLists;
} else if (arg == "ITEMS") {
@@ -258,6 +304,12 @@ bool HandleInMode(std::vector<std::string> const& args, cmMakefile& makefile)
"ZIP_LISTS can not be used with LISTS or ITEMS");
return true;
}
if (varsCount != 1u) {
makefile.IssueMessage(
MessageType::FATAL_ERROR,
"ITEMS or LISTS require exactly one iteration variable");
return true;
}
doing = DoingItems;
} else if (arg == "ZIP_LISTS") {
@@ -285,6 +337,18 @@ bool HandleInMode(std::vector<std::string> const& args, cmMakefile& makefile)
}
}
// If `ZIP_LISTS` given and variables count more than 1,
// make sure the given lists count matches variables...
if (doing == DoingZipLists && varsCount > 1u &&
(2u * varsCount) != fb->Args.size()) {
makefile.IssueMessage(
MessageType::FATAL_ERROR,
cmStrCat("Expected ", std::to_string(varsCount),
" list variables, but given ",
std::to_string(fb->Args.size() - varsCount)));
return true;
}
makefile.AddFunctionBlocker(std::move(fb));
return true;
@@ -299,8 +363,9 @@ bool cmForEachCommand(std::vector<std::string> const& args,
status.SetError("called with incorrect number of arguments");
return false;
}
if (args.size() > 1 && args[1] == "IN") {
return HandleInMode(args, status.GetMakefile());
auto kwInIter = std::find(args.begin(), args.end(), "IN");
if (kwInIter != args.end()) {
return HandleInMode(args, kwInIter, status.GetMakefile());
}
// create a function blocker
@@ -360,6 +425,8 @@ bool cmForEachCommand(std::vector<std::string> const& args,
} else {
fb->Args = args;
}
fb->SetIterationVarsCount(1u);
status.GetMakefile().AddFunctionBlocker(std::move(fb));
return true;

View File

@@ -2,8 +2,13 @@ include(RunCMake)
run_cmake(BadRangeInFunction)
run_cmake(foreach-all-test)
run_cmake(foreach-ITEMS-multiple-iter-vars-test)
run_cmake(foreach-LISTS-multiple-iter-vars-test)
run_cmake(foreach-ZIP_LISTS-test)
run_cmake(foreach-ITEMS-with-ZIP_LISTS-mix-test)
run_cmake(foreach-LISTS-with-ZIP_LISTS-mix-test)
run_cmake(foreach-ZIP_LISTS-with-ITEMS-mix-test)
run_cmake(foreach-ZIP_LISTS-with-LISTS-mix-test)
run_cmake(foreach-ZIP_LISTS-multiple-iter-vars-test)
run_cmake(foreach-ZIP_LISTS-iter-vars-mismatch-test-1)
run_cmake(foreach-ZIP_LISTS-iter-vars-mismatch-test-2)

View File

@@ -0,0 +1,4 @@
^CMake Error at foreach-ITEMS-multiple-iter-vars-test.cmake:1 \(foreach\):
ITEMS or LISTS require exactly one iteration variable
Call Stack \(most recent call first\):
CMakeLists.txt:3 \(include\)

View File

@@ -0,0 +1,2 @@
foreach(one two IN ITEMS one two)
endforeach()

View File

@@ -0,0 +1,4 @@
^CMake Error at foreach-LISTS-multiple-iter-vars-test.cmake:1 \(foreach\):
ITEMS or LISTS require exactly one iteration variable
Call Stack \(most recent call first\):
CMakeLists.txt:3 \(include\)

View File

@@ -0,0 +1,2 @@
foreach(one two IN LISTS one two)
endforeach()

View File

@@ -0,0 +1,4 @@
^CMake Error at foreach-ZIP_LISTS-iter-vars-mismatch-test-1.cmake:1 \(foreach\):
Expected 3 list variables, but given 4
Call Stack \(most recent call first\):
CMakeLists.txt:3 \(include\)

View File

@@ -0,0 +1,2 @@
foreach(less than lists IN ZIP_LISTS list_1 list_2 list_3 list_4)
endforeach()

View File

@@ -0,0 +1,4 @@
^CMake Error at foreach-ZIP_LISTS-iter-vars-mismatch-test-2.cmake:1 \(foreach\):
Expected 3 list variables, but given 2
Call Stack \(most recent call first\):
CMakeLists.txt:3 \(include\)

View File

@@ -0,0 +1,2 @@
foreach(greater than lists IN ZIP_LISTS list_1 list_2)
endforeach()

View File

@@ -0,0 +1,6 @@
-- foreach\(\.\.\. IN ZIP_LISTS\):
-- Begin output
-- | one, satu, raz
-- | two, dua, dva
-- | three, tiga, tri
-- End output

View File

@@ -0,0 +1,42 @@
function(foreachTest result list_var_1 list_var_2 list_var_3)
set(_options MUTE)
set(_one_value_args)
set(_multi_value_args)
cmake_parse_arguments(PARSE_ARGV 3 _arg "${_options}" "${_one_value_args}" "${_multi_value_args}")
set(_has_any_output FALSE)
list(APPEND CMAKE_MESSAGE_INDENT "| ")
foreach(first second third IN ZIP_LISTS ${list_var_1} ${list_var_2} ${list_var_3})
if(NOT first)
set(first "[undefiend]")
endif()
if(NOT second)
set(second "[undefiend]")
endif()
if(NOT third)
set(third "[undefiend]")
endif()
if(NOT _arg_MUTE)
message(STATUS "${first}, ${second}, ${third}")
endif()
set(_has_any_output TRUE)
endforeach()
set(${result} ${_has_any_output} PARENT_SCOPE)
endfunction()
function(foreachTestDecorated list_var_1 list_var_2 list_var_3)
list(APPEND CMAKE_MESSAGE_INDENT " ")
message(STATUS "Begin output")
foreachTest(_has_any_output ${list_var_1} ${list_var_2} ${list_var_3})
if(NOT _has_any_output)
message(STATUS "--> empty-output <--")
endif()
message(STATUS "End output")
endfunction()
list(APPEND english one two three)
list(APPEND bahasa satu dua tiga)
list(APPEND russian raz dva tri)
message(STATUS "foreach(... IN ZIP_LISTS):")
foreachTestDecorated(english bahasa russian)