list(SORT): Add COMPARATOR option for user-defined comparisons

Allow users to provide a custom comparison function for `list(SORT)`.
The comparator is validated for strict weak ordering at runtime to
produce a clear CMake error rather than a platform-dependent abort.

Closes: #27761
diff --git a/Help/command/list.rst b/Help/command/list.rst
index a84e2cb..1280c40 100644
--- a/Help/command/list.rst
+++ b/Help/command/list.rst
@@ -399,6 +399,7 @@
 
 .. signature::
   list(SORT <list> [COMPARE <compare>] [CASE <case>] [ORDER <order>])
+  list(SORT <list> COMPARATOR <function> [CASE <case>] [ORDER <order>])
 
   Sorts the list in-place alphabetically.
 
@@ -447,3 +448,47 @@
 
     ``DESCENDING``
       Sorts the list in descending order.
+
+  Instead of the built-in ``COMPARE`` methods, a user-defined comparison
+  function may be used with the ``COMPARATOR`` keyword.
+
+  .. versionadded:: 4.4
+
+  ``COMPARATOR`` is mutually exclusive with ``COMPARE``.  The ``CASE``
+  and ``ORDER`` options may still be used alongside ``COMPARATOR``:
+  the ``CASE`` filter is applied to both values before they are passed to
+  the callable, and ``ORDER DESCENDING`` reverses the comparison by
+  swapping the two arguments.
+
+  The callable must accept exactly three parameters: two input values and
+  the name of an output variable.  It must set the output variable to a
+  boolean value (``TRUE`` if the first value should come before the second,
+  ``FALSE`` otherwise) in the calling scope.
+  If the callable does not set the output variable, it is an error.
+
+  The comparator must define a
+  `strict weak ordering <https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings>`_.
+  In particular:
+
+  * It must return ``FALSE`` when comparing an element to itself
+    (irreflexivity).
+  * If it returns ``TRUE`` for ``(a, b)``, it must return ``FALSE``
+    for ``(b, a)`` (asymmetry).
+
+  An error is raised if the comparator violates these requirements.
+
+  Example:
+
+  .. code-block:: cmake
+
+    function(version_less a b result)
+      if("${a}" VERSION_LESS "${b}")
+        set(${result} TRUE PARENT_SCOPE)
+      else()
+        set(${result} FALSE PARENT_SCOPE)
+      endif()
+    endfunction()
+
+    set(versions 3.1 1.2 2.0 1.10)
+    list(SORT versions COMPARATOR version_less)
+    # versions is now: 1.2;1.10;2.0;3.1
diff --git a/Help/release/dev/list-SORT-COMPARATOR.rst b/Help/release/dev/list-SORT-COMPARATOR.rst
new file mode 100644
index 0000000..61a658d
--- /dev/null
+++ b/Help/release/dev/list-SORT-COMPARATOR.rst
@@ -0,0 +1,6 @@
+list-SORT-COMPARATOR
+--------------------
+
+* The :command:`list(SORT)` command gained a new ``COMPARATOR`` keyword
+  that invokes a user-defined :command:`function` to compare elements, complementing
+  the built-in ``COMPARE`` methods.
diff --git a/Source/cmList.cxx b/Source/cmList.cxx
index 4dfeaa2..5b4eecf 100644
--- a/Source/cmList.cxx
+++ b/Source/cmList.cxx
@@ -167,6 +167,60 @@
   PredicateEvaluator& Evaluator;
   bool IncludeMatches;
 };
