Repository: microsoft/STL (C++)

Issue:
LWG-4186 `regex_traits::transform_primary` mistakenly detects `typeid` of a function
LWG-4186 `regex_traits::transform_primary` mistakenly detects `typeid` of a function

`<regex>`: Equivalence classes have unexpected behavior with `std::wregex`
Repros with VS 2022 17.14 Preview 4 with microsoft/STL `main`, including #5392. Tracked by internal VSO-127463 / AB#127463 , originally reported by an external user through the defunct Microsoft Connect site on 2015-06-02.

```
C:\Temp>type meow.cpp
```
```cpp
#include <locale>
#include <print>
#include <regex>
#include <string>
using namespace std;

[[nodiscard]] string escape_wide(const wstring& wstr) {
    string ret{R"(L")"};
    for (const auto& wch : wstr) {
        ret += format(R"(\x{:x})", static_cast<unsigned short>(wch));
    }
    ret += R"(")";
    return ret;
}

void display_result(const wstring& wstr, const wstring& pattern) {
    const locale loc{"fr-FR"};

    wregex rgx;
    rgx.imbue(loc);
    rgx.assign(pattern, regex_constants::icase | regex_constants::collate);

    const bool result = regex_match(wstr, rgx);

    regex_traits<wchar_t> tr;
    tr.imbue(loc);
    const wstring primary_sort_key = tr.transform_primary(wstr.begin(), wstr.end());

    println("wstr: {}; result: {:>5}; primary_sort_key: {}", escape_wide(wstr), result, escape_wide(primary_sort_key));
}

int main() {
    display_result(L"E", L"[[=e=]]");
    display_result(L"\u00C8", L"[[=e=]]"); // LATIN CAPITAL LETTER E WITH GRAVE
    display_result(L"\u00C9", L"[[=e=]]"); // LATIN CAPITAL LETTER E WITH ACUTE
    display_result(L"\u00CA", L"[[=e=]]"); // LATIN CAPITAL LETTER E WITH CIRCUMFLEX

    display_result(L"e", L"[[=e=]]");
    display_result(L"\u00E8", L"[[=e=]]"); // LATIN SMALL LETTER E WITH GRAVE
    display_result(L"\u00E9", L"[[=e=]]"); // LATIN SMALL LETTER E WITH ACUTE
    display_result(L"\u00EA", L"[[=e=]]"); // LATIN SMALL LETTER E WITH CIRCUMFLEX
}
```
```
C:\Temp>cl /EHsc /nologo /W4 /std:c++latest /MTd /Od meow.cpp && meow
meow.cpp
wstr: L"\x45"; result:  true; primary_sort_key: L"\xe\x21\x1\x1\x1\x1\x0"
wstr: L"\xc8"; result: false; primary_sort_key: L"\xe\x21\x1\xf\x1\x1\x1\x0"
wstr: L"\xc9"; result: false; primary_sort_key: L"\xe\x21\x1\xe\x1\x1\x1\x0"
wstr: L"\xca"; result: false; primary_sort_key: L"\xe\x21\x1\x12\x1\x1\x1\x0"
wstr: L"\x65"; result:  true; primary_sort_key: L"\xe\x21\x1\x1\x1\x1\x0"
wstr: L"\xe8"; result: false; primary_sort_key: L"\xe\x21\x1\xf\x1\x1\x1\x0"
wstr: L"\xe9"; result: false; primary_sort_key: L"\xe\x21\x1\xe\x1\x1\x1\x0"
wstr: L"\xea"; result: false; primary_sort_key: L"\xe\x21\x1\x12\x1\x1\x1\x0"
```

The user expects `regex_match` to always return `true` here.

I don't understand why [`LCMapStringEx`](https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-lcmapstringex) with `LCMAP_SORTKEY` is producing these primary sort keys. Are we supposed to be passing extra flags to ignore diacritics?


