Merge topic 'list-predicate'

15340d6a96 list(FILTER): Add PREDICATE mode
c7af6e94d8 list(TRANSFORM): Add PREDICATE selector
0f30e0fe1c Help: Fix indentation of list(FILTER) docs

Acked-by: Kitware Robot <kwrobot@kitware.com>
Merge-request: !11945
diff --git a/Help/command/list.rst b/Help/command/list.rst
index 88516f0..a84e2cb 100644
--- a/Help/command/list.rst
+++ b/Help/command/list.rst
@@ -19,7 +19,7 @@
 
   `Modification`_
     list(`APPEND`_ <list> [<element>...])
-    list(`FILTER`_ <list> {INCLUDE | EXCLUDE} REGEX <regex>)
+    list(`FILTER`_ <list> <INCLUDE|EXCLUDE> <MODE>)
     list(`INSERT`_ <list> <index> [<element>...])
     list(`POP_BACK`_ <list> [<out-var>...])
     list(`POP_FRONT`_ <list> [<out-var>...])
@@ -120,15 +120,54 @@
   that empty list.
 
 .. signature::
-  list(FILTER <list> <INCLUDE|EXCLUDE> REGEX <regular_expression>)
+  list(FILTER <list> <INCLUDE|EXCLUDE> <MODE>)
 
-.. versionadded:: 3.6
+  .. versionadded:: 3.6
 
-Includes or removes items from the list that match the mode's pattern.
-In ``REGEX`` mode, items will be matched against the given regular expression.
+  Includes or removes items from the list that match the mode's pattern.
 
-For more information on regular expressions look under
-:ref:`string(REGEX) <Regex Specification>`.
+  ``<MODE>`` must be one of the following:
+
+    ``REGEX``
+      Items will be matched against the given regular expression.
+
+      .. code-block:: cmake
+
+        list(FILTER <list> <INCLUDE|EXCLUDE> REGEX <regular_expression>)
+
+      For more information on regular expressions look under
+      :ref:`string(REGEX) <Regex Specification>`.
+
+    ``PREDICATE``
+      Specify a user-defined callable as a predicate.
+
+      .. code-block:: cmake
+
+        list(FILTER <list> <INCLUDE|EXCLUDE> PREDICATE <function>)
+
+      .. versionadded:: 4.4
+
+      ``<function>`` is a user-defined :command:`function` that acts as a
+      unary predicate.  The callable must accept exactly two parameters: the
+      input value and the name of an output variable.  The callable must set the
+      output variable to a boolean value in the calling scope.
+      The output variable is interpreted using standard CMake boolean evaluation.
+      If the callable does not set the output variable, it is an error.
+
+      Example:
+
+      .. code-block:: cmake
+
+        function(file_exists path result)
+          if(EXISTS "${path}")
+            set(${result} TRUE PARENT_SCOPE)
+          else()
+            set(${result} FALSE PARENT_SCOPE)
+          endif()
+        endfunction()
+
+        set(candidate_files main.c missing.c utils.c)
+        list(FILTER candidate_files INCLUDE PREDICATE file_exists)
 
 .. signature::
   list(INSERT <list> <element_index> <element> [<element> ...])
@@ -316,6 +355,39 @@
 
         list(TRANSFORM <list> <ACTION> REGEX <regular_expression> ...)
 
+    ``PREDICATE``
+      Specify a user-defined callable as a predicate.
+      Only elements for which the callable returns a true value will be
+      transformed.
+
+      .. code-block:: cmake
+
+        list(TRANSFORM <list> <ACTION> PREDICATE <function> ...)
+
+      .. versionadded:: 4.4
+
+      ``<function>`` is a user-defined :command:`function` with exactly
+      two formal parameters: the input value and the name of an output
+      variable.  The callable must set the output variable to a boolean
+      value.  Standard CMake boolean evaluation is used.
+      If the callable does not set the output variable, it is an error.
+
+      Example:
+
+      .. code-block:: cmake
+
+        function(is_relative path result)
+          if(NOT IS_ABSOLUTE "${path}")
+            set(${result} TRUE PARENT_SCOPE)
+          else()
+            set(${result} FALSE PARENT_SCOPE)
+          endif()
+        endfunction()
+
+        set(search_paths /usr/include src lib /opt/lib)
+        list(TRANSFORM search_paths PREPEND "${CMAKE_CURRENT_SOURCE_DIR}/"
+             PREDICATE is_relative)
+
 
 Ordering
 ^^^^^^^^
diff --git a/Help/release/dev/list-PREDICATE.rst b/Help/release/dev/list-PREDICATE.rst
new file mode 100644
index 0000000..324bd07
--- /dev/null
+++ b/Help/release/dev/list-PREDICATE.rst
@@ -0,0 +1,10 @@
+list-PREDICATE
+--------------
+
+* The :command:`list(TRANSFORM)` command gained a new ``PREDICATE`` selector
+  that invokes a user-defined callable to decide which elements are
+  transformed.
+
+* The :command:`list(FILTER)` command gained a new ``PREDICATE`` mode
+  that invokes a user-defined callable to decide which elements are
+  included or excluded, complementing the existing ``REGEX`` mode.
diff --git a/Source/cmList.cxx b/Source/cmList.cxx
index 1c76664..4dfeaa2 100644
--- a/Source/cmList.cxx
+++ b/Source/cmList.cxx
@@ -81,6 +81,92 @@
   cmsys::RegularExpression& Regex;
   bool const IncludeMatches;
 };