+
+class ComparatorEvaluator
+{
+public:
+  ComparatorEvaluator(std::string functionName, cmMakefile& makefile)
+    : FunctionName(std::move(functionName))
+    , Makefile(&makefile)
+    , OutputVar(OutputVarFor("_cmake_comparator_out_", makefile))
+  {
+    if (!makefile.GetState()->GetCommand(this->FunctionName)) {
+      throw cmList::transform_error(
+        cmStrCat("sub-command SORT, COMPARATOR: unknown function \"",
+                 this->FunctionName, "\"."));
+    }
+  }
+
+  bool operator()(std::string const& a, std::string const& b)
+  {
+    this->Makefile->RemoveDefinition(this->OutputVar);
+
+    cmListFileContext context = this->Makefile->GetBacktrace().Top();
+    std::vector<cmListFileArgument> funcArgs;
+    funcArgs.emplace_back(a, cmListFileArgument::Quoted, context.Line);
+    funcArgs.emplace_back(b, 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("sub-command SORT, COMPARATOR: function \"",
+                 this->FunctionName, "\" failed during execution."));
+    }
+
+    cmValue result = this->Makefile->GetDefinition(this->OutputVar);
+    if (!result) {
+      throw cmList::transform_error(
+        cmStrCat("sub-command SORT, COMPARATOR: 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 OutputVar;
+};
 }
 
 cmList& cmList::filter(cm::string_view pattern, FilterMode mode)
@@ -249,6 +303,13 @@
   {
   }
 
+  StringSorter(cmList::SortConfiguration config, ComparisonFunction comparator)
+    : Filters{ nullptr, this->GetCaseFilter(config.Case) }
+    , SortMethod(std::move(comparator))
+    , Descending(config.Order == OrderMode::DESCENDING)
+  {
+  }
+
   std::string ApplyFilter(std::string const& argument)
   {
     std::string result = argument;
@@ -308,6 +369,38 @@
   return *this;
 }
 
+cmList& cmList::sort(SortConfiguration cfg, cmMakefile& makefile)
+{
+  SortConfiguration config{ cfg };
+
+  if (config.Order == SortConfiguration::OrderMode::DEFAULT) {
+    config.Order = SortConfiguration::OrderMode::ASCENDING;
+  }
+  if (config.Case == SortConfiguration::CaseSensitivity::DEFAULT) {
+    config.Case = SortConfiguration::CaseSensitivity::SENSITIVE;
+  }
+
+  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)) {
+          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 "
+            "(b, a).");
+        }
+        return result;
+      });
+    std::sort(this->Values.begin(), this->Values.end(), sorter);
+  } catch (transform_error& e) {
+    throw std::invalid_argument(e.what());
+  }
+
+  return *this;
+}
+
 namespace {
 using transform_type = std::function<std::string(std::string const&)>;
 using transform_error = cmList::transform_error;
diff --git a/Source/cmList.h b/Source/cmList.h
index b34a6ca..cccfb2c 100644
--- a/Source/cmList.h
+++ b/Source/cmList.h
@@ -848,6 +848,7 @@
       STRING,
       FILE_BASENAME,
       NATURAL,
+      COMPARATOR,
     } Compare = CompareMethod::DEFAULT;
 
     enum class CaseSensitivity
@@ -857,6 +858,8 @@
       INSENSITIVE,
     } Case = CaseSensitivity::DEFAULT;
 
+    std::string ComparatorFunction;
+
     // declare the default constructor to work-around clang bug
     SortConfiguration();
 
@@ -869,6 +872,7 @@
     }
   };
   cmList& sort(SortConfiguration config = SortConfiguration{});
+  cmList& sort(SortConfiguration config, cmMakefile& makefile);
 
   // exception raised on error during transform operations
   class transform_error : public std::runtime_error
diff --git a/Source/cmListCommand.cxx b/Source/cmListCommand.cxx
index e5338d2..aed1230 100644
--- a/Source/cmListCommand.cxx
+++ b/Source/cmListCommand.cxx
@@ -726,6 +726,7 @@
 
   using SortConfig = cmList::SortConfiguration;
   SortConfig sortConfig;
+  bool hasComparator = false;
 
   size_t argumentIndex = 2;
   std::string const messageHint = "sub-command SORT ";
@@ -733,6 +734,12 @@
   while (argumentIndex < args.size()) {
     std::string const& option = args[argumentIndex++];
     if (option == "COMPARE") {
+      if (hasComparator) {
+        status.SetError(cmStrCat(messageHint,
+                                 "option \"COMPARE\" is incompatible "
+                                 "with \"COMPARATOR\"."));
+        return false;
+      }
       if (sortConfig.Compare != SortConfig::CompareMethod::DEFAULT) {
         std::string error = cmStrCat(messageHint, "option \"", option,
                                      "\" has been specified multiple times.");
@@ -806,6 +813,27 @@
                                  option, "\"."));
         return false;
       }
