Add ToUpperOrdinal and ToLowerOrdinal casing APIs#130140
Merged
Merged
Conversation
Implement ordinal (simple, one-to-one) casing on Char, Rune, String, and MemoryExtensions, matching the mappings used by OrdinalIgnoreCase comparisons. String.ToUpperOrdinal/ToLowerOrdinal return the same instance when an all-ASCII string is already in the requested case.
Contributor
|
Tagging subscribers to this area: @dotnet/area-system-globalization |
Contributor
There was a problem hiding this comment.
Pull request overview
Adds new ordinal casing APIs across char, Rune, string, and span-based MemoryExtensions, implemented to match the StringComparison.OrdinalIgnoreCase folding behavior (one-to-one mapping, length-preserving), including a new ordinal lower-casing table path alongside the existing upper-casing tables.
Changes:
- Introduces
ToUpperOrdinal/ToLowerOrdinalAPIs onchar,Rune,string, andReadOnlySpan<char> -> Span<char>transforms. - Implements ICU-backed ordinal lower-casing table initialization (native + managed), mirroring the existing upper-casing infrastructure and special-case handling.
- Adds comprehensive
System.Globalization.Testscoverage validating BMP-wide behavior and overload consistency.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/native/libs/System.Globalization.Native/System.Globalization.Native.def | Exports the new native entrypoint for ordinal lower-casing page initialization. |
| src/native/libs/System.Globalization.Native/pal_common.c | Implements GlobalizationNative_InitOrdinalLowerCasingPage using ICU simple lower casing + special cases. |
| src/native/libs/System.Globalization.Native/pal_casing.h | Declares the new native PAL export for ordinal lower-casing page initialization. |
| src/native/libs/System.Globalization.Native/entrypoints.c | Registers the new entrypoint for DllImport resolution. |
| src/libraries/Common/src/Interop/Interop.Casing.cs | Adds managed LibraryImport for GlobalizationNative_InitOrdinalLowerCasingPage. |
| src/libraries/System.Private.CoreLib/src/System/Globalization/OrdinalCasing.Icu.cs | Adds a mirrored lower-casing table + lazy per-page initialization and span-based lower casing. |
| src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.cs | Adds TextInfo.ToLowerOrdinal(char) routing based on globalization mode (Invariant / NLS / ICU). |
| src/libraries/System.Private.CoreLib/src/System/Globalization/Ordinal.cs | Adds span-based Ordinal.ToLowerOrdinal entrypoint consistent with existing ToUpperOrdinal. |
| src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.Globalization.cs | Adds public span extension methods ToUpperOrdinal / ToLowerOrdinal. |
| src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs | Adds string.ToUpperOrdinal() / string.ToLowerOrdinal() with ASCII fast-path returning same instance when unchanged. |
| src/libraries/System.Private.CoreLib/src/System/Char.cs | Adds public char.ToUpperOrdinal(char) / char.ToLowerOrdinal(char). |
| src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs | Adds public Rune.ToUpperOrdinal(Rune) / Rune.ToLowerOrdinal(Rune) implementations. |
| src/libraries/System.Runtime/ref/System.Runtime.cs | Adds the new public APIs to the System.Runtime ref surface (char, string, Rune). |
| src/libraries/System.Memory/ref/System.Memory.cs | Adds the new public APIs to the System.Memory ref surface (MemoryExtensions). |
| src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/OrdinalCasingTests.cs | New tests validating ordinal casing invariants and special-case behavior across the BMP and overloads. |
| src/libraries/System.Runtime/tests/System.Globalization.Tests/System.Globalization.Tests.csproj | Includes the new test file in the test project. |
| src/coreclr/vm/wasm/wasi/callhelpers-pinvoke.cpp | Adds the new native export for WASM/WASI callhelper pinvoke tables. |
| src/coreclr/vm/wasm/browser/callhelpers-pinvoke.cpp | Adds the new native export for WASM/browser callhelper pinvoke tables. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Member
Author
|
CC @lewing to advise if we need to do anything more for WASM with this change? CC @jkotas & @GrabYourPitchforks for awareness. |
added 2 commits
July 2, 2026 14:40
… modes Invariant and NLS simple lowering could move characters such as the Kelvin, Ohm and Angstrom signs out of their ordinal upper-casing class, so ToLowerOrdinal stopped being consistent with OrdinalIgnoreCase in those modes. Keep the original character when its simple lower mapping would change its ordinal upper-casing form, and extend the invariant-mode test to cover ToLowerOrdinal across the BMP.
This was referenced Jul 3, 2026
Open
…ecutor Guard the MICRO SIGN and GREEK SMALL FINAL SIGMA fold cases so they only run under ICU and invariant casing, since NLS does not treat them as OrdinalIgnoreCase-equal. Gate OrdinalIgnoreCase_Equivalence_HoldsInInvariantMode on RemoteExecutor.IsSupported so it is skipped on wasm and android instead of throwing PlatformNotSupportedException.
added 2 commits
July 6, 2026 13:46
Upper-case the source and lowered spans once each instead of calling TextInfo.ToUpperOrdinal per non-ASCII character, which is expensive under NLS.
Only a fixed set of five BMP scalars (Greek capital theta symbol, Latin capital sharp S, Ohm, Kelvin and Angstrom signs) fall out of their ordinal upper-casing class under simple invariant or NLS lowering. Scan the source once with SearchValues and skip the pass entirely when none are present, instead of upper-casing the whole span on every lowering.
MihaZupan
reviewed
Jul 7, 2026
Fast-path the leading ASCII run of the ICU ToUpperOrdinal/ToLowerOrdinal span helpers with Ascii.ToUpper/ToLower, and replace the hand-rolled unsafe scan in the string ChangeCaseOrdinal fast path with a vectorized IndexOfAnyExcept over a SearchValues of the ASCII characters already in the requested case.
jkotas
reviewed
Jul 7, 2026
This was referenced Jul 8, 2026
MihaZupan
approved these changes
Jul 8, 2026
- Move ordinal-casing SearchValues into SearchValuesStorage using hardcoded ASCII literals so they are created lazily only when ordinal casing is used - Use (uint) loop bounds in OrdinalCasing.Icu.cs to elide bounds checks - Use collection expressions for single-char destination spans in tests
MihaZupan
approved these changes
Jul 8, 2026
MihaZupan
approved these changes
Jul 8, 2026
Member
Author
|
/ba-g failures are WASm timeout or unrelated failures. |
|
Yay 🎉 |
lewing
added a commit
that referenced
this pull request
Jul 11, 2026
…tive (#130543) ## Summary Fixes the browser-wasm **CoreCLR** library test leg (`WasmTestOnChrome-CLR` → `System.Globalization.Extensions.Tests`), failing on `main` since 2026-07-09 with: ``` System.DllNotFoundException : Unable to load shared library 'libSystem.Globalization.Native' ... dynamic linking not enabled at Interop.Globalization.ToUnicode(...) at System.Globalization.IdnMapping.IcuTryGetUnicodeCore(...) ``` Tracked by #130479 (`blocking-clean-ci`). ## Root cause #130140 added `GlobalizationNative_InitOrdinalLowerCasingPage` to the generated `callhelpers-pinvoke.cpp` P/Invoke tables (browser + wasi) — correctly inserting the sorted entry and its `extern "C"` declaration — but did **not** update the hard-coded `EntryCount` in `s_PInvokeTables` (left at `33`, now `34` entries). `minipal_resolve_dllimport` (`src/native/minipal/entrypoints.h`) linearly scans `entries[0 .. count-1]`. With `count = 33` over the 34-entry table, the **last** entry — `GlobalizationNative_ToUnicode` — became unreachable, so its P/Invoke resolved to `NULL` and fell back to `dlopen` (unsupported for CoreCLR-wasm) → `DllNotFoundException`. This is why **only** `IdnMapping.GetUnicode`/`ToUnicode` tests failed while `GetAscii` (`ToAscii`, index 32, still in range) kept passing. The generator emits this count from the table length, so `34` is exactly what a regeneration produces; #130140 hand-edited the generated file without re-running it. ## Fix Bump the `libSystem.Globalization.Native` `EntryCount` from `33` → `34` in both: - `src/coreclr/vm/wasm/browser/callhelpers-pinvoke.cpp` - `src/coreclr/vm/wasm/wasi/callhelpers-pinvoke.cpp` All other module counts already match their tables (verified in both files). ## Regression window (bisected on `main`, definition 129) | Build | SHA | `LibraryTestsCoreCLR` | |---|---|---| | 1500867 (Jul 9 08:00) | `0f907bfa` (no #130140) | ✅ pass | | 1502091 (Jul 9 20:00) | `6c584914` (has #130140) | ❌ fail — same signature | Emscripten 5.0.6/LLVM23 (Jun 25) and -O3 (#130111, Jul 2) were ruled out — the leg stayed green with both for two weeks. ## Verification - Native reproduction against the real `minipal/entrypoints.h`: `ToUnicode` resolves `NULL` at count 33 and `FOUND` at count 34; `ToAscii` resolves at 33 (matches the observed pass/fail split). - The browser-wasm CoreCLR `WasmTestOnChrome-CLR` CI leg exercises this end-to-end. cc @tarekgh @pavelsavara @lewing > [!NOTE] > Root-cause investigation and this fix were produced with GitHub Copilot. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the ordinal casing APIs approved in #90999.
APIs
char.ToUpperOrdinal(char)/char.ToLowerOrdinal(char)Rune.ToUpperOrdinal(Rune)/Rune.ToLowerOrdinal(Rune)string.ToUpperOrdinal()/string.ToLowerOrdinal()MemoryExtensions.ToUpperOrdinal(ReadOnlySpan<char>, Span<char>)/ToLowerOrdinal(ReadOnlySpan<char>, Span<char>)Notes
OrdinalIgnoreCasecomparisons. Two strings areOrdinalIgnoreCase-equal iff theirToUpperOrdinalresults are ordinally equal.OrdinalIgnoreCase.string.ToUpperOrdinal()/string.ToLowerOrdinal()return the same instance when an all-ASCII string is already in the requested case, avoiding an allocation.Tests under System.Globalization.Tests validate the mappings against
OrdinalIgnoreCaseacross the entire BMP.