+
+// Hash of call site (FilePath:Line) for unique variable names across recursive
+// calls.
+std::string OutputVarFor(cm::string_view prefix, cmMakefile& makefile)
+{
+  cmListFileContext context = makefile.GetBacktrace().Top();
+  std::size_t hash =
+    std::hash<std::string>{}(cmStrCat(context.FilePath, ":", context.Line));
+  return cmStrCat(prefix, hash, "_");
+}
+
+class PredicateEvaluator
+{
+public:
+  PredicateEvaluator(std::string functionName, cmMakefile& makefile,
+                     std::string errorPrefix = "sub-command TRANSFORM, "
+                                               "selector PREDICATE")
+    : FunctionName(std::move(functionName))
+    , Makefile(&makefile)
+    , ErrorPrefix(std::move(errorPrefix))
+    , OutputVar(OutputVarFor("_cmake_predicate_out_", makefile))
+  {
+    if (!makefile.GetState()->GetCommand(this->FunctionName)) {
+      throw cmList::transform_error(cmStrCat(this->ErrorPrefix,
+                                             ": unknown function \"",
+                                             this->FunctionName, "\"."));
+    }
+  }
+
+  bool operator()(std::string const& value)
+  {
+    this->Makefile->RemoveDefinition(this->OutputVar);
+
+    cmListFileContext context = this->Makefile->GetBacktrace().Top();
+    std::vector<cmListFileArgument> funcArgs;
+    funcArgs.emplace_back(value, cmListFileArgument::Quoted, context.Line);
+    funcArgs.emplace_back(this->OutputVar, cmListFileArgument::Quoted,
+                          context.Line);
+    cmListFileFunction func{ this->FunctionName, context.Line, context.Line,
+                             std::move(funcArgs) };
+
+    cmExecutionStatus status(*this->Makefile);
+    if (!this->Makefile->ExecuteCommand(func, status) ||
+        status.GetNestedError()) {
+      throw cmList::transform_error(
+        cmStrCat(this->ErrorPrefix, ": function \"", this->FunctionName,
+                 "\" failed during execution."));
+    }
+
+    cmValue result = this->Makefile->GetDefinition(this->OutputVar);
+    if (!result) {
+      throw cmList::transform_error(
+        cmStrCat(this->ErrorPrefix, ": function \"", this->FunctionName,
+                 "\" did not set the output variable."));
+    }
+
+    bool boolResult = cmIsOn(*result);
+    this->Makefile->RemoveDefinition(this->OutputVar);
+    return boolResult;
+  }
+
+private:
+  std::string FunctionName;
+  cmMakefile* Makefile = nullptr;
+  std::string ErrorPrefix;
+  std::string OutputVar;
+};
+
+class MatchesPredicate
+{
+public:
+  MatchesPredicate(PredicateEvaluator& evaluator, cmList::FilterMode mode)
+    : Evaluator(evaluator)
+    , IncludeMatches(mode == cmList::FilterMode::INCLUDE)
+  {
+  }
+
+  bool operator()(std::string const& target)
+  {
+    return this->Evaluator(target) ^ this->IncludeMatches;
+  }
+
+private:
+  PredicateEvaluator& Evaluator;
+  bool IncludeMatches;
+};
 }
 
 cmList& cmList::filter(cm::string_view pattern, FilterMode mode)
@@ -99,6 +185,23 @@
   return *this;
 }
 