+    } else if (option == "COMPARATOR") {
+      if (hasComparator) {
+        status.SetError(cmStrCat(messageHint, "option \"", option,
+                                 "\" has been specified multiple times."));
+        return false;
+      }
+      if (sortConfig.Compare != SortConfig::CompareMethod::DEFAULT) {
+        status.SetError(cmStrCat(messageHint,
+                                 "option \"COMPARATOR\" is incompatible "
+                                 "with \"COMPARE\"."));
+        return false;
+      }
+      if (argumentIndex < args.size()) {
+        sortConfig.ComparatorFunction = args[argumentIndex++];
+        sortConfig.Compare = SortConfig::CompareMethod::COMPARATOR;
+        hasComparator = true;
+      } else {
+        status.SetError(cmStrCat(messageHint, "missing argument for option \"",
+                                 option, "\"."));
+        return false;
+      }
     } else {
       status.SetError(
         cmStrCat(messageHint, "option \"", option, "\" is unknown."));
@@ -821,6 +849,17 @@
     return true;
   }
 
+  if (hasComparator) {
+    try {
+      status.GetMakefile().AddDefinition(
+        listName, list->sort(sortConfig, status.GetMakefile()).to_string());
+      return true;
+    } catch (std::invalid_argument& e) {
+      status.SetError(e.what());
+      return false;
+    }
+  }
+
   status.GetMakefile().AddDefinition(listName,
                                      list->sort(sortConfig).to_string());
   return true;
diff --git a/Tests/RunCMake/list/RunCMakeTest.cmake b/Tests/RunCMake/list/RunCMakeTest.cmake
index 1fd86a9..9705b55 100644
--- a/Tests/RunCMake/list/RunCMakeTest.cmake
+++ b/Tests/RunCMake/list/RunCMakeTest.cmake
@@ -114,9 +114,18 @@
 run_cmake(SORT-DuplicateCompareOption)
 run_cmake(SORT-DuplicateCaseOption)
 run_cmake(SORT-NoCaseOption)
+run_cmake(SORT-COMPARATOR-UnknownFunction)
+run_cmake(SORT-COMPARATOR-NoOutput)
+run_cmake(SORT-COMPARATOR-CompareConflict)
+run_cmake(SORT-COMPARATOR-CompareConflictReverse)
+run_cmake(SORT-COMPARATOR-NoFunction)
+run_cmake(SORT-COMPARATOR-DuplicateOption)
+run_cmake(SORT-COMPARATOR-NotStrictWeak)
+run_cmake(SORT-COMPARATOR-NotStrictWeakLate)
 
 # Successful tests
 run_cmake(SORT)
+run_cmake(SORT-COMPARATOR)
 
 # argument tests
 run_cmake(PREPEND-NoArgs)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict-result.txt b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict-stderr.txt b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict-stderr.txt