Discussion:
More generally, `regex_traits::transform_primary()` currently implements the general traits requirement in [\[re.req\]/20](https://eel.is/c++draft/re.req#20) and not the specified implementation in [\[re.traits\]/7](https://eel.is/c++draft/re.traits#7). Besides the missing comparison with `typeid`, this also means that diacritics are not properly handled in (most?) non-C locales.

If we want to implement the specification, we should probably add variants of `_Strxfrm()` and `_Wcsxfrm()` to the import library that call `__crtLCMapStringA/W` with appropriate flags for non-C locales (maybe `LCMAP_SORTKEY | LINGUISTIC_IGNORECASE | LINGUISTIC_IGNOREDIACRITIC | NORM_IGNOREKANA | NORM_IGNOREWIDTH` or something similar).

If we add these new variants of `_Strxfrm()` and  `_Wcsxfrm()`, we should make sure that their return values are consistent unlike the current implementations of`_Strxfrm()` and `_Wcxsfrm()` (https://github.com/microsoft/STL/issues/5210).
`std::locale` doesn't actually construct any facets of type `std::collate_byname<charT>`. It constructs facets of type `std::collate<charT>` instead. See: https://godbolt.org/z/Te4rcM173

So comparing with `typeid(std::collate_byname<charT>)` only doesn't make sense. We either have to correct the facets constructed by `std::locale` (would this break ABI?) or compare with `typeid(std::collate<charT>)` as well.
> We either have to correct the facets constructed by `std::locale` (would this break ABI?)

I don't understand `std::locale` well enough to say whether it would be binary-compatible. All I know is that `std::locale` is the most fragile part of the library and changes have had a high risk of damaging it, even back when we could break ABI every major version.

> Are we supposed to be passing extra flags to ignore diacritics?

Yes.

For purposes of collation order, a character is assigned a number of sorting weights. Some of these underlying sorting weights also leak into the description of `LCMapStringEx`. For example, the description for the `NORM_IGNORENONSPACE` flag states:

> NORM_IGNORENONSPACE ignores any secondary distinction, whether it is a diacritic or not. [...] LINGUISTIC_IGNOREDIACRITIC causes the function to ignore only actual diacritics, instead of ignoring the *second sorting weight*.

`transform()` is supposed to produce a sort key that considers all of these sorting weights. 

 `transform_primary()`, however, is supposed to return a primary sort key. A primary sort key is derived from the primary (first) sorting weight only while ignoring all the others. That said, we probably want to choose linguistically more accurate options for a particular locale than to just neglect all the non-primary weights. 

I already wrote in https://github.com/microsoft/STL/issues/5291#issuecomment-2745283769:

> If we want to implement the specification, we should probably add variants of `_Strxfrm()` and `_Wcsxfrm()` to the import library that call __crtLCMapStringA/W with appropriate flags for non-C locales (maybe `LCMAP_SORTKEY | LINGUISTIC_IGNORECASE | LINGUISTIC_IGNOREDIACRITIC | NORM_IGNOREKANA | NORM_IGNOREWIDTH` or something similar).


For the "C" locale, the best we can probably do is to convert all characters to lower or upper case and return that as the sort key.
Since equivalence classes are inherited from POSIX, I checked what the POSIX standard says about the sorting weights in the C locale (or, more accurately, the POSIX locale). My reading is that all characters should be assigned a unique primary weight. This would mean that even "a" and "A" do not have the same primary sort key. A quick test with grep seems to confirm that.

However, this is somewhat inconsistent with the general traits requirements of `transform_primary()` in [\[re.req\]/20](https://eel.is/c++draft/re.req#20) that require that `transform_primary()` must not consider character case.

I assumed these general traits requirements when I implemented #5392. If these are actually not true, then the implementation has to be fixed in `icase` mode.
I think the traits requirement in  [\[re.req\]/20](https://eel.is/c++draft/re.req#20) is wrong for the C locale and the primary sort key according to POSIX is correct.

In the C locale, this is the collation order for alphabetic characters:

```
A <= B <= ... <= Z <= ... <= a <= b <= ... <= z 
```

This collation order based on all sorting weights should be consistent with the order that is based on the primary weight only. This means that all these relations should also hold when we compare primary sort keys.

If we also impose that primary sort keys compare equal for lower and upper case versions of the same character, we get

```
A = B = ... = Z = ... = a = b = ... = z
```

So all alphabetic characters would be assigned the same primary sort key. This is clearly not what we want.

This means that we also have to fix the processing of equivalences in the parser by adding a character translation phase before calling `transform_primary()`.



Patch:
diff --git a/stl/CMakeLists.txt b/stl/CMakeLists.txt
index ac17fb0a55f..10d21b3173c 100644
--- a/stl/CMakeLists.txt
+++ b/stl/CMakeLists.txt
@@ -213,6 +213,7 @@ set(IMPLIB_SOURCES
     ${CMAKE_CURRENT_LIST_DIR}/src/locale0_implib.cpp
     ${CMAKE_CURRENT_LIST_DIR}/src/nothrow.cpp
     ${CMAKE_CURRENT_LIST_DIR}/src/print.cpp
+    ${CMAKE_CURRENT_LIST_DIR}/src/regex.cpp
     ${CMAKE_CURRENT_LIST_DIR}/src/sharedmutex.cpp
     ${CMAKE_CURRENT_LIST_DIR}/src/stacktrace.cpp
     ${CMAKE_CURRENT_LIST_DIR}/src/syserror_import_lib.cpp
diff --git a/stl/inc/locale b/stl/inc/locale
index cc08f4d4eee..7e75f4d7634 100644
--- a/stl/inc/locale
+++ b/stl/inc/locale
@@ -94,6 +94,9 @@ inline size_t __CRTDECL _LStrxfrm(_Out_writes_(_Last1 - _First1) _Post_readable_
 }
 #endif // defined(_CRTBLD)
 
+template <class _Elem>
+class _Regex_traits;
+
 _EXPORT_STD template <class _Elem>
 class collate : public locale::facet { // facet for ordering sequences of elements
 public:
@@ -189,6 +192,8 @@ protected:
 
 private:
     _Locinfo::_Collvec _Coll; // used by _LStrcoll and _XStrxfrm
+
+    friend _Regex_traits<_Elem>;
 };
 
 #ifdef __clang__
diff --git a/stl/inc/regex b/stl/inc/regex
index b63f7e83066..cdfefd0cddf 100644
--- a/stl/inc/regex
+++ b/stl/inc/regex
@@ -53,6 +53,17 @@ _STL_DISABLE_CLANG_WARNINGS
 #endif // ^^^ !defined(_DEBUG) ^^^
 #endif // !defined(_ENHANCED_REGEX_VISUALIZER)
 
+#if defined(_CPPRTTI) && !defined(_M_CEE_PURE)
+extern "C" {
+_STD size_t __stdcall __std_regex_transform_primary_char(
+    _Out_writes_(_Last1 - _First1) _Post_readable_size_(return) char* _First1, char* _Last1,
+    _In_reads_(_Last2 - _First2) const char* _First2, const char* _Last2, _In_opt_ const _Collvec*) noexcept;
+_STD size_t __stdcall __std_regex_transform_primary_wchar_t(
+    _Out_writes_(_Last1 - _First1) _Post_readable_size_(return) wchar_t* _First1, wchar_t* _Last1,
+    _In_reads_(_Last2 - _First2) const wchar_t* _First2, const wchar_t* _Last2, _In_opt_ const _Collvec*) noexcept;
+} // extern "C"
+#endif // ^^^ defined(_CPPRTTI) && !defined(_M_CEE_PURE) ^^^
+
 _STD_BEGIN
 
 enum _Meta_type : int { // meta character representations for parser
@@ -267,6 +278,20 @@ struct _Regex_traits_base { // base of all regular expression traits
     using char_class_type = ctype_base::mask;
 };
 
+#if defined(_CPPRTTI) && !defined(_M_CEE_PURE)
+inline size_t _Regex_transform_primary(_Out_writes_(_Last1 - _First1) _Post_readable_size_(return) char* _First1,
+    char* _Last1, _In_reads_(_Last2 - _First2) const char* _First2, const char* _Last2,
+    _In_opt_ const _Locinfo::_Collvec* _Vector) noexcept {
+    return __std_regex_transform_primary_char(_First1, _Last1, _First2, _Last2, _Vector);
+}
+
+inline size_t _Regex_transform_primary(_Out_writes_(_Last1 - _First1) _Post_readable_size_(return) wchar_t* _First1,
+    wchar_t* _Last1, _In_reads_(_Last2 - _First2) const wchar_t* _First2, const wchar_t* _Last2,
+    _In_opt_ const _Locinfo::_Collvec* _Vector) noexcept {
+    return __std_regex_transform_primary_wchar_t(_First1, _Last1, _First2, _Last2, _Vector);
+}
+#endif // ^^^ defined(_CPPRTTI) && !defined(_M_CEE_PURE) ^^^
+
 template <class _Elem>
 class _Regex_traits : public _Regex_traits_base { // base class for regular expression traits
 public:
@@ -312,13 +337,38 @@ public:
     string_type transform_primary(_FwdIt _First, _FwdIt _Last) const {
         // apply locale-specific case-insensitive transformation
         string_type _Res;
-
-        if (_First != _Last) { // non-empty string, transform it
-            vector<_Elem> _Temp(_First, _Last);
-
-            _Getctype()->tolower(_Temp.data(), _Temp.data() + _Temp.size());
-            _Res = _Getcoll()->transform(_Temp.data(), _Temp.data() + _Temp.size());
+#if defined(_CPPRTTI) && !defined(_M_CEE_PURE)
+        if (_First != _Last) {
+            const collate<_Elem>* _Coll = _Getcoll();
+            const auto& _Coll_type      = typeid(*_Coll);
+            // TRANSITION, ABI: GH-5394: locale creates collate objects of type collate, not collate_byname.
+            // Depending on the resolution of LWG-2338, comparison to typeid(collate) might also become
+            // required by the standard.
+            if (_Coll_type == typeid(collate_byname<_Elem>) || _Coll_type == typeid(collate<_Elem>)) {
+                // non-empty string with known collate facet, transform it
+                const string_type _Src(_First, _Last);
+                const auto _Src_first = _Src.data();
+                const auto _Src_last  = _Src_first + _Src.size();
+
+                size_t _Count = _Src.size();
+                while (_Res.size() < _Count) {
+                    _Res.resize(_Count);
+                    _Count = _STD _Regex_transform_primary(
+                        &_Res[0], &_Res[0] + _Count, _Src_first, _Src_last, &_Coll->_Coll);
+
+                    if (_Count == static_cast<size_t>(-1)) {
+                        // return empty string in case of error
+                        _Count = 0;
+                        break;
+                    }
+                }
+                _Res.resize(_Count);
+            }
         }
+#else // ^^^ defined(_CPPRTTI) && !defined(_M_CEE_PURE) / !defined(_CPPRTTI) || defined(_M_CEE_PURE) vvv
+        (void) _First;
+        (void) _Last;
+#endif // ^^^ !defined(_CPPRTTI) || defined(_M_CEE_PURE) ^^^
         return _Res;
     }
 
@@ -4211,26 +4261,30 @@ _Prs_ret _Parser<_FwdIt, _Elem, _RxTraits>::_Do_ex_class2(
 
         _Elem* const _Coll_elem_first      = &_Coll_elem.front();
         const _Elem* const _Coll_elem_last = _Coll_elem_first + _Size;
+
+        if (_Size == 1 && _End_arg == _Meta_dot) {
+            // process single-element collating elements like individual characters
+            _Val = *_Coll_elem_first;
+            return _Prs_chr;
+        }
+
+        if (_Flags & regex_constants::icase) {
+            for (auto _Current = _Coll_elem_first; _Current != _Coll_elem_last; ++_Current) {
+                *_Current = _Traits.translate_nocase(*_Current);
+            }
+        } else if (_Flags & regex_constants::collate) {
+            for (auto _Current = _Coll_elem_first; _Current != _Coll_elem_last; ++_Current) {
+                *_Current = _Traits.translate(*_Current);
+            }
+        }
+
         if (_End_arg == _Meta_equal) { // process equivalence
             _Nfa._Add_equiv2(_Coll_elem_first, _Coll_elem_last);
             return _Prs_set;
         } else { // process collating element
-            if (_Size == 1) {
-                _Val = *_Coll_elem_first;
-                return _Prs_chr;
-            }
 
             // Character ranges with multi-character bounds cannot be represented in NFA nodes yet (see GH-5391).
             // Provisionally treat multi-character collating elements as character sets.
-            if (_Flags & regex_constants::icase) {
-                for (auto _Current = _Coll_elem_first; _Current != _Coll_elem_last; ++_Current) {
-                    *_Current = _Traits.translate_nocase(*_Current);
-                }
-            } else if (_Flags & regex_constants::collate) {
-                for (auto _Current = _Coll_elem_first; _Current != _Coll_elem_last; ++_Current) {
-                    *_Current = _Traits.translate(*_Current);
-                }
-            }
             _Nfa._Add_coll2(_Coll_elem_first, _Coll_elem_last);
             return _Prs_set;
         }
diff --git a/stl/inc/yvals.h b/stl/inc/yvals.h
index f6c6d4a2288..0ac04d6b4a4 100644
--- a/stl/inc/yvals.h
+++ b/stl/inc/yvals.h
@@ -3,7 +3,8 @@
 // Copyright (c) Microsoft Corporation.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
-// This header is used to compile the import library (via locale0_implib.cpp => locale0.cpp => xfacet => yvals.h).
+// This header is used to compile the import library
+// (via locale0_implib.cpp => locale0.cpp => xfacet => yvals.h and regex.cpp => awint.hpp => yvals.h).
 // MAJOR LIMITATIONS apply to what can be included here!
 // Before editing this file, read: /docs/import_library.md
 
diff --git a/stl/msbuild/stl_base/stl.files.settings.targets b/stl/msbuild/stl_base/stl.files.settings.targets
index 9b7db156955..0669a4c0a59 100644
--- a/stl/msbuild/stl_base/stl.files.settings.targets
+++ b/stl/msbuild/stl_base/stl.files.settings.targets
@@ -158,6 +158,7 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
             $(CrtRoot)\github\stl\src\locale0_implib.cpp;
             $(CrtRoot)\github\stl\src\nothrow.cpp;
             $(CrtRoot)\github\stl\src\print.cpp;
+            $(CrtRoot)\github\stl\src\regex.cpp;
             $(CrtRoot)\github\stl\src\sharedmutex.cpp;
             $(CrtRoot)\github\stl\src\stacktrace.cpp;
             $(CrtRoot)\github\stl\src\syserror_import_lib.cpp;
diff --git a/stl/src/awint.hpp b/stl/src/awint.hpp
index 797801b7a98..da88c79bc7d 100644
--- a/stl/src/awint.hpp
+++ b/stl/src/awint.hpp
@@ -2,6 +2,11 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 // Internal definitions for A&W Win32 wrapper routines.
+
+// This file is compiled into the import library (via regex.cpp => awint.hpp).
+// MAJOR LIMITATIONS apply to what can be included here!
+// Before editing this file, read: /docs/import_library.md
+
 #pragma once
 
 #include <yvals.h>
diff --git a/stl/src/regex.cpp b/stl/src/regex.cpp
new file mode 100644
index 00000000000..6ecd7744cd7
--- /dev/null
+++ b/stl/src/regex.cpp
@@ -0,0 +1,128 @@
+// Copyright (c) Microsoft Corporation.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+// This file is compiled into the import library.
+// MAJOR LIMITATIONS apply to what can be included here!
+// Before editing this file, read: /docs/import_library.md
+
+#include <__msvc_xlocinfo_types.hpp>
+#include <clocale>
+#include <crtdefs.h>
+#include <cstdlib>
+#include <cstring>
+#include <internal_shared.h>
+
+#include <Windows.h>
+
+#undef _ENFORCE_ONLY_CORE_HEADERS
+#include "awint.hpp"
+
+extern "C" {
+
+// derived from xstrxfrm.cpp
+size_t __stdcall __std_regex_transform_primary_char(
+    _Out_writes_(end1 - string1) _Post_readable_size_(return) char* string1, char* end1,
+    _In_reads_(end2 - string2) const char* string2, const char* end2, _In_opt_ const _Collvec* ploc) noexcept {
+    size_t n1     = end1 - string1;
+    size_t n2     = end2 - string2;
+    size_t retval = static_cast<size_t>(-1);
+    UINT codepage;
+    const wchar_t* locale_name;
+
+    if (ploc == nullptr) {
+        locale_name = ___lc_locale_name_func()[LC_COLLATE];
+        codepage    = ___lc_collate_cp_func();
+    } else {
+        locale_name = ploc->_LocaleName;
+        codepage    = ploc->_Page;
+    }
+
+    if (locale_name == nullptr && codepage == CP_ACP) {
+        if (n2 <= n1) {
+            memcpy(string1, string2, n2);
+        }
+        retval = n2;
+    } else {
+        // Inquire size of dst string in BYTES
+        const int dstlen = __crtLCMapStringA(locale_name,
+            LCMAP_SORTKEY | LINGUISTIC_IGNORECASE | LINGUISTIC_IGNOREDIACRITIC | NORM_IGNOREKANATYPE | NORM_IGNOREWIDTH,
+            string2, static_cast<int>(n2), nullptr, 0, codepage, TRUE);
+
+        if (dstlen != 0) {
+            retval = dstlen;
+
+            // if not enough room, return amount needed
+            if (dstlen <= static_cast<int>(n1)) {
+                // Map src string to dst string
+                __crtLCMapStringA(locale_name,
+                    LCMAP_SORTKEY | LINGUISTIC_IGNORECASE | LINGUISTIC_IGNOREDIACRITIC | NORM_IGNOREKANATYPE
+                        | NORM_IGNOREWIDTH,
+                    string2, static_cast<int>(n2), string1, static_cast<int>(n1), codepage, TRUE);
+            }
+        }
+    }
+
+    return retval;
+}
+
+// derived from xwcsxfrm.cpp
+size_t __stdcall __std_regex_transform_primary_wchar_t(
+    _Out_writes_(end1 - string1) _Post_readable_size_(return) wchar_t* string1, wchar_t* end1,
+    _In_reads_(end2 - string2) const wchar_t* string2, const wchar_t* end2, _In_opt_ const _Collvec* ploc) noexcept {
+    size_t n1   = end1 - string1;
+    size_t n2   = end2 - string2;
+    size_t size = static_cast<size_t>(-1);
+    const wchar_t* locale_name;
+
+    if (ploc == nullptr) {
+        locale_name = ___lc_locale_name_func()[LC_COLLATE];
+    } else {
+        locale_name = ploc->_LocaleName;
+    }
+
+    if (locale_name == nullptr) {
+        if (n2 <= n1) {
+            memcpy(string1, string2, n2 * sizeof(wchar_t));
+        }
+        size = n2;
+    } else {
+        // When using LCMAP_SORTKEY, LCMapStringW handles BYTES not wide
+        // chars. We use a byte buffer to hold bytes and then convert the
+        // byte string to a wide char string and return this so it can be
+        // compared using wcscmp(). User's buffer is n1 wide chars, so
+        // use an internal buffer of n1 bytes.
+
+        auto bbuffer = _malloc_crt_t(unsigned char, n1);
+
+        if (bbuffer) {
+#pragma warning(push)
+#pragma warning(disable : 6386) // PREfast doesn't understand LCMAP_SORTKEY
+            size = __crtLCMapStringW(locale_name,
+                LCMAP_SORTKEY | LINGUISTIC_IGNORECASE | LINGUISTIC_IGNOREDIACRITIC | NORM_IGNOREKANATYPE
+                    | NORM_IGNOREWIDTH,
+                string2, static_cast<int>(n2), reinterpret_cast<wchar_t*>(bbuffer.get()), static_cast<int>(n1));
+#pragma warning(pop)
+
+            if (size == 0) {
+                // buffer not big enough, get size required.
+                size = __crtLCMapStringW(locale_name,
+                    LCMAP_SORTKEY | LINGUISTIC_IGNORECASE | LINGUISTIC_IGNOREDIACRITIC | NORM_IGNOREKANATYPE
+                        | NORM_IGNOREWIDTH,
+                    string2, static_cast<int>(n2), nullptr, 0);
+
+                if (size == 0) {
+                    size = static_cast<size_t>(-1); // default error
+                }
+            } else {
+                // string successfully mapped, convert to wide char
+
+                for (size_t i = 0; i < size; ++i) {
+                    string1[i] = static_cast<wchar_t>(bbuffer.get()[i]);
+                }
+            }
+        }
+    }
+
+    return size;
+}
+} // extern "C"


Test patch:
diff --git a/tests/libcxx/expected_results.txt b/tests/libcxx/expected_results.txt
index 64b389eaf67..c0fff7a9161 100644
--- a/tests/libcxx/expected_results.txt
+++ b/tests/libcxx/expected_results.txt
@@ -830,7 +830,6 @@ std/re/re.alg/re.alg.search/basic.pass.cpp FAIL
 std/re/re.alg/re.alg.search/ecma.pass.cpp FAIL
 std/re/re.alg/re.alg.search/extended.pass.cpp FAIL
 std/re/re.traits/lookup_collatename.pass.cpp FAIL
-std/re/re.traits/transform_primary.pass.cpp FAIL
 
 # Not analyzed, likely STL bugs. Various assertions.
 std/numerics/complex.number/complex.ops/complex_divide_complex.pass.cpp FAIL
diff --git a/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp b/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp
index b24141a9fc3..687812ce480 100644
--- a/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp
+++ b/tests/std/tests/GH_005204_regex_collating_ranges/test.cpp
@@ -535,31 +535,98 @@ void test_gh_994() {
     gh_994_should_throw("[a-[.cs.]]", error_range);
     gh_994_should_throw("[[.cs.]-[.dzs.]]", error_range);
 
-#ifndef SKIP_COLLATE_TESTS
+#ifndef _M_CEE_PURE
     g_regexTester.should_throw("[[=a=]-c]", error_range);
     g_regexTester.should_throw("[c-[=z=]]", error_range);
     g_regexTester.should_throw("[[=a=]-[=z=]]", error_range);
 
     g_regexTester.should_match("a", "[[=a=]]");
-    g_regexTester.should_match("A", "[[=a=]]");
+    g_regexTester.should_not_match("A", "[[=a=]]");
     g_regexTester.should_not_match("b", "[[=a=]]");
     g_regexTester.should_not_match("B", "[[=a=]]");
-    g_regexTester.should_match("z", "[[=Z=]]");
+    g_regexTester.should_not_match("z", "[[=Z=]]");
     g_regexTester.should_match("Z", "[[=Z=]]");
     g_regexTester.should_not_match("b", "[[=Z=]]");
     g_regexTester.should_not_match("B", "[[=Z=]]");
 
+    g_regexTester.should_match("a", "[[=a=]]", icase);
+    g_regexTester.should_match("A", "[[=a=]]", icase);
+    g_regexTester.should_not_match("b", "[[=a=]]", icase);
+    g_regexTester.should_not_match("B", "[[=a=]]", icase);
+    g_regexTester.should_match("z", "[[=Z=]]", icase);
+    g_regexTester.should_match("Z", "[[=Z=]]", icase);
+    g_regexTester.should_not_match("b", "[[=Z=]]", icase);
+    g_regexTester.should_not_match("B", "[[=Z=]]", icase);
+
     g_regexTester.should_match("ab", "[[=a=]]b");
-    g_regexTester.should_match("Ab", "[[=a=]]b");
+    g_regexTester.should_not_match("Ab", "[[=a=]]b");
     g_regexTester.should_not_match("Ab", "[[=a=]]B");
     g_regexTester.should_not_match("b", "[[=a=]]b");
     g_regexTester.should_not_match("aab", "[[=a=]]b");
     g_regexTester.should_not_match("B", "[[=a=]]b");
-
-    g_regexTester.should_match("AaAaaAaab", "[[=a=]]*b");
+    g_regexTester.should_not_match("ab", "[[=A=]]b");
+    g_regexTester.should_match("Ab", "[[=A=]]b");
+    g_regexTester.should_not_match("Ab", "[[=A=]]B");
+    g_regexTester.should_not_match("b", "[[=A=]]b");
+    g_regexTester.should_not_match("AAb", "[[=A=]]b");
+    g_regexTester.should_not_match("B", "[[=A=]]b");
+
+    g_regexTester.should_match("ab", "[[=a=]]b", icase);
+    g_regexTester.should_match("Ab", "[[=a=]]b", icase);
+    g_regexTester.should_match("Ab", "[[=a=]]B", icase);
+    g_regexTester.should_not_match("b", "[[=a=]]b", icase);
+    g_regexTester.should_not_match("aab", "[[=a=]]b", icase);
+    g_regexTester.should_not_match("B", "[[=a=]]b", icase);
+    g_regexTester.should_match("ab", "[[=A=]]b", icase);
+    g_regexTester.should_match("Ab", "[[=A=]]b", icase);
+    g_regexTester.should_match("Ab", "[[=A=]]B", icase);
+    g_regexTester.should_not_match("b", "[[=A=]]b", icase);
+    g_regexTester.should_not_match("AAb", "[[=A=]]b", icase);
+    g_regexTester.should_not_match("B", "[[=A=]]b", icase);
+
+    g_regexTester.should_not_match("AaAaaAaab", "[[=a=]]*b");
     g_regexTester.should_not_match("AaAaaAaab", "[[=a=]]*c");
-    g_regexTester.should_match("AaAabcaAaad", "[[=a=]bc]*d");
-#endif // !defined(SKIP_COLLATE_TESTS)
+    g_regexTester.should_not_match("AaAabcaAaad", "[[=a=]bc]*d");
+    g_regexTester.should_match("AaAaaAaab", "[[=a=]]*b", icase);
+    g_regexTester.should_not_match("AaAaaAaab", "[[=a=]]*c", icase);
+    g_regexTester.should_match("AaAabcaAaad", "[[=a=]bc]*d", icase);
+#endif // ^^^ !defined(_M_CEE_PURE) ^^^
+}
+
+void test_gh_5435() {
+    // GH-5435: <regex>: Equivalence classes have unexpected behavior with std::wregex
+#ifndef _M_CEE_PURE
+    {
+        test_wregex_locale eq_a_regex(&g_regexTester, L"^[[=a=]]*b$", "en-US");
+        eq_a_regex.should_search_match(L"A\u00c0ab", L"A\u00c0ab"); // U+00C0 LATIN CAPITAL LETTER A WITH GRAVE
+        eq_a_regex.should_search_fail(L"Ab\u00c0ab"); // U+00C0 LATIN CAPITAL LETTER A WITH GRAVE
+        eq_a_regex.should_search_match(L"A\u00e0ab", L"A\u00e0ab"); // U+00E0 LATIN SMALL LETTER A WITH GRAVE
+        eq_a_regex.should_search_match(L"A\u00c1ab", L"A\u00c1ab"); // U+00C1 LATIN CAPITAL LETTER A WITH ACUTE
+        eq_a_regex.should_search_match(L"A\u00e1ab", L"A\u00e1ab"); // U+00E1 LATIN SMALL LETTER A WITH ACUTE
+        eq_a_regex.should_search_match(L"A\u00c2ab", L"A\u00c2ab"); // U+00C2 LATIN CAPITAL LETTER A WITH CIRCUMFLEX
+        eq_a_regex.should_search_match(L"A\u00e2ab", L"A\u00e2ab"); // U+00E2 LATIN SMALL LETTER A WITH CIRCUMFLEX
+        eq_a_regex.should_search_match(L"A\u00c3ab", L"A\u00c3ab"); // U+00C3 LATIN CAPITAL LETTER A WITH TILDE
+        eq_a_regex.should_search_match(L"A\u00e3ab", L"A\u00e3ab"); // U+00E3 LATIN SMALL LETTER A WITH TILDE
+        eq_a_regex.should_search_match(L"A\u00c4ab", L"A\u00c4ab"); // U+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS
+        eq_a_regex.should_search_match(L"A\u00e4ab", L"A\u00e4ab"); // U+00E4 LATIN SMALL LETTER A WITH DIAERESIS
+        eq_a_regex.should_search_match(L"A\u00c5ab", L"A\u00c5ab"); // U+00C5 LATIN CAPITAL LETTER A WITH RING ABOVE
+        eq_a_regex.should_search_match(L"A\u00e5ab", L"A\u00e5ab"); // U+00E5 LATIN SMALL LETTER A WITH RING ABOVE
+    }
+    {
+        test_wregex_locale eq_e_regex(&g_regexTester, L"^[[=e=]]*b$", "en-US");
+        eq_e_regex.should_search_match(L"e\u00c8Eb", L"e\u00c8Eb"); // U+00C8 LATIN CAPITAL LETTER E WITH GRAVE
+        eq_e_regex.should_search_fail(L"eb\u00c8Eb"); // U+00C8 LATIN CAPITAL LETTER E WITH GRAVE
+        eq_e_regex.should_search_match(L"e\u00e8Eb", L"e\u00e8Eb"); // U+00E8 LATIN SMALL LETTER E WITH GRAVE
+        eq_e_regex.should_search_match(L"e\u00c9Eb", L"e\u00c9Eb"); // U+00C9 LATIN CAPITAL LETTER E WITH ACUTE
+        eq_e_regex.should_search_match(L"e\u00e9Eb", L"e\u00e9Eb"); // U+00E9 LATIN SMALL LETTER E WITH ACUTE
+        eq_e_regex.should_search_match(L"e\u00caEb", L"e\u00caEb"); // U+00CA LATIN CAPITAL LETTER E WITH CIRCUMFLEX
+        eq_e_regex.should_search_match(L"e\u00eaEb", L"e\u00eaEb"); // U+00EA LATIN SMALL LETTER E WITH CIRCUMFLEX
+        eq_e_regex.should_search_match(L"e\u00cbEb", L"e\u00cbEb"); // U+00CB LATIN CAPITAL LETTER E WITH DIAERESIS
+        eq_e_regex.should_search_match(L"e\u00ebEb", L"e\u00ebEb"); // U+00EB LATIN SMALL LETTER E WITH DIAERESIS
+        eq_e_regex.should_search_fail(L"e\u00ccEb"); // U+00CC LATIN CAPITAL LETTER I WITH GRAVE
+        eq_e_regex.should_search_fail(L"e\u00ecEb"); // U+00EC LATIN SMALL LETTER I WITH GRAVE
+    }
+#endif // ^^^ !defined(_M_CEE_PURE) ^^^
 }
 
 void test_gh_5437_ECMAScript_or_collate(syntax_option_type ECMAScript_or_collate) {
@@ -592,6 +659,7 @@ void test_gh_5437() {
 int main() {
     test_collating_ranges_german();
     test_gh_994();
+    test_gh_5435();
     test_gh_5437();
 
     return g_regexTester.result();
diff --git a/tests/std/tests/VSO_0000000_regex_use/test.cpp b/tests/std/tests/VSO_0000000_regex_use/test.cpp
index d7a4dc65f38..3d6fe5380a7 100644
--- a/tests/std/tests/VSO_0000000_regex_use/test.cpp
+++ b/tests/std/tests/VSO_0000000_regex_use/test.cpp
@@ -764,9 +764,11 @@ void test_gh_4995() {
     g_regexTester.should_throw("[[:digit:]-e]", error_range);
     g_regexTester.should_throw("[e-[:digit:]]", error_range);
     g_regexTester.should_throw("[[:alpha:]-[:digit:]]", error_range);
+#ifndef _M_CEE_PURE
     g_regexTester.should_throw("[[=a=]-e]", error_range, ECMAScript | regex::collate);
     g_regexTester.should_throw("[e-[=a=]]", error_range, ECMAScript | regex::collate);
     g_regexTester.should_throw("[[=a=]-[=b=]]", error_range, ECMAScript | regex::collate);
+#endif // ^^^ !defined(_M_CEE_PURE) ^^^
 
     // Test valid cases:
     g_regexTester.should_not_match("b", R"([\d-])");
diff --git a/tests/tr1/tests/regex1/test.cpp b/tests/tr1/tests/regex1/test.cpp
index a9f4689b176..2aaf0ea2e7e 100644
--- a/tests/tr1/tests/regex1/test.cpp
+++ b/tests/tr1/tests/regex1/test.cpp
@@ -173,9 +173,11 @@ static void test_traits() { // test template regex_traits
     CHECKSTRING(v0.transform(carr, carr + xlen(carr)), v0.transform(carr, carr + xlen(carr)));
     CHECK(v0.transform(carr, carr + xlen(carr)) != v0.transform(carr0, carr0 + xlen(carr0)));
     CHECK(v0.transform(carr, carr + xlen(carr)) < v0.transform(carr1, carr1 + xlen(carr1)));
+#ifndef _M_CEE_PURE
     CHECK(v0.transform_primary(carr, carr + xlen(carr)) == v0.transform_primary(carr, carr + xlen(carr)));
-    CHECK(v0.transform_primary(carr, carr + xlen(carr)) == v0.transform_primary(carr0, carr0 + xlen(carr0)));
+    CHECK(v0.transform_primary(carr, carr + xlen(carr)) != v0.transform_primary(carr0, carr0 + xlen(carr0)));
     CHECK(v0.transform_primary(carr0, carr0 + xlen(carr0)) < v0.transform_primary(carr1, carr1 + xlen(carr1)));
+#endif // ^^^ !defined(_M_CEE_PURE) ^^^
 
     for (size_t i = 0; i < sizeof(class_names) / sizeof(*class_names); ++i) {
         CHECK(v0.lookup_classname(class_names[i], class_names[i] + xlen(class_names[i])) != 0);
diff --git a/tests/tr1/tests/regex2/test.cpp b/tests/tr1/tests/regex2/test.cpp
index 44cb5632126..d52371192de 100644
--- a/tests/tr1/tests/regex2/test.cpp
+++ b/tests/tr1/tests/regex2/test.cpp
@@ -588,8 +588,10 @@ static const regex_test tests[] = {
     {__LINE__, T("[[:xdigit:]]"), T("g"), "0", ALL},
     {__LINE__, T("[[:xdigit:]]"), T("1"), "1 0 1", ALL},
     {__LINE__, T("[[:xdigit:]]"), T(" "), "0", ALL},
-    {__LINE__, T("[[=x=]]"), T("X"), "1 0 1", ALL},
+#ifndef _M_CEE_PURE
+    {__LINE__, T("[[=x=]]"), T("X"), "0", ALL},
     {__LINE__, T("[[=x=]]"), T("x"), "1 0 1", ALL},
+#endif // ^^^ !defined(_M_CEE_PURE) ^^^
 
     // character class ranges
     {__LINE__, T("[-]"), T("-"), "1 0 1", ALL},