+cmList& cmList::filter(std::string const& functionName, FilterMode mode,
+                       cmMakefile& makefile)
+{
+  try {
+    PredicateEvaluator evaluator(functionName, makefile,
+                                 "sub-command FILTER, mode PREDICATE");
+
+    auto it = std::remove_if(this->Values.begin(), this->Values.end(),
+                             MatchesPredicate{ evaluator, mode });
+    this->Values.erase(it, this->Values.end());
+  } catch (transform_error& e) {
+    throw std::invalid_argument(e.what());
+  }
+
+  return *this;
+}
+
 namespace {
 class StringSorter
 {
@@ -270,6 +373,26 @@
 
   cmsys::RegularExpression Regex;
 };
+class TransformSelectorPredicate : public TransformSelector
+{
+public:
+  TransformSelectorPredicate(std::string const& functionName,
+                             cmMakefile& makefile)
+    : TransformSelector("PREDICATE")
+    , Evaluator(functionName, makefile)
+  {
+  }
+
+  bool Validate(std::size_t) override { return true; }
+
+  bool InSelection(std::string const& value) override
+  {
+    return this->Evaluator(value);
+  }
+
+private:
+  PredicateEvaluator Evaluator;
+};
 class TransformSelectorIndexes : public TransformSelector
 {
 public:
@@ -580,6 +703,7 @@
     TransformAction::Initialize(selector);
     this->FunctionName = functionName;
     this->Makefile = &makefile;
+    this->OutputVar = OutputVarFor("_cmake_transform_apply_out_", makefile);
 
     // Validate: command must exist
     if (!makefile.GetState()->GetCommand(this->FunctionName)) {
@@ -603,17 +727,15 @@
       return s;
     }
 
-    // Use a unique output variable name to avoid collisions
-    std::string const outputVar = "_list_transform_apply_out_";
-
     // Unset the output variable before calling
-    this->Makefile->RemoveDefinition(outputVar);
+    this->Makefile->RemoveDefinition(this->OutputVar);
 
     // Build the function call: functionName(s, outputVar)
     cmListFileContext context = this->Makefile->GetBacktrace().Top();
     std::vector<cmListFileArgument> funcArgs;
     funcArgs.emplace_back(s, cmListFileArgument::Quoted, context.Line);
-    funcArgs.emplace_back(outputVar, cmListFileArgument::Quoted, context.Line);
+    funcArgs.emplace_back(this->OutputVar, cmListFileArgument::Quoted,
+                          context.Line);
     cmListFileFunction func{ this->FunctionName, context.Line, context.Line,
                              std::move(funcArgs) };
 
@@ -626,7 +748,7 @@
     }
 
     // Read back the output variable
-    cmValue result = this->Makefile->GetDefinition(outputVar);
+    cmValue result = this->Makefile->GetDefinition(this->OutputVar);
     if (!result) {
       throw transform_error(
         cmStrCat("sub-command TRANSFORM, action APPLY: function \"",
@@ -638,7 +760,7 @@
     std::string output = *result;
 
     // Clean up
-    this->Makefile->RemoveDefinition(outputVar);
+    this->Makefile->RemoveDefinition(this->OutputVar);
 
     return output;
   }
@@ -646,6 +768,7 @@
 private:
   std::string FunctionName;
   cmMakefile* Makefile = nullptr;
+  std::string OutputVar;
 };
 
 // Descriptor of action
@@ -827,6 +950,14 @@
   return std::unique_ptr<cmList::TransformSelector>(selector.release());
 }
 
+std::unique_ptr<cmList::TransformSelector>
+cmList::TransformSelector::NewPREDICATE(std::string const& functionName,
+                                        cmMakefile& makefile)
+{
+  return std::unique_ptr<cmList::TransformSelector>(
+    new TransformSelectorPredicate(functionName, makefile));
+}
+
 cmList& cmList::transform(TransformAction action,
                           std::unique_ptr<TransformSelector> selector)
 {
@@ -897,16 +1028,19 @@
                           cmMakefile& makefile,
                           std::unique_ptr<TransformSelector> selector)
 {
-  auto descriptor = TransformConfigure(action, selector, 1);
+  // Validate action and arity via the static registry.
+  TransformConfigure(action, selector, 1);
 
-  auto* applyAction =
-    static_cast<TransformActionApply*>(descriptor->Transform.get());
-  applyAction->Initialize(static_cast<::TransformSelector*>(selector.get()),
-                          arg, makefile);
+  // Create a local instance rather than reusing the singleton from
+  // Descriptors.  A user function invoked by APPLY may itself call
+  // list(TRANSFORM ... APPLY ...), which would clobber a shared instance.
+  TransformActionApply applyAction;
+  applyAction.Initialize(static_cast<::TransformSelector*>(selector.get()),
+                         arg, makefile);
 
   static_cast<::TransformSelector&>(*selector).Transform(
-    this->Values, [&descriptor](std::string const& s) -> std::string {
-      return (*descriptor->Transform)(s);
+    this->Values, [&applyAction](std::string const& s) -> std::string {
+      return applyAction(s);
     });
 
   return *this;
diff --git a/Source/cmList.h b/Source/cmList.h
index febc2c5f..b34a6ca 100644
--- a/Source/cmList.h
+++ b/Source/cmList.h
@@ -821,8 +821,11 @@
     EXCLUDE
   };
   // Includes or removes items from the list
-  // Throw std::invalid_argument if regular expression is invalid
+  // Throw std::invalid_argument if regular expression is invalid or predicate
+  // function is unknown / does not set its output variable
   cmList& filter(cm::string_view regex, FilterMode mode);
+  cmList& filter(std::string const& functionName, FilterMode mode,
+                 cmMakefile& makefile);
 
   cmList& reverse()
   {
@@ -886,6 +889,7 @@
     struct AT;
     struct FOR;
     struct REGEX;
+    struct PREDICATE;
 
     virtual ~TransformSelector() = default;
 
@@ -911,6 +915,12 @@
     template <typename Type>
     static std::unique_ptr<TransformSelector> New(std::string&&);
 
+    // NewPREDICATE is public (unlike NewAT/NewFOR/NewREGEX) because it takes
+    // a cmMakefile& parameter that cannot be dispatched through the existing
+    // New<Type>() templates.
+    static std::unique_ptr<TransformSelector> NewPREDICATE(
+      std::string const& functionName, cmMakefile& makefile);
+
     cmMakefile* Makefile = nullptr;
 
   private:
diff --git a/Source/cmListCommand.cxx b/Source/cmListCommand.cxx
index 06eba11..e5338d2 100644
--- a/Source/cmListCommand.cxx
+++ b/Source/cmListCommand.cxx
@@ -520,6 +520,7 @@
   std::string const REGEX{ "REGEX" };
   std::string const AT{ "AT" };
   std::string const FOR{ "FOR" };
+  std::string const PREDICATE{ "PREDICATE" };
   std::string const OUTPUT_VARIABLE{ "OUTPUT_VARIABLE" };
   std::unique_ptr<cmList::TransformSelector> selector;
   std::string outputName = listName;
@@ -527,7 +528,8 @@
   try {
     // handle optional arguments
     while (args.size() > index) {
-      if ((args[index] == REGEX || args[index] == AT || args[index] == FOR) &&
+      if ((args[index] == REGEX || args[index] == AT || args[index] == FOR ||
+           args[index] == PREDICATE) &&
           selector) {
         status.SetError(
           cmStrCat("sub-command TRANSFORM, selector already specified (",
@@ -653,6 +655,21 @@
         continue;
       }
 
+      // PREDICATE selector
+      if (args[index] == PREDICATE) {
+        if (args.size() == ++index) {
+          status.SetError("sub-command TRANSFORM, selector PREDICATE expects "
+                          "'function name' argument.");
+          return false;
+        }
+
+        selector = cmList::TransformSelector::NewPREDICATE(
+          args[index], status.GetMakefile());
+
+        index += 1;
+        continue;
+      }
+
       // output variable
       if (args[index] == OUTPUT_VARIABLE) {
         if (args.size() == ++index) {
@@ -951,25 +968,46 @@
   }
 
   std::string const& mode = args[3];
-  if (mode != "REGEX") {
-    status.SetError("sub-command FILTER does not recognize mode " + mode);
-    return false;
-  }
-  if (args.size() != 5) {
-    status.SetError("sub-command FILTER, mode REGEX "
-                    "requires five arguments.");
-    return false;
-  }
-  std::string const& pattern = args[4];
+  if (mode == "REGEX") {
+    if (args.size() != 5) {
+      status.SetError("sub-command FILTER, mode REGEX "
+                      "requires five arguments.");
+      return false;
+    }
+    std::string const& pattern = args[4];
 
-  try {
-    status.GetMakefile().AddDefinition(
-      listName, list->filter(pattern, filterMode).to_string());
-    return true;
-  } catch (std::invalid_argument& e) {
-    status.SetError(e.what());
-    return false;
+    try {
+      status.GetMakefile().AddDefinition(
+        listName, list->filter(pattern, filterMode).to_string());
+      return true;
+    } catch (std::invalid_argument& e) {
+      status.SetError(e.what());
+      return false;
+    }
   }
+
+  if (mode == "PREDICATE") {
+    if (args.size() != 5) {
+      status.SetError("sub-command FILTER, mode PREDICATE "
+                      "requires five arguments.");
+      return false;
+    }
+    std::string const& functionName = args[4];
+
+    try {
+      status.GetMakefile().AddDefinition(
+        listName,
+        list->filter(functionName, filterMode, status.GetMakefile())
+          .to_string());
+      return true;
+    } catch (std::invalid_argument& e) {
+      status.SetError(e.what());
+      return false;
+    }
+  }
+
+  status.SetError("sub-command FILTER does not recognize mode " + mode);
+  return false;
 }
 } // namespace
 
diff --git a/Tests/RunCMake/list/FILTER-PREDICATE-Exclude-stderr.txt b/Tests/RunCMake/list/FILTER-PREDICATE-Exclude-stderr.txt
new file mode 100644
index 0000000..d9ba38d
--- /dev/null
+++ b/Tests/RunCMake/list/FILTER-PREDICATE-Exclude-stderr.txt
@@ -0,0 +1,2 @@
+^mylist was: FILTER_THIS_BIT;DO_NOT_FILTER_THIS;thisisanitem;FILTER_THIS_THING
+mylist is: DO_NOT_FILTER_THIS;thisisanitem$
diff --git a/Tests/RunCMake/list/FILTER-PREDICATE-Exclude.cmake b/Tests/RunCMake/list/FILTER-PREDICATE-Exclude.cmake
new file mode 100644
index 0000000..7b1f349
--- /dev/null
+++ b/Tests/RunCMake/list/FILTER-PREDICATE-Exclude.cmake
@@ -0,0 +1,50 @@
+# Predicate: returns TRUE for items starting with "FILTER_THIS_"
+function(starts_with_filter input result)
+  if(input MATCHES "^FILTER_THIS_")
+    set(${result} TRUE PARENT_SCOPE)
+  else()
+    set(${result} FALSE PARENT_SCOPE)
+  endif()
+endfunction()
+
+set(mylist FILTER_THIS_BIT DO_NOT_FILTER_THIS thisisanitem FILTER_THIS_THING)
+message("mylist was: ${mylist}")
+list(FILTER mylist EXCLUDE PREDICATE starts_with_filter)
+message("mylist is: ${mylist}")
+
+# EXCLUDE with macro predicate
+macro(is_short input result)
+  string(LENGTH "${input}" _len)
+  if(_len LESS 6)
+    set(${result} TRUE)
+  else()
+    set(${result} FALSE)
+  endif()
+endmacro()
+
+set(mylist ab cdefgh ij klmnop qr)
+list(FILTER mylist EXCLUDE PREDICATE is_short)
+if(NOT mylist STREQUAL "cdefgh;klmnop")
+  message(FATAL_ERROR "FILTER(EXCLUDE PREDICATE macro) is \"${mylist}\", expected \"cdefgh;klmnop\"")
+endif()
+
+# EXCLUDE on empty list
+set(empty_list "")
+list(FILTER empty_list EXCLUDE PREDICATE starts_with_filter)
+if(NOT empty_list STREQUAL "")
+  message(FATAL_ERROR "FILTER(EXCLUDE PREDICATE empty) is \"${empty_list}\", expected \"\"")
+endif()
+
+# EXCLUDE where nothing matches (all elements kept)
+set(mylist alpha bravo charlie)
+list(FILTER mylist EXCLUDE PREDICATE starts_with_filter)
+if(NOT mylist STREQUAL "alpha;bravo;charlie")
+  message(FATAL_ERROR "FILTER(EXCLUDE PREDICATE no-match) is \"${mylist}\", expected \"alpha;bravo;charlie\"")
+endif()
+
+# EXCLUDE where everything matches (all elements removed)
+set(mylist FILTER_THIS_A FILTER_THIS_B FILTER_THIS_C)
+list(FILTER mylist EXCLUDE PREDICATE starts_with_filter)
+if(NOT mylist STREQUAL "")
+  message(FATAL_ERROR "FILTER(EXCLUDE PREDICATE all-match) is \"${mylist}\", expected \"\"")
+endif()
diff --git a/Tests/RunCMake/list/FILTER-PREDICATE-Include-stderr.txt b/Tests/RunCMake/list/FILTER-PREDICATE-Include-stderr.txt
new file mode 100644
index 0000000..5e5280d
--- /dev/null
+++ b/Tests/RunCMake/list/FILTER-PREDICATE-Include-stderr.txt
@@ -0,0 +1,2 @@
+^mylist was: FILTER_THIS_BIT;DO_NOT_FILTER_THIS;thisisanitem;FILTER_THIS_THING
+mylist is: FILTER_THIS_BIT;FILTER_THIS_THING$
diff --git a/Tests/RunCMake/list/FILTER-PREDICATE-Include.cmake b/Tests/RunCMake/list/FILTER-PREDICATE-Include.cmake
new file mode 100644
index 0000000..412ff25
--- /dev/null
+++ b/Tests/RunCMake/list/FILTER-PREDICATE-Include.cmake
@@ -0,0 +1,51 @@
+# Predicate: returns TRUE for items starting with "FILTER_THIS_"
+function(starts_with_filter input result)
+  if(input MATCHES "^FILTER_THIS_")
+    set(${result} TRUE PARENT_SCOPE)
+  else()
+    set(${result} FALSE PARENT_SCOPE)
+  endif()
+endfunction()
+
+# Predicate as macro
+macro(is_short input result)
+  string(LENGTH "${input}" _len)
+  if(_len LESS 6)
+    set(${result} TRUE)
+  else()
+    set(${result} FALSE)
+  endif()
+endmacro()
+
+set(mylist FILTER_THIS_BIT DO_NOT_FILTER_THIS thisisanitem FILTER_THIS_THING)
+message("mylist was: ${mylist}")
+list(FILTER mylist INCLUDE PREDICATE starts_with_filter)
+message("mylist is: ${mylist}")
+
+# INCLUDE with macro predicate
+set(mylist ab cdefgh ij klmnop qr)
+list(FILTER mylist INCLUDE PREDICATE is_short)
+if(NOT mylist STREQUAL "ab;ij;qr")
+  message(FATAL_ERROR "FILTER(INCLUDE PREDICATE macro) is \"${mylist}\", expected \"ab;ij;qr\"")
+endif()
+
+# INCLUDE on empty list
+set(empty_list "")
+list(FILTER empty_list INCLUDE PREDICATE starts_with_filter)
+if(NOT empty_list STREQUAL "")
+  message(FATAL_ERROR "FILTER(INCLUDE PREDICATE empty) is \"${empty_list}\", expected \"\"")
+endif()
+
+# INCLUDE where nothing matches (all elements removed)
+set(mylist alpha bravo charlie)
+list(FILTER mylist INCLUDE PREDICATE starts_with_filter)
+if(NOT mylist STREQUAL "")
+  message(FATAL_ERROR "FILTER(INCLUDE PREDICATE no-match) is \"${mylist}\", expected \"\"")
+endif()
+
+# INCLUDE where everything matches
+set(mylist FILTER_THIS_A FILTER_THIS_B FILTER_THIS_C)
+list(FILTER mylist INCLUDE PREDICATE starts_with_filter)
+if(NOT mylist STREQUAL "FILTER_THIS_A;FILTER_THIS_B;FILTER_THIS_C")
+  message(FATAL_ERROR "FILTER(INCLUDE PREDICATE all-match) is \"${mylist}\", expected \"FILTER_THIS_A;FILTER_THIS_B;FILTER_THIS_C\"")
+endif()
diff --git a/Tests/RunCMake/list/FILTER-PREDICATE-NoOutput-result.txt b/Tests/RunCMake/list/FILTER-PREDICATE-NoOutput-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/list/FILTER-PREDICATE-NoOutput-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/list/FILTER-PREDICATE-NoOutput-stderr.txt b/Tests/RunCMake/list/FILTER-PREDICATE-NoOutput-stderr.txt
new file mode 100644
index 0000000..2fbc148
--- /dev/null
+++ b/Tests/RunCMake/list/FILTER-PREDICATE-NoOutput-stderr.txt
@@ -0,0 +1,5 @@
+^CMake Error at FILTER-PREDICATE-NoOutput\.cmake:6 \(list\):
+  list sub-command FILTER, mode PREDICATE: function "bad_predicate" did not
+  set the output variable\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/FILTER-PREDICATE-NoOutput.cmake b/Tests/RunCMake/list/FILTER-PREDICATE-NoOutput.cmake
new file mode 100644
index 0000000..f1f4621
--- /dev/null
+++ b/Tests/RunCMake/list/FILTER-PREDICATE-NoOutput.cmake
@@ -0,0 +1,6 @@
+function(bad_predicate in out)
+  # Deliberately does NOT set ${out}
+endfunction()
+
+set(mylist alpha bravo charlie)
+list(FILTER mylist INCLUDE PREDICATE bad_predicate)
diff --git a/Tests/RunCMake/list/FILTER-PREDICATE-TooManyArguments-result.txt b/Tests/RunCMake/list/FILTER-PREDICATE-TooManyArguments-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/list/FILTER-PREDICATE-TooManyArguments-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/list/FILTER-PREDICATE-TooManyArguments-stderr.txt b/Tests/RunCMake/list/FILTER-PREDICATE-TooManyArguments-stderr.txt
new file mode 100644
index 0000000..e3c7a10
--- /dev/null
+++ b/Tests/RunCMake/list/FILTER-PREDICATE-TooManyArguments-stderr.txt
@@ -0,0 +1,4 @@
+^CMake Error at FILTER-PREDICATE-TooManyArguments\.cmake:6 \(list\):
+  list sub-command FILTER, mode PREDICATE requires five arguments\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/FILTER-PREDICATE-TooManyArguments.cmake b/Tests/RunCMake/list/FILTER-PREDICATE-TooManyArguments.cmake
new file mode 100644
index 0000000..87a8532
--- /dev/null
+++ b/Tests/RunCMake/list/FILTER-PREDICATE-TooManyArguments.cmake
@@ -0,0 +1,6 @@
+function(my_predicate in out)
+  set(${out} TRUE PARENT_SCOPE)
+endfunction()
+
+set(mylist alpha bravo charlie)
+list(FILTER mylist INCLUDE PREDICATE my_predicate extra_arg)
diff --git a/Tests/RunCMake/list/FILTER-PREDICATE-UnknownFunction-result.txt b/Tests/RunCMake/list/FILTER-PREDICATE-UnknownFunction-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/list/FILTER-PREDICATE-UnknownFunction-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/list/FILTER-PREDICATE-UnknownFunction-stderr.txt b/Tests/RunCMake/list/FILTER-PREDICATE-UnknownFunction-stderr.txt
new file mode 100644
index 0000000..2b6234b
--- /dev/null
+++ b/Tests/RunCMake/list/FILTER-PREDICATE-UnknownFunction-stderr.txt
@@ -0,0 +1,5 @@
+^CMake Error at FILTER-PREDICATE-UnknownFunction\.cmake:2 \(list\):
+  list sub-command FILTER, mode PREDICATE: unknown function
+  "no_such_function"\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/FILTER-PREDICATE-UnknownFunction.cmake b/Tests/RunCMake/list/FILTER-PREDICATE-UnknownFunction.cmake
new file mode 100644
index 0000000..48461bd
--- /dev/null
+++ b/Tests/RunCMake/list/FILTER-PREDICATE-UnknownFunction.cmake
@@ -0,0 +1,2 @@
+set(mylist alpha bravo charlie)
+list(FILTER mylist INCLUDE PREDICATE no_such_function)
diff --git a/Tests/RunCMake/list/RunCMakeTest.cmake b/Tests/RunCMake/list/RunCMakeTest.cmake
index dd4264c..1fd86a9 100644
--- a/Tests/RunCMake/list/RunCMakeTest.cmake
+++ b/Tests/RunCMake/list/RunCMakeTest.cmake
@@ -37,6 +37,11 @@
 run_cmake(FILTER-REGEX-InvalidOperator)
 run_cmake(FILTER-REGEX-Valid0)
 run_cmake(FILTER-REGEX-Valid1)
+run_cmake(FILTER-PREDICATE-UnknownFunction)
+run_cmake(FILTER-PREDICATE-NoOutput)
+run_cmake(FILTER-PREDICATE-TooManyArguments)
+run_cmake(FILTER-PREDICATE-Include)
+run_cmake(FILTER-PREDICATE-Exclude)
 
 run_cmake(JOIN-NoArguments)
 run_cmake(JOIN-NoVariable)
@@ -65,6 +70,7 @@
 run_cmake(TRANSFORM-REPLACE-InvalidReplace1)
 run_cmake(TRANSFORM-REPLACE-InvalidReplace2)
 run_cmake(TRANSFORM-APPLY-NoFunction)
+run_cmake(TRANSFORM-APPLY-UnknownFunction)
 run_cmake(TRANSFORM-APPLY-NoOutput)
 # 'selector' oriented tests
 run_cmake(TRANSFORM-Selector-REGEX-NoArguments)
@@ -81,6 +87,9 @@
 run_cmake(TRANSFORM-Selector-FOR-ZeroStepArgument)
 run_cmake(TRANSFORM-Selector-FOR-NegativeStepArgument)
 run_cmake(TRANSFORM-Selector-FOR-BackwardsRange)
+run_cmake(TRANSFORM-Selector-PREDICATE-NoArguments)
+run_cmake(TRANSFORM-Selector-PREDICATE-UnknownFunction)
+run_cmake(TRANSFORM-Selector-PREDICATE-NoOutput)
 # 'output' oriented tests
 run_cmake(TRANSFORM-Output-OUTPUT_VARIABLE-NoArguments)
 run_cmake(TRANSFORM-Output-OUTPUT_VARIABLE-TooManyArguments)
@@ -93,6 +102,7 @@
 run_cmake(TRANSFORM-PREPEND)
 run_cmake(TRANSFORM-REPLACE)
 run_cmake(TRANSFORM-APPLY)
+run_cmake(TRANSFORM-PREDICATE)
 run_cmake(CMP0186)
 
 # argument tests
diff --git a/Tests/RunCMake/list/TRANSFORM-APPLY-UnknownFunction-result.txt b/Tests/RunCMake/list/TRANSFORM-APPLY-UnknownFunction-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/list/TRANSFORM-APPLY-UnknownFunction-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/list/TRANSFORM-APPLY-UnknownFunction-stderr.txt b/Tests/RunCMake/list/TRANSFORM-APPLY-UnknownFunction-stderr.txt
new file mode 100644
index 0000000..cb59954
--- /dev/null
+++ b/Tests/RunCMake/list/TRANSFORM-APPLY-UnknownFunction-stderr.txt
@@ -0,0 +1,5 @@
+^CMake Error at TRANSFORM-APPLY-UnknownFunction\.cmake:2 \(list\):
+  list sub-command TRANSFORM, action APPLY: unknown function
+  "no_such_function"\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/TRANSFORM-APPLY-UnknownFunction.cmake b/Tests/RunCMake/list/TRANSFORM-APPLY-UnknownFunction.cmake
new file mode 100644
index 0000000..5275439
--- /dev/null
+++ b/Tests/RunCMake/list/TRANSFORM-APPLY-UnknownFunction.cmake
@@ -0,0 +1,2 @@
+set(mylist alpha bravo charlie)
+list(TRANSFORM mylist APPLY no_such_function)
diff --git a/Tests/RunCMake/list/TRANSFORM-APPLY.cmake b/Tests/RunCMake/list/TRANSFORM-APPLY.cmake
index 14fe360..68e1528 100644
--- a/Tests/RunCMake/list/TRANSFORM-APPLY.cmake
+++ b/Tests/RunCMake/list/TRANSFORM-APPLY.cmake
@@ -104,3 +104,28 @@
 if(NOT mylist STREQUAL ";;")
   message(FATAL_ERROR "TRANSFORM(APPLY make_empty) is \"${mylist}\", expected is \";;\"")
 endif()
+
+# Recursive APPLY: an APPLY function that itself calls list(TRANSFORM APPLY).
+# The inner function returns the output variable name it was given.
+function(return_out_var_name in out)
+  set(${out} "${out}" PARENT_SCOPE)
+endfunction()
+
+# The outer function triggers a nested APPLY and verifies that the inner
+# output variable name differs from its own.
+function(inner_name_is_different in out)
+  set(_inner x)
+  list(TRANSFORM _inner APPLY return_out_var_name OUTPUT_VARIABLE _inner_out)
+  # _inner_out now holds the inner output variable name
+  if("${out}" STREQUAL "${_inner_out}")
+    set(${out} "FALSE" PARENT_SCOPE)
+  else()
+    set(${out} "TRUE" PARENT_SCOPE)
+  endif()
+endfunction()
+
+set(mylist a b c)
+list(TRANSFORM mylist APPLY inner_name_is_different OUTPUT_VARIABLE output)
+if(NOT output STREQUAL "TRUE;TRUE;TRUE")
+  message(FATAL_ERROR "TRANSFORM(APPLY nested smoke) is \"${output}\", expected \"TRUE;TRUE;TRUE\"")
+endif()
diff --git a/Tests/RunCMake/list/TRANSFORM-PREDICATE.cmake b/Tests/RunCMake/list/TRANSFORM-PREDICATE.cmake
new file mode 100644
index 0000000..103f021
--- /dev/null
+++ b/Tests/RunCMake/list/TRANSFORM-PREDICATE.cmake
@@ -0,0 +1,100 @@
+# Predicate: returns TRUE for items starting with "b"
+function(starts_with_b input result)
+  if(input MATCHES "^b")
+    set(${result} TRUE PARENT_SCOPE)
+  else()
+    set(${result} FALSE PARENT_SCOPE)
+  endif()
+endfunction()
+
+# Predicate as macro
+macro(is_short input result)
+  string(LENGTH "${input}" _len)
+  if(_len LESS 4)
+    set(${result} TRUE)
+  else()
+    set(${result} FALSE)
+  endif()
+endmacro()
+
+set(mylist alpha bravo charlie bravo_two delta)
+
+# Basic PREDICATE with TOUPPER - only items starting with "b" are uppercased
+list(TRANSFORM mylist TOUPPER PREDICATE starts_with_b OUTPUT_VARIABLE output)
+if(NOT output STREQUAL "alpha;BRAVO;charlie;BRAVO_TWO;delta")
+  message(FATAL_ERROR "TRANSFORM(TOUPPER PREDICATE) is \"${output}\", expected \"alpha;BRAVO;charlie;BRAVO_TWO;delta\"")
+endif()
+
+# Verify original list unchanged (OUTPUT_VARIABLE)
+if(NOT mylist STREQUAL "alpha;bravo;charlie;bravo_two;delta")
+  message(FATAL_ERROR "Original list modified: \"${mylist}\"")
+endif()
+
+# PREDICATE in-place
+list(TRANSFORM mylist TOUPPER PREDICATE starts_with_b)
+if(NOT mylist STREQUAL "alpha;BRAVO;charlie;BRAVO_TWO;delta")
+  message(FATAL_ERROR "TRANSFORM(TOUPPER PREDICATE in-place) is \"${mylist}\", expected \"alpha;BRAVO;charlie;BRAVO_TWO;delta\"")
+endif()
+
+# PREDICATE with macro
+set(mylist ab cde fg hijklm no)
+list(TRANSFORM mylist TOUPPER PREDICATE is_short OUTPUT_VARIABLE output)
+if(NOT output STREQUAL "AB;CDE;FG;hijklm;NO")
+  message(FATAL_ERROR "TRANSFORM(TOUPPER PREDICATE macro) is \"${output}\", expected \"AB;CDE;FG;hijklm;NO\"")
+endif()
+
+# PREDICATE combined with APPLY
+function(add_prefix in out)
+  set(${out} "prefix_${in}" PARENT_SCOPE)
+endfunction()
+
+set(mylist alpha bravo charlie delta)
+list(TRANSFORM mylist APPLY add_prefix PREDICATE starts_with_b OUTPUT_VARIABLE output)
+if(NOT output STREQUAL "alpha;prefix_bravo;charlie;delta")
+  message(FATAL_ERROR "TRANSFORM(APPLY PREDICATE) is \"${output}\", expected \"alpha;prefix_bravo;charlie;delta\"")
+endif()
+
+# PREDICATE on empty list
+set(empty_list "")
+list(TRANSFORM empty_list TOUPPER PREDICATE starts_with_b OUTPUT_VARIABLE output)
+if(NOT output STREQUAL "")
+  message(FATAL_ERROR "TRANSFORM(PREDICATE empty) is \"${output}\", expected \"\"")
+endif()
+
+# PREDICATE where nothing matches (all elements unchanged)
+set(mylist alpha charlie delta)
+list(TRANSFORM mylist TOUPPER PREDICATE starts_with_b OUTPUT_VARIABLE output)
+if(NOT output STREQUAL "alpha;charlie;delta")
+  message(FATAL_ERROR "TRANSFORM(PREDICATE no-match) is \"${output}\", expected \"alpha;charlie;delta\"")
+endif()
+
+# PREDICATE where everything matches
+set(mylist bravo bronze bull)
+list(TRANSFORM mylist TOUPPER PREDICATE starts_with_b OUTPUT_VARIABLE output)
+if(NOT output STREQUAL "BRAVO;BRONZE;BULL")
+  message(FATAL_ERROR "TRANSFORM(PREDICATE all-match) is \"${output}\", expected \"BRAVO;BRONZE;BULL\"")
+endif()
+
+# Recursive PREDICATE: a PREDICATE function that itself calls list(TRANSFORM APPLY).
+# The inner function returns the output variable name it was given.
+function(return_pred_var_name in out)
+  set(${out} "${out}" PARENT_SCOPE)
+endfunction()
+
+# The outer predicate triggers a nested APPLY and verifies that the inner
+# output variable name differs from its own result variable.
+function(inner_pred_name_is_different input result)
+  set(_inner x)
+  list(TRANSFORM _inner APPLY return_pred_var_name OUTPUT_VARIABLE _inner_names)
+  if("${result}" STREQUAL "${_inner_names}")
+    set(${result} FALSE PARENT_SCOPE)
+  else()
+    set(${result} TRUE PARENT_SCOPE)
+  endif()
+endfunction()
+
+set(mylist x y z)
+list(TRANSFORM mylist TOUPPER PREDICATE inner_pred_name_is_different OUTPUT_VARIABLE output)
+if(NOT output STREQUAL "X;Y;Z")
+  message(FATAL_ERROR "TRANSFORM(PREDICATE nested smoke) is \"${output}\", expected \"X;Y;Z\"")
+endif()
diff --git a/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoArguments-result.txt b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoArguments-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoArguments-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoArguments-stderr.txt b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoArguments-stderr.txt
new file mode 100644
index 0000000..22c4133
--- /dev/null
+++ b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoArguments-stderr.txt
@@ -0,0 +1,5 @@
+^CMake Error at TRANSFORM-Selector-PREDICATE-NoArguments\.cmake:2 \(list\):
+  list sub-command TRANSFORM, selector PREDICATE expects 'function name'
+  argument\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoArguments.cmake b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoArguments.cmake
new file mode 100644
index 0000000..f263d8b
--- /dev/null
+++ b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoArguments.cmake
@@ -0,0 +1,2 @@
+set(mylist alpha bravo charlie)
+list(TRANSFORM mylist TOUPPER PREDICATE)
diff --git a/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoOutput-result.txt b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoOutput-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoOutput-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoOutput-stderr.txt b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoOutput-stderr.txt
new file mode 100644
index 0000000..87310b2
--- /dev/null
+++ b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoOutput-stderr.txt
@@ -0,0 +1,5 @@
+^CMake Error at TRANSFORM-Selector-PREDICATE-NoOutput\.cmake:6 \(list\):
+  list sub-command TRANSFORM, selector PREDICATE: function "bad_predicate"
+  did not set the output variable\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoOutput.cmake b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoOutput.cmake
new file mode 100644
index 0000000..73b18c2
--- /dev/null
+++ b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-NoOutput.cmake
@@ -0,0 +1,6 @@
+function(bad_predicate in out)
+  # Deliberately does NOT set ${out}
+endfunction()
+
+set(mylist alpha bravo charlie)
+list(TRANSFORM mylist TOUPPER PREDICATE bad_predicate)
diff --git a/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-UnknownFunction-result.txt b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-UnknownFunction-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-UnknownFunction-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-UnknownFunction-stderr.txt b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-UnknownFunction-stderr.txt
new file mode 100644
index 0000000..21bea51
--- /dev/null
+++ b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-UnknownFunction-stderr.txt
@@ -0,0 +1,5 @@
+^CMake Error at TRANSFORM-Selector-PREDICATE-UnknownFunction\.cmake:2 \(list\):
+  list sub-command TRANSFORM, selector PREDICATE: unknown function
+  "no_such_function"\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-UnknownFunction.cmake b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-UnknownFunction.cmake
new file mode 100644
index 0000000..d497288
--- /dev/null
+++ b/Tests/RunCMake/list/TRANSFORM-Selector-PREDICATE-UnknownFunction.cmake
@@ -0,0 +1,2 @@
+set(mylist alpha bravo charlie)
+list(TRANSFORM mylist TOUPPER PREDICATE no_such_function)