Repository: ParkMyCar/compact_str
Pull request #284 (2023-08-10 05:30:26)

feat: Impl `CompactString::from_str_to_{lower, upper}case`
Enable converting a `str` to lower/upper case and get a `CompactString` directly for inline storage.

NobodyXu: MIPS CI failed due to pre-compiled libstd not available PowerPC Big Endian 32-bit failed due to `SIGILL: illegal instruction`, which I suppose is unrelated to this PR.

Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..8bb64082
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,14 @@
+# Dependabot dependency version checks / updates
+
+version: 2
+updates:
+  - package-ecosystem: "github-actions"
+    # Workflow files stored in the
+    # default location of `.github/workflows`
+    directory: "/"
+    schedule:
+      interval: "daily"
+  - package-ecosystem: "cargo"
+    directory: "/"
+    schedule:
+      interval: "daily"
diff --git a/.github/workflows/cross_platform.yml b/.github/workflows/cross_platform.yml
index 09110f44..e26c2b6b 100644
--- a/.github/workflows/cross_platform.yml
+++ b/.github/workflows/cross_platform.yml
@@ -86,172 +86,174 @@ jobs:
           command: miri
           args: test --all-features --manifest-path=compact_str/Cargo.toml
 
-  linux_32bit:
-    name: Linux 32-bit
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v2
-        name: Checkout Repo
-      - uses: actions-rs/toolchain@v1
-        name: Install Rust
-        with:
-          toolchain: nightly
-          target: i686-unknown-linux-gnu
-          override: true
-          components: miri
-      - uses: actions/cache@v3
-        with:
-          path: |
-            ~/.cargo/bin/
-            ~/.cargo/registry/index/
-            ~/.cargo/registry/cache/
-            ~/.cargo/git/db/
-            target/
-          key: ${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-x-plat-linux-32bit-v2
-      - uses: actions-rs/cargo@v1
-        name: cargo test
-        with:
-          use-cross: true
-          command: test
-          args: --release --all-features --manifest-path=compact_str/Cargo.toml --target i686-unknown-linux-gnu
-      - uses: actions-rs/cargo@v1
-        name: cargo test miri
-        with:
-          command: miri
-          args: test --all-features --manifest-path=compact_str/Cargo.toml --target i686-unknown-linux-gnu
+  # TODO(parkmycar): Re-enable before release of next version.
+  #
+  # linux_32bit:
+  #   name: Linux 32-bit
+  #   runs-on: ubuntu-latest
+  #   steps:
+  #     - uses: actions/checkout@v2
+  #       name: Checkout Repo
+  #     - uses: actions-rs/toolchain@v1
+  #       name: Install Rust
+  #       with:
+  #         toolchain: nightly
+  #         target: i686-unknown-linux-gnu
+  #         override: true
+  #         components: miri
+  #     - uses: actions/cache@v3
+  #       with:
+  #         path: |
+  #           ~/.cargo/bin/
+  #           ~/.cargo/registry/index/
+  #           ~/.cargo/registry/cache/
+  #           ~/.cargo/git/db/
+  #           target/
+  #         key: ${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-x-plat-linux-32bit-v2
+  #     - uses: actions-rs/cargo@v1
+  #       name: cargo test
+  #       with:
+  #         use-cross: true
+  #         command: test
+  #         args: --release --all-features --manifest-path=compact_str/Cargo.toml --target i686-unknown-linux-gnu
+  #     - uses: actions-rs/cargo@v1
+  #       name: cargo test miri
+  #       with:
+  #         command: miri
+  #         args: test --all-features --manifest-path=compact_str/Cargo.toml --target i686-unknown-linux-gnu
 
-  linux_mips_32bit:
-    name: Linux MIPS Big Endian 32-bit
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v2
-        name: Checkout Repo
-      - uses: actions-rs/toolchain@v1
-        name: Install Rust
-        with:
-          toolchain: nightly
-          target: mips-unknown-linux-gnu
-          override: true
-          components: miri
-      - uses: actions/cache@v3
-        with:
-          path: |
-            ~/.cargo/bin/
-            ~/.cargo/registry/index/
-            ~/.cargo/registry/cache/
-            ~/.cargo/git/db/
-            target/
-          key: ${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-x-plat-linux-mips-32bit-v2
-      - uses: actions-rs/cargo@v1
-        name: cargo test
-        with:
-          use-cross: true
-          command: test
-          args: --release --all-features --manifest-path=compact_str/Cargo.toml --target mips-unknown-linux-gnu
-      - uses: actions-rs/cargo@v1
-        name: cargo test miri
-        with:
-          command: miri
-          args: test --all-features --manifest-path=compact_str/Cargo.toml --target mips-unknown-linux-gnu
+  # linux_mips_32bit:
+  #   name: Linux MIPS Big Endian 32-bit
+  #   runs-on: ubuntu-latest
+  #   steps:
+  #     - uses: actions/checkout@v2
+  #       name: Checkout Repo
+  #     - uses: actions-rs/toolchain@v1
+  #       name: Install Rust
+  #       with:
+  #         toolchain: nightly
+  #         target: mips-unknown-linux-gnu
+  #         override: true
+  #         components: miri
+  #     - uses: actions/cache@v3
+  #       with:
+  #         path: |
+  #           ~/.cargo/bin/
+  #           ~/.cargo/registry/index/
+  #           ~/.cargo/registry/cache/
+  #           ~/.cargo/git/db/
+  #           target/
+  #         key: ${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-x-plat-linux-mips-32bit-v2
+  #     - uses: actions-rs/cargo@v1
+  #       name: cargo test
+  #       with:
+  #         use-cross: true
+  #         command: test
+  #         args: --release --all-features --manifest-path=compact_str/Cargo.toml --target mips-unknown-linux-gnu
+  #     - uses: actions-rs/cargo@v1
+  #       name: cargo test miri
+  #       with:
+  #         command: miri
+  #         args: test --all-features --manifest-path=compact_str/Cargo.toml --target mips-unknown-linux-gnu
 
-  linux_mips_le_32bit:
-    name: Linux MIPS Little Endian 32-bit
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v2
-        name: Checkout Repo
-      - uses: actions-rs/toolchain@v1
-        name: Install Rust
-        with:
-          toolchain: nightly
-          target: mipsel-unknown-linux-gnu
-          override: true
-          components: miri
-      - uses: actions/cache@v3
-        with:
-          path: |
-            ~/.cargo/bin/
-            ~/.cargo/registry/index/
-            ~/.cargo/registry/cache/
-            ~/.cargo/git/db/
-            target/
-          key: ${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-x-plat-linux-mips-le-32bit-v2
-      - uses: actions-rs/cargo@v1
-        name: cargo test
-        with:
-          use-cross: true
-          command: test
-          args: --release --all-features --manifest-path=compact_str/Cargo.toml --target mipsel-unknown-linux-gnu
-      - uses: actions-rs/cargo@v1
-        name: cargo test miri
-        with:
-          command: miri
-          args: test --all-features --manifest-path=compact_str/Cargo.toml --target mipsel-unknown-linux-gnu
+  # linux_mips_le_32bit:
+  #   name: Linux MIPS Little Endian 32-bit
+  #   runs-on: ubuntu-latest
+  #   steps:
+  #     - uses: actions/checkout@v2
+  #       name: Checkout Repo
+  #     - uses: actions-rs/toolchain@v1
+  #       name: Install Rust
+  #       with:
+  #         toolchain: nightly
+  #         target: mipsel-unknown-linux-gnu
+  #         override: true
+  #         components: miri
+  #     - uses: actions/cache@v3
+  #       with:
+  #         path: |
+  #           ~/.cargo/bin/
+  #           ~/.cargo/registry/index/
+  #           ~/.cargo/registry/cache/
+  #           ~/.cargo/git/db/
+  #           target/
+  #         key: ${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-x-plat-linux-mips-le-32bit-v2
+  #     - uses: actions-rs/cargo@v1
+  #       name: cargo test
+  #       with:
+  #         use-cross: true
+  #         command: test
+  #         args: --release --all-features --manifest-path=compact_str/Cargo.toml --target mipsel-unknown-linux-gnu
+  #     - uses: actions-rs/cargo@v1
+  #       name: cargo test miri
+  #       with:
+  #         command: miri
+  #         args: test --all-features --manifest-path=compact_str/Cargo.toml --target mipsel-unknown-linux-gnu
 
-  linux_powerpc_bit:
-    name: Linux PowerPC Big Endian 32-bit
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v2
-        name: Checkout Repo
-      - uses: actions-rs/toolchain@v1
-        name: Install Rust
-        with:
-          toolchain: nightly
-          target: powerpc-unknown-linux-gnu
-          override: true
-          components: miri
-      - uses: actions/cache@v3
-        with:
-          path: |
-            ~/.cargo/bin/
-            ~/.cargo/registry/index/
-            ~/.cargo/registry/cache/
-            ~/.cargo/git/db/
-            target/
-          key: ${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-x-plat-linux-powerpc-32bit-v2
-      - uses: actions-rs/cargo@v1
-        name: cargo test
-        with:
-          use-cross: true
-          command: test
-          args: --release --all-features --manifest-path=compact_str/Cargo.toml --target powerpc-unknown-linux-gnu
-      - uses: actions-rs/cargo@v1
-        name: cargo test miri
-        with:
-          command: miri
-          args: test --all-features --manifest-path=compact_str/Cargo.toml --target powerpc-unknown-linux-gnu
+  # linux_powerpc_bit:
+  #   name: Linux PowerPC Big Endian 32-bit
+  #   runs-on: ubuntu-latest
+  #   steps:
+  #     - uses: actions/checkout@v2
+  #       name: Checkout Repo
+  #     - uses: actions-rs/toolchain@v1
+  #       name: Install Rust
+  #       with:
+  #         toolchain: nightly
+  #         target: powerpc-unknown-linux-gnu
+  #         override: true
+  #         components: miri
+  #     - uses: actions/cache@v3
+  #       with:
+  #         path: |
+  #           ~/.cargo/bin/
+  #           ~/.cargo/registry/index/
+  #           ~/.cargo/registry/cache/
+  #           ~/.cargo/git/db/
+  #           target/
+  #         key: ${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-x-plat-linux-powerpc-32bit-v2
+  #     - uses: actions-rs/cargo@v1
+  #       name: cargo test
+  #       with:
+  #         use-cross: true
+  #         command: test
+  #         args: --release --all-features --manifest-path=compact_str/Cargo.toml --target powerpc-unknown-linux-gnu
+  #     - uses: actions-rs/cargo@v1
+  #       name: cargo test miri
+  #       with:
+  #         command: miri
+  #         args: test --all-features --manifest-path=compact_str/Cargo.toml --target powerpc-unknown-linux-gnu
 
-  linux_powerpc_le_64bit:
-    name: Linux PowerPC Little Endian 64-bit
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v2
-        name: Checkout Repo
-      - uses: actions-rs/toolchain@v1
-        name: Install Rust
-        with:
-          toolchain: nightly
-          target: powerpc64le-unknown-linux-gnu
-          override: true
-          components: miri
-      - uses: actions/cache@v3
-        with:
-          path: |
-            ~/.cargo/bin/
-            ~/.cargo/registry/index/
-            ~/.cargo/registry/cache/
-            ~/.cargo/git/db/
-            target/
-          key: ${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-x-plat-linux-powerpc-le-64bit-v2
-      - uses: actions-rs/cargo@v1
-        name: cargo test
-        with:
-          use-cross: true
-          command: test
-          args: --release --all-features --manifest-path=compact_str/Cargo.toml --target powerpc64le-unknown-linux-gnu
-      - uses: actions-rs/cargo@v1
-        name: cargo test miri
-        with:
-          command: miri
-          args: test --all-features --manifest-path=compact_str/Cargo.toml --target powerpc64le-unknown-linux-gnu
+  # linux_powerpc_le_64bit:
+  #   name: Linux PowerPC Little Endian 64-bit
+  #   runs-on: ubuntu-latest
+  #   steps:
+  #     - uses: actions/checkout@v2
+  #       name: Checkout Repo
+  #     - uses: actions-rs/toolchain@v1
+  #       name: Install Rust
+  #       with:
+  #         toolchain: nightly
+  #         target: powerpc64le-unknown-linux-gnu
+  #         override: true
+  #         components: miri
+  #     - uses: actions/cache@v3
+  #       with:
+  #         path: |
+  #           ~/.cargo/bin/
+  #           ~/.cargo/registry/index/
+  #           ~/.cargo/registry/cache/
+  #           ~/.cargo/git/db/
+  #           target/
+  #         key: ${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}-x-plat-linux-powerpc-le-64bit-v2
+  #     - uses: actions-rs/cargo@v1
+  #       name: cargo test
+  #       with:
+  #         use-cross: true
+  #         command: test
+  #         args: --release --all-features --manifest-path=compact_str/Cargo.toml --target powerpc64le-unknown-linux-gnu
+  #     - uses: actions-rs/cargo@v1
+  #       name: cargo test miri
+  #       with:
+  #         command: miri
+  #         args: test --all-features --manifest-path=compact_str/Cargo.toml --target powerpc64le-unknown-linux-gnu
diff --git a/README.md b/README.md
index da07d16a..d531783a 100644
--- a/README.md
+++ b/README.md
@@ -131,11 +131,10 @@ For **inline** strings we only have a 24 byte buffer on the stack. This might ma
 To do this, we utilize the fact that the last byte of our string could only ever have a value in the range `[0, 192)`. We know this because all strings in Rust are valid [UTF-8](https://en.wikipedia.org/wiki/UTF-8), and the only valid byte pattern for the last byte of a UTF-8 character (and thus the possible last byte of a string) is `0b0XXXXXXX` aka `[0, 128)` or `0b10XXXXXX` aka `[128, 192)`. This leaves all values in `[192, 255]` as unused in our last byte. Therefore, we can use values in the range of `[192, 215]` to represent a length in the range of `[0, 23]`, and if our last byte has a value `< 192`, we know that's a UTF-8 character, and can interpret the length of our string as `24`.
 
 Specifically, the last byte on the stack for a `CompactString` has the following uses:
-* `[0, 192)` - Is the last byte of a UTF-8 char, the `CompactString` is stored on the stack and implicitly has a length of `24`
+* `[0, 191]` - Is the last byte of a UTF-8 char, the `CompactString` is stored on the stack and implicitly has a length of `24`
 * `[192, 215]` - Denotes a length in the range of `[0, 23]`, this `CompactString` is stored on the stack.
-* `[215, 254)` - Unused
-* `254` - Denotes this `CompactString` is stored on the heap
-* `255` - Denotes the `None` variant for an `Option<CompactString>`
+* `216` - Denotes this `CompactString` is stored on the heap
+* `[217, 255]` - Unused, denotes e.g. the `None` variant for `Option<CompactString>`
 
 ### Testing
 Strings and unicode can be quite messy, even further, we're working with things at the bit level. `compact_str` has an _extensive_ test suite comprised of unit testing, property testing, and fuzz testing, to ensure our invariants are upheld. We test across all major OSes (Windows, macOS, and Linux), architectures (64-bit and 32-bit), and endian-ness (big endian and little endian).
diff --git a/compact_str/src/lib.rs b/compact_str/src/lib.rs
index c9707d35..170d74a6 100644
--- a/compact_str/src/lib.rs
+++ b/compact_str/src/lib.rs
@@ -1647,14 +1647,58 @@ impl CompactString {
     ///
     /// assert_eq!(new_year, new_year.to_lowercase());
     /// ```
-    #[must_use = "this returns the lowercase string as a new String, \
+    #[must_use = "this returns the lowercase string as a new CompactString, \
                   without modifying the original"]
     pub fn to_lowercase(&self) -> Self {
-        let mut s = convert_while_ascii(self.as_bytes(), u8::to_ascii_lowercase);
+        Self::from_str_to_lowercase(self.as_str())
+    }
+
+    /// Returns the lowercase equivalent of this string slice, as a new [`CompactString`].
+    ///
+    /// 'Lowercase' is defined according to the terms of the Unicode Derived Core Property
+    /// `Lowercase`.
+    ///
+    /// Since some characters can expand into multiple characters when changing
+    /// the case, this function returns a [`CompactString`] instead of modifying the
+    /// parameter in-place.
+    ///
+    /// # Examples
+    ///
+    /// Basic usage:
+    ///
+    /// ```
+    /// use compact_str::CompactString;
+    ///
+    /// assert_eq!("hello", CompactString::from_str_to_lowercase("HELLO"));
+    /// ```
+    ///
+    /// A tricky example, with sigma:
+    ///
+    /// ```
+    /// use compact_str::CompactString;
+    ///
+    /// assert_eq!("σ", CompactString::from_str_to_lowercase("Σ"));
+    ///
+    /// // but at the end of a word, it's ς, not σ:
+    /// assert_eq!("ὀδυσσεύς", CompactString::from_str_to_lowercase("ὈΔΥΣΣΕΎΣ"));
+    /// ```
+    ///
+    /// Languages without case are not changed:
+    ///
+    /// ```
+    /// use compact_str::CompactString;
+    ///
+    /// let new_year = "农历新年";
+    /// assert_eq!(new_year, CompactString::from_str_to_lowercase(new_year));
+    /// ```
+    #[must_use = "this returns the lowercase string as a new CompactString, \
+                  without modifying the original"]
+    pub fn from_str_to_lowercase(input: &str) -> Self {
+        let mut s = convert_while_ascii(input.as_bytes(), u8::to_ascii_lowercase);
 
         // Safety: we know this is a valid char boundary since
         // out.len() is only progressed if ascii bytes are found
-        let rest = unsafe { self.get_unchecked(s.len()..) };
+        let rest = unsafe { input.get_unchecked(s.len()..) };
 
         for (i, c) in rest.char_indices() {
             if c == 'Σ' {
@@ -1725,14 +1769,54 @@ impl CompactString {
     ///
     /// assert_eq!("TSCHÜSS", s.to_uppercase());
     /// ```
-    #[must_use = "this returns the uppercase string as a new String, \
+    #[must_use = "this returns the uppercase string as a new CompactString, \
                   without modifying the original"]
     pub fn to_uppercase(&self) -> Self {
-        let mut out = convert_while_ascii(self.as_bytes(), u8::to_ascii_uppercase);
+        Self::from_str_to_uppercase(self.as_str())
+    }
+
+    /// Returns the uppercase equivalent of this string slice, as a new [`CompactString`].
+    ///
+    /// 'Uppercase' is defined according to the terms of the Unicode Derived Core Property
+    /// `Uppercase`.
+    ///
+    /// Since some characters can expand into multiple characters when changing
+    /// the case, this function returns a [`CompactString`] instead of modifying the
+    /// parameter in-place.
+    ///
+    /// # Examples
+    ///
+    /// Basic usage:
+    ///
+    /// ```
+    /// use compact_str::CompactString;
+    ///
+    /// assert_eq!("HELLO", CompactString::from_str_to_uppercase("hello"));
+    /// ```
+    ///
+    /// Scripts without case are not changed:
+    ///
+    /// ```
+    /// use compact_str::CompactString;
+    ///
+    /// let new_year = "农历新年";
+    /// assert_eq!(new_year, CompactString::from_str_to_uppercase(new_year));
+    /// ```
+    ///
+    /// One character can become multiple:
+    /// ```
+    /// use compact_str::CompactString;
+    ///
+    /// assert_eq!("TSCHÜSS", CompactString::from_str_to_uppercase("tschüß"));
+    /// ```
+    #[must_use = "this returns the uppercase string as a new CompactString, \
+                  without modifying the original"]
+    pub fn from_str_to_uppercase(input: &str) -> Self {
+        let mut out = convert_while_ascii(input.as_bytes(), u8::to_ascii_uppercase);
 
         // Safety: we know this is a valid char boundary since
         // out.len() is only progressed if ascii bytes are found
-        let rest = unsafe { self.get_unchecked(out.len()..) };
+        let rest = unsafe { input.get_unchecked(out.len()..) };
 
         for c in rest.chars() {
             out.extend(c.to_uppercase());
diff --git a/compact_str/src/repr/nonmax.rs b/compact_str/src/repr/last_utf8_char.rs
similarity index 65%
rename from compact_str/src/repr/nonmax.rs
rename to compact_str/src/repr/last_utf8_char.rs
index 52353a34..903227a7 100644
--- a/compact_str/src/repr/nonmax.rs
+++ b/compact_str/src/repr/last_utf8_char.rs
@@ -1,14 +1,14 @@
-/// [`NonMaxU8`] is an unsigned 8-bit integer data type that has a valid range of `[0, 254]`.
-/// Excluding `255` allows the Rust compiler to use `255` as a niche.
+/// [`LastUtf8Char`] is an unsigned 8-bit integer data type that has a valid range of `[0, 216]`.
+/// Excluding `[217, 255]` allows the Rust compiler to use these values as niches.
 ///
-/// Specifically the compiler can use `255` to encode the `None` variant of `Option<NonMaxU8>`
-/// allowing `std::mem::size_of::<NonMaxU8> == std::mem::size_of::<Option<NonMaxU8>>()`
-#[allow(clippy::upper_case_acronyms)]
+/// Specifically the compiler can use a value in this range to encode the `None` variant of
+/// `Option<NonMaxU8>` allowing
+/// `std::mem::size_of::<NonMaxU8> == std::mem::size_of::<Option<NonMaxU8>>()`
 #[allow(dead_code)]
-#[allow(non_camel_case_types)]
 #[derive(Copy, Clone, Debug)]
 #[repr(u8)]
-pub enum NonMaxU8 {
+pub enum LastUtf8Char {
+    // single character, ASCII:
     V0 = 0,
     V1 = 1,
     V2 = 2,
@@ -137,6 +137,8 @@ pub enum NonMaxU8 {
     V125 = 125,
     V126 = 126,
     V127 = 127,
+
+    // trailing byte in a multi-byte UTF-8 sequence
     V128 = 128,
     V129 = 129,
     V130 = 130,
@@ -201,69 +203,40 @@ pub enum NonMaxU8 {
     V189 = 189,
     V190 = 190,
     V191 = 191,
-    V192 = 192,
-    V193 = 193,
-    V194 = 194,
-    V195 = 195,
-    V196 = 196,
-    V197 = 197,
-    V198 = 198,
-    V199 = 199,
-    V200 = 200,
-    V201 = 201,
-    V202 = 202,
-    V203 = 203,
-    V204 = 204,
-    V205 = 205,
-    V206 = 206,
-    V207 = 207,
-    V208 = 208,
-    V209 = 209,
-    V210 = 210,
-    V211 = 211,
-    V212 = 212,
-    V213 = 213,
-    V214 = 214,
-    V215 = 215,
-    V216 = 216,
-    V217 = 217,
-    V218 = 218,
-    V219 = 219,
-    V220 = 220,
-    V221 = 221,
-    V222 = 222,
-    V223 = 223,
-    V224 = 224,
-    V225 = 225,
-    V226 = 226,
-    V227 = 227,
-    V228 = 228,
-    V229 = 229,
-    V230 = 230,
-    V231 = 231,
-    V232 = 232,
-    V233 = 233,
-    V234 = 234,
-    V235 = 235,
-    V236 = 236,
-    V237 = 237,
-    V238 = 238,
-    V239 = 239,
-    V240 = 240,
-    V241 = 241,
-    V242 = 242,
-    V243 = 243,
-    V244 = 244,
-    V245 = 245,
-    V246 = 246,
-    V247 = 247,
-    V248 = 248,
-    V249 = 249,
-    V250 = 250,
-    V251 = 251,
-    V252 = 252,
-    V253 = 253,
-    V254 = 254,
+
+    // Cannot be last character of a UTF-8 sequence (leading byte of a sequence)
+    // V192 .. V244,
+    // Can never occur in UTF-8 (start for a codepoint > U+10FFFF)
+    // V245 .. 255,
+
+    // length marker:
+    L0 = 192,
+    L1 = 193,
+    L2 = 194,
+    L3 = 195,
+    L4 = 196,
+    L5 = 197,
+    L6 = 198,
+    L7 = 199,
+    L8 = 200,
+    L9 = 201,
+    L10 = 202,
+    L11 = 203,
+    L12 = 204,
+    L13 = 205,
+    L14 = 206,
+    L15 = 207,
+    L16 = 208,
+    L17 = 209,
+    L18 = 210,
+    L19 = 211,
+    L20 = 212,
+    L21 = 213,
+    L22 = 214,
+    L23 = 215,
+
+    Heap = 216,
 }
 
-static_assertions::assert_eq_size!(NonMaxU8, Option<NonMaxU8>, u8);
+static_assertions::assert_eq_size!(LastUtf8Char, Option<LastUtf8Char>, u8);
+static_assertions::const_assert!(std::mem::size_of::<String>() <= 24);
diff --git a/compact_str/src/repr/mod.rs b/compact_str/src/repr/mod.rs
index ca5266fa..d1928fd4 100644
--- a/compact_str/src/repr/mod.rs
+++ b/compact_str/src/repr/mod.rs
@@ -14,23 +14,23 @@ mod capacity;
 mod heap;
 mod inline;
 mod iter;
-mod nonmax;
+mod last_utf8_char;
 mod num;
 mod traits;
 
 use capacity::Capacity;
 use heap::HeapBuffer;
 use inline::InlineBuffer;
-use nonmax::NonMaxU8;
+use last_utf8_char::LastUtf8Char;
 pub use traits::IntoRepr;
 
 /// The max size of a string we can fit inline
 pub const MAX_SIZE: usize = std::mem::size_of::<String>();
 /// Used as a discriminant to identify different variants
-pub const HEAP_MASK: u8 = 0b11111110;
+pub const HEAP_MASK: u8 = LastUtf8Char::Heap as u8;
 /// When our string is stored inline, we represent the length of the string in the last byte, offset
 /// by `LENGTH_MASK`
-pub const LENGTH_MASK: u8 = 0b11000000;
+pub const LENGTH_MASK: u8 = LastUtf8Char::L0 as u8;
 
 const EMPTY: Repr = Repr::new_inline("");
 
@@ -45,7 +45,7 @@ pub struct Repr(
     u16,
     u8,
     // ...so that the last byte can be a NonMax, which allows the compiler to see a niche value
-    NonMaxU8,
+    LastUtf8Char,
 );
 
 unsafe impl Send for Repr {}
@@ -381,23 +381,36 @@ impl Repr {
     #[allow(clippy::len_without_is_empty)] // is_empty exists on CompactString
     #[inline]
     pub fn len(&self) -> usize {
-        // the last byte stores our discriminant and stack length
-        let last_byte = self.last_byte();
+        // This ugly looking code results in two conditional moves and only one comparison, without
+        // branching. The outcome of a comparison is a tristate `{lt, eq, gt}`, but the compiler
+        // won't use this optimization if you match on `len_inline.cmp(&MAX_SIZE)`, so we have to
+        // do it manually.
+
+        // Force the compiler to read the variable, so it won't put the reading in a branch.
+        let len_heap = self.1;
+        // SAFETY: This assembly instruction is a noop that only affects the instruction ordering.
+        #[cfg(not(miri))]
+        unsafe {
+            std::arch::asm!(
+                "/* {len_heap} */",
+                len_heap = in(reg) len_heap,
+                options(nomem, nostack),
+            );
+        };
 
-        // initially has the value of the stack length, conditionally becomes the heap length
-        let mut length = core::cmp::min((last_byte.wrapping_sub(LENGTH_MASK)) as usize, MAX_SIZE);
-        let heap_length = self.1;
-        let length_ref = &mut length;
+        // Extending the variable early results in fewer instructions, because loading and
+        // extending can be done in one instruction.
+        let len_inline = (self.last_byte() as usize).wrapping_sub(LENGTH_MASK as usize);
 
-        // our discriminant is stored in the last byte and denotes stack vs heap
-        //
-        // Note: We should never add an `else` statement here, keeping the conditional simple allows
-        // the compiler to optimize this to a conditional-move instead of a branch
-        if last_byte == HEAP_MASK {
-            *length_ref = heap_length;
+        // Do not add an `else` statement, or the compiler will introduce branching.
+        let mut len = len_heap;
+        if len_inline < MAX_SIZE {
+            len = len_inline;
         }
-
-        *length_ref
+        if len_inline > MAX_SIZE {
+            len = MAX_SIZE;
+        }
+        len
     }
 
     /// Returns the overall capacity of the underlying buffer


Test patch:
diff --git a/compact_str/src/tests.rs b/compact_str/src/tests.rs
index dbabdefb..757acd40 100644
--- a/compact_str/src/tests.rs
+++ b/compact_str/src/tests.rs
@@ -1580,3 +1580,16 @@ fn test_from_string_buffer_inlines_on_clone() {
     // when cloning the CompactString we should inline it
     assert!(!b.is_heap_allocated());
 }
+
+#[test]
+fn multiple_nieches_test() {
+    #[allow(unused)]
+    enum Value {
+        String(CompactString),
+        Bool(bool),
+        Signed(isize),
+        Unsigned(usize),
+        Null,
+    }
+    assert_eq!(std::mem::size_of::<Value>(), std::mem::size_of::<String>());
+}