new file mode 100644
index 0000000..0224d8c
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict-stderr.txt
@@ -0,0 +1,4 @@
+^CMake Error at SORT-COMPARATOR-CompareConflict\.cmake:10 \(list\):
+  list sub-command SORT option "COMPARATOR" is incompatible with "COMPARE"\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict.cmake b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict.cmake
new file mode 100644
index 0000000..2e62821
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict.cmake
@@ -0,0 +1,10 @@
+function(my_cmp a b out)
+  if("${a}" STRLESS "${b}")
+    set(${out} TRUE PARENT_SCOPE)
+  else()
+    set(${out} FALSE PARENT_SCOPE)
+  endif()
+endfunction()
+
+set(mylist c a b)
+list(SORT mylist COMPARE STRING COMPARATOR my_cmp)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse-result.txt b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse-stderr.txt b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse-stderr.txt
new file mode 100644
index 0000000..30659dc
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse-stderr.txt
@@ -0,0 +1,4 @@
+^CMake Error at SORT-COMPARATOR-CompareConflictReverse\.cmake:10 \(list\):
+  list sub-command SORT option "COMPARE" is incompatible with "COMPARATOR"\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse.cmake b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse.cmake
new file mode 100644
index 0000000..c08314d
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse.cmake
@@ -0,0 +1,10 @@
+function(my_cmp a b out)
+  if("${a}" STRLESS "${b}")
+    set(${out} TRUE PARENT_SCOPE)
+  else()
+    set(${out} FALSE PARENT_SCOPE)
+  endif()
+endfunction()
+
+set(mylist c a b)
+list(SORT mylist COMPARATOR my_cmp COMPARE STRING)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption-result.txt b/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption-stderr.txt b/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption-stderr.txt
new file mode 100644
index 0000000..b387e8e
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption-stderr.txt
@@ -0,0 +1,5 @@
+^CMake Error at SORT-COMPARATOR-DuplicateOption\.cmake:10 \(list\):
+  list sub-command SORT option "COMPARATOR" has been specified multiple
+  times\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption.cmake b/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption.cmake
new file mode 100644
index 0000000..418a843
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption.cmake
@@ -0,0 +1,10 @@
+function(my_cmp a b out)
+  if("${a}" STRLESS "${b}")
+    set(${out} TRUE PARENT_SCOPE)
+  else()
+    set(${out} FALSE PARENT_SCOPE)
+  endif()
+endfunction()
+
+set(mylist c a b)
+list(SORT mylist COMPARATOR my_cmp COMPARATOR my_cmp)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction-result.txt b/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction-stderr.txt b/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction-stderr.txt
new file mode 100644
index 0000000..7b74e3b
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction-stderr.txt
@@ -0,0 +1,4 @@
+^CMake Error at SORT-COMPARATOR-NoFunction\.cmake:2 \(list\):
+  list sub-command SORT missing argument for option "COMPARATOR"\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction.cmake b/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction.cmake
new file mode 100644
index 0000000..0c834f1
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction.cmake
@@ -0,0 +1,2 @@
+set(mylist a b c)
+list(SORT mylist COMPARATOR)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput-result.txt b/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput-stderr.txt b/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput-stderr.txt
new file mode 100644
index 0000000..af5e3cc
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput-stderr.txt
@@ -0,0 +1,5 @@
+^CMake Error at SORT-COMPARATOR-NoOutput\.cmake:6 \(list\):
+  list sub-command SORT, COMPARATOR: function "bad_comparator" did not set
+  the output variable\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput.cmake b/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput.cmake
new file mode 100644
index 0000000..9c279f8
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput.cmake
@@ -0,0 +1,6 @@
+function(bad_comparator a b out)
+  # Deliberately does NOT set ${out}
+endfunction()
+
+set(mylist c a b)
+list(SORT mylist COMPARATOR bad_comparator)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak-result.txt b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak-stderr.txt b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak-stderr.txt
new file mode 100644
index 0000000..2978265
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak-stderr.txt
@@ -0,0 +1,5 @@
+^CMake Error at SORT-COMPARATOR-NotStrictWeak\.cmake:6 \(list\):
+  list sub-command SORT, COMPARATOR: function does not induce a strict weak
+  ordering\.  The comparator returned TRUE for both \(a, b\) and \(b, a\)\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak.cmake b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak.cmake
new file mode 100644
index 0000000..bf7e88f
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak.cmake
@@ -0,0 +1,6 @@
+function(reflexive_comparator a b out)
+  set(${out} TRUE PARENT_SCOPE)
+endfunction()
+
+set(mylist c a b)
+list(SORT mylist COMPARATOR reflexive_comparator)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate-result.txt b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate-stderr.txt b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate-stderr.txt
new file mode 100644
index 0000000..6188b8d
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate-stderr.txt
@@ -0,0 +1,5 @@
+^CMake Error at SORT-COMPARATOR-NotStrictWeakLate\.cmake:19 \(list\):
+  list sub-command SORT, COMPARATOR: function does not induce a strict weak
+  ordering\.  The comparator returned TRUE for both \(a, b\) and \(b, a\)\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate.cmake b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate.cmake
new file mode 100644
index 0000000..c0193ed
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate.cmake
@@ -0,0 +1,19 @@
+# Comparator that is correct for most pairs but violates strict weak ordering
+# when both elements start with "b".
+function(late_violation a b result)
+  string(SUBSTRING "${a}" 0 1 a_first)
+  string(SUBSTRING "${b}" 0 1 b_first)
+  if(a_first STREQUAL "b" AND b_first STREQUAL "b")
+    # Violates asymmetry: always returns TRUE for b-vs-b pairs
+    set(${result} TRUE PARENT_SCOPE)
+  else()
+    if("${a}" STRLESS "${b}")
+      set(${result} TRUE PARENT_SCOPE)
+    else()
+      set(${result} FALSE PARENT_SCOPE)
+    endif()
+  endif()
+endfunction()
+
+set(mylist "a" "c" "bb" "ba")
+list(SORT mylist COMPARATOR late_violation)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction-result.txt b/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction-result.txt
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction-stderr.txt b/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction-stderr.txt
new file mode 100644
index 0000000..e5c9543
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction-stderr.txt
@@ -0,0 +1,4 @@
+^CMake Error at SORT-COMPARATOR-UnknownFunction\.cmake:2 \(list\):
+  list sub-command SORT, COMPARATOR: unknown function "no_such_function"\.
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction.cmake b/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction.cmake
new file mode 100644
index 0000000..8e5901b
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction.cmake
@@ -0,0 +1,2 @@
+set(mylist a b c)
+list(SORT mylist COMPARATOR no_such_function)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR.cmake b/Tests/RunCMake/list/SORT-COMPARATOR.cmake
new file mode 100644
index 0000000..0303846
--- /dev/null
+++ b/Tests/RunCMake/list/SORT-COMPARATOR.cmake
@@ -0,0 +1,135 @@
+# Comparator: returns TRUE if a < b (string comparison)
+function(string_less a b result)
+  if("${a}" STRLESS "${b}")
+    set(${result} TRUE PARENT_SCOPE)
+  else()
+    set(${result} FALSE PARENT_SCOPE)
+  endif()
+endfunction()
+
+# Comparator as macro
+macro(string_less_macro a b result)
+  if(${a} STRLESS ${b})
+    set(${result} TRUE)
+  else()
+    set(${result} FALSE)
+  endif()
+endmacro()
+
+# Comparator: string length comparison
+function(shorter_first a b result)
+  string(LENGTH "${a}" len_a)
+  string(LENGTH "${b}" len_b)
+  if(len_a LESS len_b)
+    set(${result} TRUE PARENT_SCOPE)
+  else()
+    set(${result} FALSE PARENT_SCOPE)
+  endif()
+endfunction()
+
+## Basic COMPARATOR with function
+set(mylist c a b)
+list(SORT mylist COMPARATOR string_less)
+if(NOT mylist STREQUAL "a;b;c")
+  message(FATAL_ERROR "SORT(COMPARATOR function) is \"${mylist}\", expected \"a;b;c\"")
+endif()
+
+## COMPARATOR with macro
+set(mylist c a b)
+list(SORT mylist COMPARATOR string_less_macro)
+if(NOT mylist STREQUAL "a;b;c")
+  message(FATAL_ERROR "SORT(COMPARATOR macro) is \"${mylist}\", expected \"a;b;c\"")
+endif()
+
+## COMPARATOR with ORDER DESCENDING
+set(mylist c a b)
+list(SORT mylist COMPARATOR string_less ORDER DESCENDING)
+if(NOT mylist STREQUAL "c;b;a")
+  message(FATAL_ERROR "SORT(COMPARATOR ORDER DESCENDING) is \"${mylist}\", expected \"c;b;a\"")
+endif()
+
+## COMPARATOR with CASE INSENSITIVE
+# With CASE INSENSITIVE, values are lowercased before being passed to the comparator
+set(mylist "C" "a" "B")
+list(SORT mylist COMPARATOR string_less CASE INSENSITIVE)
+if(NOT mylist STREQUAL "a;B;C")
+  message(FATAL_ERROR "SORT(COMPARATOR CASE INSENSITIVE) is \"${mylist}\", expected \"a;B;C\"")
+endif()
+
+## COMPARATOR with CASE INSENSITIVE and ORDER DESCENDING
+set(mylist "C" "a" "B")
+list(SORT mylist COMPARATOR string_less CASE INSENSITIVE ORDER DESCENDING)
+if(NOT mylist STREQUAL "C;B;a")
+  message(FATAL_ERROR "SORT(COMPARATOR CASE INSENSITIVE ORDER DESCENDING) is \"${mylist}\", expected \"C;B;a\"")
+endif()
+
+## Custom comparator (sort by string length)
+set(mylist "bbb" "a" "cc")
+list(SORT mylist COMPARATOR shorter_first)
+if(NOT mylist STREQUAL "a;cc;bbb")
+  message(FATAL_ERROR "SORT(COMPARATOR shorter_first) is \"${mylist}\", expected \"a;cc;bbb\"")
+endif()
+
+## Custom comparator with ORDER DESCENDING (sort by length descending)
+set(mylist "bbb" "a" "cc")
+list(SORT mylist COMPARATOR shorter_first ORDER DESCENDING)
+if(NOT mylist STREQUAL "bbb;cc;a")
+  message(FATAL_ERROR "SORT(COMPARATOR shorter_first ORDER DESCENDING) is \"${mylist}\", expected \"bbb;cc;a\"")
+endif()
+
+## Empty list (no-op)
+set(empty_list "")
+list(SORT empty_list COMPARATOR string_less)
+if(NOT empty_list STREQUAL "")
+  message(FATAL_ERROR "SORT(COMPARATOR empty) is \"${empty_list}\", expected \"\"")
+endif()
+
+## Single element list (no-op)
+set(mylist "only")
+list(SORT mylist COMPARATOR string_less)
+if(NOT mylist STREQUAL "only")
+  message(FATAL_ERROR "SORT(COMPARATOR single) is \"${mylist}\", expected \"only\"")
+endif()
+
+## Already sorted list
+set(mylist a b c)
+list(SORT mylist COMPARATOR string_less)
+if(NOT mylist STREQUAL "a;b;c")
+  message(FATAL_ERROR "SORT(COMPARATOR already-sorted) is \"${mylist}\", expected \"a;b;c\"")
+endif()
+
+## Recursive COMPARATOR - verify inner and outer output variables are distinct
+# Inner comparator: checks that neither element equals its own output variable
+# name (which would mean the outer and inner names collided), then performs a
+# valid string comparison to satisfy strict weak ordering.
+function(check_outer_name a b out)
+  if("${a}" STREQUAL "${out}")
+    message(FATAL_ERROR "Recursive COMPARATOR: inner and outer output variable names are the same: ${out}")
+  endif()
+  if("${b}" STREQUAL "${out}")
+    message(FATAL_ERROR "Recursive COMPARATOR: inner and outer output variable names are the same: ${out}")
+  endif()
+  if("${a}" STRLESS "${b}")
+    set(${out} TRUE PARENT_SCOPE)
+  else()
+    set(${out} FALSE PARENT_SCOPE)
+  endif()
+endfunction()
+
+# Outer comparator: passes its own output variable name into a nested sort
+# as a list element, so the inner comparator can compare the two names.
+function(nested_comparator a b out)
+  set(_inner "${out}" dummy)
+  list(SORT _inner COMPARATOR check_outer_name)
+  if("${a}" STRLESS "${b}")
+    set(${out} TRUE PARENT_SCOPE)
+  else()
+    set(${out} FALSE PARENT_SCOPE)
+  endif()
+endfunction()
+
+set(mylist c a b)
+list(SORT mylist COMPARATOR nested_comparator)
+if(NOT mylist STREQUAL "a;b;c")
+  message(FATAL_ERROR "SORT(COMPARATOR nested) is \"${mylist}\", expected \"a;b;c\"")
+endif()