Repository: ParkMyCar/compact_str
Pull request #49 (2022-02-09 02:41:42)

refactor: Combine Inline and Packed Variants
This PR refactors `Repr` to combine the Inline and Packed variants. Now a `Repr` only has two states, `Heap` and `Inline`. Our discriminant/metadata is moved to be the last bit, which enables us to inline 24 character strings (12 characters on 32-bit archs), regardless of whether or not the first character is ASCII


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ed60cdf6..09cca149 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,6 +9,7 @@ name: CI
 
 env:
   RUSTFLAGS: "-D warnings"
+  PROPTEST_CASES: 10000
 
 jobs:
   check:
@@ -29,8 +30,6 @@ jobs:
   test:
     name: cargo test
     runs-on: ubuntu-latest
-    env:
-      PROPTEST_CASES: 500
     steps:
       - uses: actions/checkout@v2
       - uses: actions-rs/toolchain@v1
@@ -41,13 +40,11 @@ jobs:
       - uses: actions-rs/cargo@v1
         with:
           command: test
-          args: --all-features
+          args: --release --all-features -- --include-ignored
 
   test-nightly:
     name: cargo test nightly
     runs-on: ubuntu-latest
-    env:
-      PROPTEST_CASES: 500
     steps:
       - uses: actions/checkout@v2
       - uses: actions-rs/toolchain@v1
@@ -58,7 +55,7 @@ jobs:
       - uses: actions-rs/cargo@v1
         with:
           command: test
-          args: --all-features
+          args: --release --all-features -- --include-ignored
 
   miri:
     name: cargo miri test
@@ -73,7 +70,7 @@ jobs:
           components: miri
       - name: Run Miri
         run: |
-          cargo miri test
+          cargo miri test --all-features
 
   fuzz-creation:
     name: fuzz creation
diff --git a/.github/workflows/msrv.yml b/.github/workflows/msrv.yml
index 45f62595..5de5eab8 100644
--- a/.github/workflows/msrv.yml
+++ b/.github/workflows/msrv.yml
@@ -8,8 +8,6 @@ on:
 name: MSRV
 
 env:
-  # local default for proptest is 100
-  PROPTEST_CASES: 1000
   RUSTFLAGS: "-D warnings"
 
 jobs:
diff --git a/compact_str/Cargo.toml b/compact_str/Cargo.toml
index 15852c0c..43fde2f6 100644
--- a/compact_str/Cargo.toml
+++ b/compact_str/Cargo.toml
@@ -38,3 +38,7 @@ harness = false
 [[bench]]
 name = "comparison"
 harness = false
+
+[[bench]]
+name = "random"
+harness = false
diff --git a/compact_str/benches/random.rs b/compact_str/benches/random.rs
new file mode 100644
index 00000000..edfcdaa9
--- /dev/null
+++ b/compact_str/benches/random.rs
@@ -0,0 +1,37 @@
+//! Random benchmarks to determine if one bit of code is faster than another
+
+use criterion::{
+    black_box,
+    criterion_group,
+    criterion_main,
+    Criterion,
+};
+
+fn if_statement_min(c: &mut Criterion) {
+    let mask = 192;
+    let vals: [u8; 4] = [0, 46, 202, 255];
+    c.bench_function("if statement min", |b| {
+        b.iter(|| {
+            for x in vals {
+                let len = if x >= mask { (x & !mask) as usize } else { 24 };
+                black_box(len);
+            }
+        })
+    });
+}
+
+fn cmp_min(c: &mut Criterion) {
+    let mask = 192;
+    let vals: [u8; 4] = [0, 46, 202, 255];
+    c.bench_function("cmp min", |b| {
+        b.iter(|| {
+            for x in vals {
+                let len = core::cmp::min(x.wrapping_sub(mask), 24);
+                black_box(len);
+            }
+        })
+    });
+}
+
+criterion_group!(random, if_statement_min, cmp_min);
+criterion_main!(random);
diff --git a/compact_str/fuzz/fuzz_targets/creation.rs b/compact_str/fuzz/fuzz_targets/creation.rs
index 8ee1b3f8..bee2ea33 100644
--- a/compact_str/fuzz/fuzz_targets/creation.rs
+++ b/compact_str/fuzz/fuzz_targets/creation.rs
@@ -27,9 +27,7 @@ fuzz_target!(|method: CreationMethod<'_>| {
             assert_eq!(compact, word);
 
             // assert the CompactStr is properly allocated
-            if word.len() < MAX_INLINE_LENGTH {
-                assert!(!compact.is_heap_allocated());
-            } else if word.len() == MAX_INLINE_LENGTH && word.as_bytes()[0] <= 127 {
+            if word.len() <= MAX_INLINE_LENGTH {
                 assert!(!compact.is_heap_allocated());
             } else {
                 assert!(compact.is_heap_allocated());
@@ -43,10 +41,7 @@ fuzz_target!(|method: CreationMethod<'_>| {
             assert_eq!(compact, std_str);
 
             // assert the CompactStr is properly allocated
-            //
-            // Note: Creating a CompactStr from an iterator doesn't yet support the Packed
-            // representation, so we can only inline MAX_INLINE_LENGTH - 1
-            if std_str.len() < MAX_INLINE_LENGTH {
+            if std_str.len() <= MAX_INLINE_LENGTH {
                 assert!(!compact.is_heap_allocated());
             } else {
                 assert!(compact.is_heap_allocated());
@@ -60,10 +55,7 @@ fuzz_target!(|method: CreationMethod<'_>| {
             assert_eq!(compact, std_str);
 
             // assert the CompactStr is properly allocated
-            //
-            // Note: Creating a CompactStr from an iterator doesn't yet support the Packed
-            // representation, so we can only inline MAX_INLINE_LENGTH - 1
-            if std_str.len() < MAX_INLINE_LENGTH {
+            if std_str.len() <= MAX_INLINE_LENGTH {
                 assert!(!compact.is_heap_allocated());
             } else {
                 assert!(compact.is_heap_allocated());
@@ -81,9 +73,7 @@ fuzz_target!(|method: CreationMethod<'_>| {
                     assert_eq!(c, s);
 
                     // assert the CompactStr is properly allocated
-                    if s.len() < MAX_INLINE_LENGTH {
-                        assert!(!c.is_heap_allocated());
-                    } else if s.len() == MAX_INLINE_LENGTH && s.as_bytes()[0] <= 127 {
+                    if s.len() <= MAX_INLINE_LENGTH {
                         assert!(!c.is_heap_allocated());
                     } else {
                         assert!(c.is_heap_allocated());
diff --git a/compact_str/src/features/bytes.rs b/compact_str/src/features/bytes.rs
index 69da4f13..6702c639 100644
--- a/compact_str/src/features/bytes.rs
+++ b/compact_str/src/features/bytes.rs
@@ -74,7 +74,7 @@ mod test {
 
     use crate::CompactStr;
 
-    const MAX_INLINED_SIZE: usize = core::mem::size_of::<String>();
+    const MAX_SIZE: usize = core::mem::size_of::<String>();
 
     // generates random unicode strings, upto 80 chars long
     fn rand_unicode() -> impl Strategy<Value = String> {
@@ -98,9 +98,7 @@ mod test {
             let mut buf = Cursor::new(word.as_bytes());
             let compact = CompactStr::from_utf8_buf(&mut buf).unwrap();
 
-            if word.len() < MAX_INLINED_SIZE {
-                prop_assert!(!compact.is_heap_allocated())
-            } else if word.len() == MAX_INLINED_SIZE && word.as_bytes()[0] <= 127 {
+            if word.len() <= MAX_SIZE {
                 prop_assert!(!compact.is_heap_allocated())
             } else {
                 prop_assert!(compact.is_heap_allocated())
diff --git a/compact_str/src/lib.rs b/compact_str/src/lib.rs
index d20d9ed9..e74eda5e 100644
--- a/compact_str/src/lib.rs
+++ b/compact_str/src/lib.rs
@@ -9,7 +9,7 @@
 //! 3. A `usize` denoting the capacity of the string
 //!
 //! On 64-bit architectures this results in 24 bytes being stored on the stack (12 bytes for 32-bit
-//! architectures). For small strings, e.g. <= 23 characters, instead of storing a pointer, length,
+//! architectures). For small strings, e.g. <= 24 characters, instead of storing a pointer, length,
 //! and capacity on the stack, you store the string itself! This avoids the need to heap allocate
 //! which reduces the amount of memory used, and improves performance.
 
@@ -165,7 +165,7 @@ impl CompactStr {
     /// ```
     /// # use compact_str::CompactStr;
     /// let empty = CompactStr::with_capacity(0);
-    /// let min_size = std::mem::size_of::<String>() - 1;
+    /// let min_size = std::mem::size_of::<String>();
     ///
     /// assert_eq!(empty.capacity(), min_size);
     /// assert_ne!(0, min_size);
@@ -175,10 +175,10 @@ impl CompactStr {
     /// ### Heap Allocating
     /// ```
     /// # use compact_str::CompactStr;
-    /// // If you create a `CompactStr` with a capacity greater than or equal to
+    /// // If you create a `CompactStr` with a capacity greater than
     /// // `std::mem::size_of::<String>`, it will heap allocated
     ///
-    /// let heap_size = std::mem::size_of::<String>();
+    /// let heap_size = std::mem::size_of::<String>() + 1;
     /// let empty = CompactStr::with_capacity(heap_size);
     ///
     /// assert_eq!(empty.capacity(), heap_size);
@@ -316,7 +316,7 @@ impl CompactStr {
     /// ```
     #[inline]
     pub fn as_bytes(&self) -> &[u8] {
-        self.repr.as_slice()
+        &self.repr.as_slice()[..self.len()]
     }
 
     // TODO: Implement a `try_as_mut_slice(...)` that will fail if it results in cloning?
diff --git a/compact_str/src/repr/bytes.rs b/compact_str/src/repr/bytes.rs
index fb552e31..13e48991 100644
--- a/compact_str/src/repr/bytes.rs
+++ b/compact_str/src/repr/bytes.rs
@@ -4,24 +4,18 @@ use bytes::Buf;
 
 use super::{
     Repr,
+    HEAP_MASK,
     MAX_SIZE,
 };
 
-#[cfg(target_pointer_width = "32")]
-const DEFAULT_TEXT: &str = "000000000000";
-#[cfg(target_pointer_width = "64")]
-const DEFAULT_TEXT: &str = "000000000000000000000000";
-
-const DEFAULT_PACKED: Repr = Repr::new_const(DEFAULT_TEXT);
-
 impl Repr {
     /// Converts a buffer of bytes to a `Repr`,
     pub fn from_utf8_buf<B: Buf>(buf: &mut B) -> Result<Self, Utf8Error> {
         // SAFETY: We check below to make sure the provided buffer is valid UTF-8
-        let repr = unsafe { Self::from_utf8_buf_unchecked(buf) };
+        let (repr, bytes_written) = unsafe { Self::collect_buf(buf) };
 
         // Check to make sure the provided bytes are valid UTF-8, return the Repr if they are!
-        match core::str::from_utf8(repr.as_slice()) {
+        match core::str::from_utf8(&repr.as_slice()[..bytes_written]) {
             Ok(_) => Ok(repr),
             Err(e) => Err(e),
         }
@@ -32,46 +26,53 @@ impl Repr {
     /// # Safety
     /// The provided buffer must be valid UTF-8
     pub unsafe fn from_utf8_buf_unchecked<B: Buf>(buf: &mut B) -> Self {
-        let size = buf.remaining();
-        let chunk = buf.chunk();
-
-        // Check to make sure we're not empty, so accessing the first byte below doesn't panic
-        if chunk.is_empty() {
-            // If the chunk is empty, then we should have 0 remaining bytes
-            debug_assert_eq!(size, 0);
-            return super::EMPTY;
+        let (repr, _bytes_written) = Self::collect_buf(buf);
+        repr
+    }
+
+    unsafe fn collect_buf<B: Buf>(buf: &mut B) -> (Self, usize) {
+        // Get an empty Repr we can write into
+        let mut repr = super::EMPTY;
+        let mut bytes_written = 0;
+
+        debug_assert_eq!(repr.len(), bytes_written);
+
+        while buf.has_remaining() {
+            let chunk = buf.chunk();
+            let chunk_len = chunk.len();
+
+            // There's an edge case where the final byte of this buffer == `HEAP_MASK`, which is
+            // invalid UTF-8, but would result in us creating an inline variant, that identifies as
+            // a heap variant. If a user ever tried to reference the data at all, we'd incorrectly
+            // try and read data from an invalid memory address, causing undefined behavior.
+            if bytes_written < MAX_SIZE && bytes_written + chunk_len == MAX_SIZE {
+                let last_byte = chunk[chunk_len - 1];
+                // If we hit the edge case, reserve additional space to make the repr becomes heap
+                // allocated, which prevents us from writing this last byte inline
+                if last_byte == HEAP_MASK {
+                    repr.reserve(MAX_SIZE + 1);
+                }
+            }
+
+            // reserve at least enough space to fit this chunk
+            repr.reserve(chunk_len);
+
+            // SAFETY: The caller is responsible for making sure the provided buffer is UTF-8. This
+            // invariant is documented in the public API
+            let slice = repr.as_mut_slice();
+            // write the chunk into the Repr
+            slice[bytes_written..bytes_written + chunk_len].copy_from_slice(chunk);
+
+            // Set the length of the Repr
+            // SAFETY: We just wrote an additional `chunk_len` bytes into the Repr
+            bytes_written += chunk_len;
+            repr.set_len(bytes_written);
+
+            // advance the pointer of the buffer
+            buf.advance(chunk_len);
         }
-        let first_byte = buf.chunk()[0];
-
-        // Get an "empty" Repr we can write into
-        //
-        // HACK: There currently isn't a way to provide an "empty" Packed repr, so we do this check
-        // and return a "default" Packed repr if the buffer can fit
-        let mut repr = if size == MAX_SIZE && first_byte <= 127 {
-            // Note: No need to reserve additional bytes here, because we know we can fit all
-            // remaining bytes of `buf` into a Packed repr
-            DEFAULT_PACKED
-        } else {
-            let mut default = super::EMPTY;
-            debug_assert_eq!(default.len(), 0);
-
-            // Reserve enough bytes, possibly allocating on the heap, to store the text
-            default.reserve(size);
-
-            default
-        };
-
-        // SAFETY: The caller is responsible for making sure the provided buffer is UTF-8. This
-        // invariant is documented in the public API
-        let slice = repr.as_mut_slice();
-        // Copy the bytes from the buffer into our Repr!
-        buf.copy_to_slice(&mut slice[..size]);
-
-        // Set the length of the Repr
-        // SAFETY: We just wrote `size` bytes into the Repr
-        repr.set_len(size);
 
-        repr
+        (repr, bytes_written)
     }
 }
 
@@ -124,6 +125,36 @@ mod test {
         assert!(!repr.is_heap_allocated());
     }
 
+    #[test]
+    fn test_fuzz_panic() {
+        let bytes = &[
+            255, 255, 255, 255, 255, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 1, 12, 0, 0, 96,
+        ];
+        let mut buf: Cursor<&[u8]> = Cursor::new(bytes);
+
+        assert!(Repr::from_utf8_buf(&mut buf).is_err());
+    }
+
+    #[test]
+    fn test_valid_repr_but_invalid_utf8() {
+        let bytes = &[
+            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192,
+        ];
+        let mut buf: Cursor<&[u8]> = Cursor::new(bytes);
+
+        assert!(Repr::from_utf8_buf(&mut buf).is_err());
+    }
+
+    #[test]
+    fn test_fake_heap_variant() {
+        let bytes = &[
+            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255,
+        ];
+        let mut buf: Cursor<&[u8]> = Cursor::new(bytes);
+
+        assert!(Repr::from_utf8_buf(&mut buf).is_err());
+    }
+
     #[test]
     #[should_panic(expected = "Utf8Error")]
     fn test_invalid_utf8() {
diff --git a/compact_str/src/repr/discriminant.rs b/compact_str/src/repr/discriminant.rs
index 03228afa..a74ebc33 100644
--- a/compact_str/src/repr/discriminant.rs
+++ b/compact_str/src/repr/discriminant.rs
@@ -1,34 +1,30 @@
 use super::{
     HEAP_MASK,
-    LEADING_BIT_MASK,
     MAX_SIZE,
 };
 
-const PADDING_SIZE: usize = MAX_SIZE - std::mem::size_of::<Discriminant>();
+const PADDING_SIZE: usize = MAX_SIZE - std::mem::size_of::<u8>();
 
 #[derive(Debug, Copy, Clone)]
 pub enum Discriminant {
     Heap,
     Inline,
-    Packed,
 }
 
 #[repr(C)]
 #[derive(Debug, Copy, Clone)]
 pub struct DiscriminantMask {
-    val: u8,
     _padding: [u8; PADDING_SIZE],
+    val: u8,
 }
 
 impl DiscriminantMask {
-    #[inline]
+    #[inline(always)]
     pub const fn discriminant(&self) -> Discriminant {
         if self.val == HEAP_MASK {
             Discriminant::Heap
-        } else if self.val & LEADING_BIT_MASK == LEADING_BIT_MASK {
-            Discriminant::Inline
         } else {
-            Discriminant::Packed
+            Discriminant::Inline
         }
     }
 }
diff --git a/compact_str/src/repr/heap.rs b/compact_str/src/repr/heap.rs
index ec6bb19e..f833f621 100644
--- a/compact_str/src/repr/heap.rs
+++ b/compact_str/src/repr/heap.rs
@@ -12,8 +12,8 @@ const PADDING: [u8; PADDING_SIZE] = [HEAP_MASK; PADDING_SIZE];
 #[repr(C)]
 #[derive(Debug, Clone)]
 pub struct HeapString {
-    padding: [u8; PADDING_SIZE],
     pub string: ArcString,
+    padding: [u8; PADDING_SIZE],
 }
 
 impl HeapString {
diff --git a/compact_str/src/repr/inline.rs b/compact_str/src/repr/inline.rs
index e18a76f2..1efc828d 100644
--- a/compact_str/src/repr/inline.rs
+++ b/compact_str/src/repr/inline.rs
@@ -1,45 +1,37 @@
-use super::{
-    LEADING_BIT_MASK,
-    MAX_SIZE,
-};
+use super::MAX_SIZE;
 
-pub const MAX_INLINE_SIZE: usize = MAX_SIZE - core::mem::size_of::<Metadata>();
-
-type Metadata = u8;
+const LENGTH_MASK: u8 = 0b11000000;
 
 #[repr(C)]
 #[derive(Debug, Copy, Clone)]
 pub struct InlineString {
-    metadata: Metadata,
-    buffer: [u8; MAX_INLINE_SIZE],
+    buffer: [u8; MAX_SIZE],
 }
 
 impl InlineString {
-    const fn empty() -> Self {
-        InlineString {
-            metadata: LEADING_BIT_MASK,
-            buffer: [0u8; MAX_INLINE_SIZE],
-        }
-    }
-
     #[inline]
     pub fn new(text: &str) -> Self {
-        debug_assert!(text.len() <= MAX_INLINE_SIZE);
+        debug_assert!(text.len() <= MAX_SIZE);
 
         let len = text.len();
-        let mut new = Self::empty();
+        let mut buffer = [0u8; MAX_SIZE];
 
         // set the length
-        new.metadata |= len as u8;
+        buffer[MAX_SIZE - 1] = len as u8 | LENGTH_MASK;
+
         // copy the string
-        new.buffer.as_mut()[..len].copy_from_slice(text.as_bytes());
+        //
+        // note: in the case where len == MAX_SIZE, we'll overwrite the len, but that's okay because
+        // when reading the length we can detect that the last byte is part of UTF-8 and return a
+        // length of MAX_SIZE
+        unsafe { std::ptr::copy_nonoverlapping(text.as_ptr(), buffer.as_mut_ptr(), len) };
 
-        new
+        InlineString { buffer }
     }
 
     #[inline]
     pub const fn new_const(text: &str) -> Self {
-        if text.len() > MAX_INLINE_SIZE {
+        if text.len() > MAX_SIZE {
             // HACK: This allows us to make assertions within a `const fn` without requiring
             // nightly, see unstable `const_panic` feature. This results in a build
             // failure, not a runtime panic
@@ -49,8 +41,10 @@ impl InlineString {
         }
 
         let len = text.len();
-        let metadata = (len as u8) | LEADING_BIT_MASK;
-        let mut buffer = [0u8; MAX_INLINE_SIZE];
+        let mut buffer = [0u8; MAX_SIZE];
+
+        // set the length
+        buffer[MAX_SIZE - 1] = len as u8 | LENGTH_MASK;
 
         // Note: for loops aren't allowed in `const fn`, hence the while
         let mut i = 0;
@@ -59,35 +53,38 @@ impl InlineString {
             i += 1;
         }
 
-        InlineString { metadata, buffer }
+        InlineString { buffer }
     }
 
-    /// Creates an `InlineString` from raw parts without checking that it's valid UTF-8.
+    /// Creates an `InlineString` from raw parts without checking that it's valid UTF-8
     #[inline]
-    pub const unsafe fn from_parts(len: usize, buffer: [u8; MAX_INLINE_SIZE]) -> Self {
-        let metadata = (len as u8) | LEADING_BIT_MASK;
-        InlineString { metadata, buffer }
+    pub const unsafe fn from_parts(len: usize, mut buffer: [u8; MAX_SIZE]) -> Self {
+        if len != MAX_SIZE {
+            buffer[MAX_SIZE - 1] = len as u8 | LENGTH_MASK;
+        }
+        InlineString { buffer }
     }
 
-    #[inline]
-    pub const fn len(&self) -> usize {
-        (self.metadata & !LEADING_BIT_MASK) as usize
+    #[inline(always)]
+    pub fn len(&self) -> usize {
+        let last_byte = self.buffer[MAX_SIZE - 1];
+        core::cmp::min((last_byte.wrapping_sub(LENGTH_MASK)) as usize, MAX_SIZE)
     }
 
     #[inline]
     pub const fn capacity(&self) -> usize {
-        MAX_INLINE_SIZE
+        MAX_SIZE
     }
 
     #[inline]
     pub fn as_str(&self) -> &str {
         // SAFETY: You can only construct an InlineString via a &str
-        unsafe { ::std::str::from_utf8_unchecked(self.as_slice()) }
+        unsafe { ::std::str::from_utf8_unchecked(&self.as_slice()[..self.len()]) }
     }
 
-    #[inline(always)]
+    #[inline]
     pub fn as_slice(&self) -> &[u8] {
-        &self.buffer[..self.len()]
+        &self.buffer[..]
     }
 
     /// Provides a mutable reference to the underlying buffer
@@ -101,10 +98,14 @@ impl InlineString {
 
     #[inline]
     pub unsafe fn set_len(&mut self, length: usize) {
-        debug_assert!(length <= MAX_INLINE_SIZE);
+        debug_assert!(length <= MAX_SIZE);
 
-        // Set the leading bit mask, and then or our length
-        self.metadata = LEADING_BIT_MASK | length as u8;
+        // If `length` == MAX_SIZE, then we infer the length to be the capacity of the buffer. We
+        // can infer this because the way we encode length doesn't overlap with any valid UTF-8
+        // bytes
+        if length < MAX_SIZE {
+            self.buffer[MAX_SIZE - 1] = length as u8 | LENGTH_MASK;
+        }
     }
 }
 
@@ -112,8 +113,60 @@ crate::asserts::assert_size_eq!(InlineString, String);
 
 #[cfg(test)]
 mod tests {
+    use std::convert::TryFrom;
+
+    use proptest::prelude::*;
+
+    use super::{
+        InlineString,
+        MAX_SIZE,
+    };
+    use crate::tests::rand_unicode_bytes;
+
+    #[test]
+    fn test_sanity() {
+        let hello = "hello world!";
+        let inline = InlineString::new(hello);
+
+        assert_eq!(inline.as_str(), hello);
+        assert_eq!(inline.len(), hello.len());
+        assert_eq!(inline.capacity(), MAX_SIZE);
+    }
+
+    proptest! {
+        #[test]
+        #[cfg_attr(miri, ignore)]
+        fn test_roundtrip(s in rand_unicode_bytes(MAX_SIZE)) {
+            let inline = InlineString::new(&s);
+
+            assert_eq!(inline.as_str(), s);
+            assert_eq!(inline.len(), s.len());
+        }
+    }
+
     #[test]
-    fn test_sanity_not_valid_utf8() {
-        assert!(std::str::from_utf8(&[0b11111111]).is_err())
+    #[ignore] // we run this in CI, but unless you're compiling in release, this takes a while
+    fn test_unused_utf8_bytes() {
+        // test to validate for all char the first and last bytes are never within a specified range
+        // note: according to the UTF-8 spec it shouldn't be, but we double check that here
+        for i in 0..u32::MAX {
+            if let Ok(c) = char::try_from(i) {
+                let mut buf = [0_u8; 4];
+                c.encode_utf8(&mut buf);
+
+                // check ranges for first byte
+                match buf[0] {
+                    x @ 128..=191 => panic!("first byte within 128..=191, {}", x),
+                    x @ 248..=255 => panic!("first byte within 248..=255, {}", x),
+                    _ => (),
+                }
+
+                // check ranges for last byte
+                match buf[c.len_utf8() - 1] {
+                    x @ 192..=255 => panic!("last byte within 192..=255, {}", x),
+                    _ => (),
+                }
+            }
+        }
     }
 }
diff --git a/compact_str/src/repr/iter.rs b/compact_str/src/repr/iter.rs
index bad0948b..f983d820 100644
--- a/compact_str/src/repr/iter.rs
+++ b/compact_str/src/repr/iter.rs
@@ -3,11 +3,11 @@
 use core::iter::FromIterator;
 use core::mem::ManuallyDrop;
 
-use super::inline::MAX_INLINE_SIZE;
 use super::{
     HeapString,
     InlineString,
     Repr,
+    MAX_SIZE,
 };
 
 impl FromIterator<char> for Repr {
@@ -16,7 +16,7 @@ impl FromIterator<char> for Repr {
 
         // If the size hint indicates we can't store this inline, then create a heap string
         let (size_hint, _) = iter.size_hint();
-        if size_hint > MAX_INLINE_SIZE {
+        if size_hint > MAX_SIZE {
             let heap = HeapString::from(iter.collect::<String>());
             return Repr {
                 heap: ManuallyDrop::new(heap),
@@ -25,13 +25,13 @@ impl FromIterator<char> for Repr {
 
         // Otherwise, continuously pull chars from the iterator
         let mut curr_len = 0;
-        let mut inline_buf = [0u8; MAX_INLINE_SIZE];
+        let mut inline_buf = [0u8; MAX_SIZE];
         while let Some(c) = iter.next() {
             let char_len = c.len_utf8();
 
             // If this new character is too large to fit into the inline buffer, then create a heap
             // string
-            if char_len + curr_len > MAX_INLINE_SIZE {
+            if char_len + curr_len > MAX_SIZE {
                 let (min_remaining, _) = iter.size_hint();
                 let mut heap_buf = String::with_capacity(char_len + curr_len + min_remaining);
 
@@ -55,8 +55,6 @@ impl FromIterator<char> for Repr {
             curr_len += char_len;
         }
 
-        // TODO: Support PackedString here in an efficient way
-
         // SAFETY: We know `inline_buf` is valid UTF-8 because it consists entriely of `char`s
         let inline = unsafe { InlineString::from_parts(curr_len, inline_buf) };
         Repr { inline }
@@ -78,7 +76,7 @@ where
 {
     // If there are more strings than we can fit bytes inline, then immediately make a `HeapString`
     let (size_hint, _) = iter.size_hint();
-    if size_hint > MAX_INLINE_SIZE {
+    if size_hint > MAX_SIZE {
         let heap = HeapString::from(iter.collect::<String>());
         return Repr {
             heap: ManuallyDrop::new(heap),
@@ -87,13 +85,13 @@ where
 
     // Otherwise, continuously pull strings from the iterator
     let mut curr_len = 0;
-    let mut inline_buf = [0u8; MAX_INLINE_SIZE];
+    let mut inline_buf = [0u8; MAX_SIZE];
     while let Some(s) = iter.next() {
         let str_slice = s.as_ref();
         let bytes_len = str_slice.len();
 
         // this new string is too large to fit into our inline buffer, so heap allocate the rest
-        if bytes_len + curr_len > MAX_INLINE_SIZE {
+        if bytes_len + curr_len > MAX_SIZE {
             let (min_remaining, _) = iter.size_hint();
             let mut heap_buf = String::with_capacity(bytes_len + curr_len + min_remaining);
 
@@ -116,8 +114,6 @@ where
         curr_len += bytes_len;
     }
 
-    // TODO: Support PackedString here in an efficient way
-
     // SAFETY: We know `inline_buf` is valid UTF-8 because it consists entriely of `&str`s
     let inline = unsafe { InlineString::from_parts(curr_len, inline_buf) };
     Repr { inline }
@@ -163,6 +159,25 @@ mod tests {
         assert!(!repr.is_heap_allocated());
     }
 
+    #[test]
+    #[cfg_attr(target_pointer_width = "32", ignore)]
+    fn packed_char_iter() {
+        let chars = [
+            '\u{92f01}',
+            '\u{81515}',
+            '\u{81515}',
+            '\u{81515}',
+            '\u{81515}',
+            '\u{41515}',
+        ];
+
+        let repr: Repr = chars.iter().collect();
+        let s: String = chars.iter().collect();
+
+        assert_eq!(repr.as_str(), s.as_str());
+        assert!(!repr.is_heap_allocated());
+    }
+
     #[test]
     fn long_char_iter() {
         let long = "This is supposed to be a really long string";
diff --git a/compact_str/src/repr/mod.rs b/compact_str/src/repr/mod.rs
index 97f8eadb..80088ed9 100644
--- a/compact_str/src/repr/mod.rs
+++ b/compact_str/src/repr/mod.rs
@@ -10,7 +10,6 @@ mod arc;
 mod discriminant;
 mod heap;
 mod inline;
-mod packed;
 
 use discriminant::{
     Discriminant,
@@ -18,7 +17,6 @@ use discriminant::{
 };
 use heap::HeapString;
 use inline::InlineString;
-use packed::PackedString;
 
 const MAX_SIZE: usize = std::mem::size_of::<String>();
 const EMPTY: Repr = Repr {
@@ -27,13 +25,11 @@ const EMPTY: Repr = Repr {
 
 // Used as a discriminant to identify different variants
 pub const HEAP_MASK: u8 = 0b11111111;
-pub const LEADING_BIT_MASK: u8 = 0b10000000;
 
 pub union Repr {
     mask: DiscriminantMask,
     heap: ManuallyDrop<HeapString>,
     inline: InlineString,
-    packed: PackedString,
 }
 
 impl Repr {
@@ -44,12 +40,9 @@ impl Repr {
 
         if len == 0 {
             EMPTY
-        } else if len <= inline::MAX_INLINE_SIZE {
+        } else if len <= MAX_SIZE {
             let inline = InlineString::new(text);
             Repr { inline }
-        } else if len == MAX_SIZE && text.as_bytes()[0] <= 127 {
-            let packed = PackedString::new(text);
-            Repr { packed }
         } else {
             let heap = ManuallyDrop::new(HeapString::new(text));
             Repr { heap }
@@ -60,12 +53,9 @@ impl Repr {
     pub const fn new_const(text: &str) -> Self {
         let len = text.len();
 
-        if len <= inline::MAX_INLINE_SIZE {
+        if len <= MAX_SIZE {
             let inline = InlineString::new_const(text);
             Repr { inline }
-        } else if len == MAX_SIZE && text.as_bytes()[0] <= 127 {
-            let packed = PackedString::new_const(text);
-            Repr { packed }
         } else {
             // HACK: This allows us to make assertions within a `const fn` without requiring
             // nightly, see unstable `const_panic` feature. This results in a build
@@ -79,7 +69,7 @@ impl Repr {
 
     #[inline]
     pub fn with_capacity(capacity: usize) -> Self {
-        if capacity <= inline::MAX_INLINE_SIZE {
+        if capacity <= MAX_SIZE {
             EMPTY
         } else {
             let heap = ManuallyDrop::new(HeapString::with_capacity(capacity));
@@ -107,8 +97,8 @@ impl Repr {
             return;
         }
 
-        // Note: Inlined strings (i.e. inline and packed) are already their maximum size. So if our
-        // current capacity isn't large enough, then we always need to create a heap variant
+        // Note: Inlined strings are already their maximum size. So if our current capacity isn't
+        // large enough, then we always need to create a heap variant
 
         // Create a `HeapString` with `text.len() + additional` capacity
         let heap = HeapString::with_additional(self.as_str(), additional);
@@ -154,22 +144,8 @@ impl Repr {
     pub fn pop(&mut self) -> Option<char> {
         let ch = self.as_str().chars().rev().next()?;
 
-        match self.cast() {
-            StrongRepr::Packed(packed) => {
-                let mut inline_buffer = [0; inline::MAX_INLINE_SIZE];
-
-                let new_len = packed.len() - ch.len_utf8();
-                let buffer: &mut [u8] = &mut inline_buffer[..new_len];
-
-                buffer.copy_from_slice(&packed.as_slice()[..new_len]);
-
-                let inline = unsafe { InlineString::from_parts(new_len, inline_buffer) };
-                *self = Repr { inline }
-            }
-            StrongRepr::Inline(_) | StrongRepr::Heap(_) => {
-                unsafe { self.set_len(self.len() - ch.len_utf8()) };
-            }
-        }
+        // SAFETY: We know this is is a valid length which falls on a char boundary
+        unsafe { self.set_len(self.len() - ch.len_utf8()) };
 
         Some(ch)
     }
@@ -205,7 +181,7 @@ impl Repr {
 
     #[inline(always)]
     fn discriminant(&self) -> Discriminant {
-        // SAFETY: `heap`, `inline`, and `packed` all store a discriminant in their first byte
+        // SAFETY: `heap` and `inline` all store a discriminant in their last byte
         unsafe { self.mask.discriminant() }
     }
 
@@ -220,10 +196,6 @@ impl Repr {
                 // SAFETY: We checked the discriminant to make sure the union is `inline`
                 StrongRepr::Inline(unsafe { &self.inline })
             }
-            Discriminant::Packed => {
-                // SAFETY: We checked the discriminant to make sure the union is `packed`
-                StrongRepr::Packed(unsafe { &self.packed })
-            }
         }
     }
 
@@ -238,10 +210,6 @@ impl Repr {
                 // SAFETY: We checked the discriminant to make sure the union is `inline`
                 MutStrongRepr::Inline(unsafe { &mut self.inline })
             }
-            Discriminant::Packed => {
-                // SAFETY: We checked the discriminant to make sure the union is `packed`
-                MutStrongRepr::Packed(unsafe { &mut self.packed })
-            }
         }
     }
 }
@@ -251,7 +219,6 @@ impl Clone for Repr {
         match self.cast() {
             StrongRepr::Heap(heap) => Repr { heap: heap.clone() },
             StrongRepr::Inline(inline) => Repr { inline: *inline },
-            StrongRepr::Packed(packed) => Repr { packed: *packed },
         }
     }
 }
@@ -264,7 +231,7 @@ impl Drop for Repr {
                 unsafe { ManuallyDrop::drop(&mut self.heap) };
             }
             // No-op, the value is on the stack and doesn't need to be explicitly dropped
-            Discriminant::Inline | Discriminant::Packed => {}
+            Discriminant::Inline => {}
         }
     }
 }
@@ -282,15 +249,8 @@ impl Extend<char> for Repr {
 
         match self.cast_mut() {
             MutStrongRepr::Heap(heap) => heap.string.extend(iterator),
-            MutStrongRepr::Packed(packed) => {
-                let mut heap = HeapString::with_additional(packed.as_str(), lower_bound);
-                heap.string.extend(iterator);
-
-                // Replace `self` with the new Repr
-                let heap = ManuallyDrop::new(heap);
-                *self = Repr { heap };
-            }
             MutStrongRepr::Inline(inline) => {
+                // Check if the lower_bound of the iterator indicates we'll need to heap allocate
                 if lower_bound + inline.len() > MAX_SIZE {
                     let mut heap = HeapString::with_additional(inline.as_str(), lower_bound);
                     heap.string.extend(iterator);
@@ -301,11 +261,13 @@ impl Extend<char> for Repr {
                     return;
                 }
 
+                // Keep pulling characters off the iterator, eventually heap allocating if we run
+                // out of space inline!
                 while let Some(ch) = iterator.next() {
                     let inline_len = inline.len();
                     let char_len = ch.len_utf8();
 
-                    if inline_len + char_len < MAX_SIZE {
+                    if inline_len + char_len <= MAX_SIZE {
                         // SAFTEY: We're writing a `char` into the buffer, which we know is valid
                         // UTF-8
                         let buffer = unsafe { inline.as_mut_slice() };
@@ -316,49 +278,17 @@ impl Extend<char> for Repr {
                         unsafe { inline.set_len(inline_len + char_len) };
                     } else {
                         // We can't fit the remainder of the iterator in an InlineString, so we
-                        // either need to make a PackedString or HeapString
-                        let maybe_ch2 = iterator.next();
-                        let inline_slice = inline.as_slice();
-
-                        // We can make a PackedString!
-                        if inline_len + char_len == MAX_SIZE
-                            && maybe_ch2.is_none()
-                            && inline_slice[0] <= 127
-                        {
-                            // Guard against char_len being 0?
-                            debug_assert!(inline_len != MAX_SIZE);
-
-                            // Copy the contents of the InlineString, into a buffer which will be a
-                            // PackedString
-                            let mut buffer = [0; MAX_SIZE];
-                            buffer[..inline_len].copy_from_slice(inline_slice);
-                            ch.encode_utf8(&mut buffer[inline_len..]);
-
-                            // SAFETY: We created `buffer` from an InlineString which is valid
-                            // UTF-8, and appended a char, which is also
-                            // valid UTF-8
-                            let packed = unsafe { PackedString::from_parts(buffer) };
-                            *self = Repr { packed };
-                        } else {
-                            // We can't fit the remaining characters in a PackedString, so make a
-                            // HeapString
-                            let mut heap =
-                                HeapString::with_additional(inline.as_str(), lower_bound);
-
-                            heap.string.push(ch);
-
-                            // If we had a second character
-                            if let Some(ch2) = maybe_ch2 {
-                                heap.string.push(ch2);
-                            }
-
-                            // Extend the HeapString with the rest of the iterator
-                            heap.string.extend(iterator);
-
-                            // Replace `self` with the new Repr
-                            let heap = ManuallyDrop::new(heap);
-                            *self = Repr { heap };
-                        }
+                        // either need to make a HeapString
+                        let mut heap = HeapString::with_additional(inline.as_str(), lower_bound);
+
+                        // push the char we just popped off, but couldn't fit inline
+                        heap.string.push(ch);
+                        // write in the rest of the iterator!
+                        heap.string.extend(iterator);
+
+                        // Replace `self` with the new Repr
+                        let heap = ManuallyDrop::new(heap);
+                        *self = Repr { heap };
 
                         // All done!
                         return;
@@ -395,9 +325,8 @@ impl Extend<String> for Repr {
 
 #[derive(Debug)]
 enum StrongRepr<'a> {
-    Heap(&'a ManuallyDrop<HeapString>),
     Inline(&'a InlineString),
-    Packed(&'a PackedString),
+    Heap(&'a ManuallyDrop<HeapString>),
 }
 
 impl<'a> StrongRepr<'a> {
@@ -405,7 +334,6 @@ impl<'a> StrongRepr<'a> {
     pub fn len(self) -> usize {
         match self {
             Self::Inline(inline) => inline.len(),
-            Self::Packed(packed) => packed.len(),
             Self::Heap(heap) => heap.string.len(),
         }
     }
@@ -414,7 +342,6 @@ impl<'a> StrongRepr<'a> {
     pub fn capacity(self) -> usize {
         match self {
             Self::Inline(inline) => inline.capacity(),
-            Self::Packed(packed) => packed.capacity(),
             Self::Heap(heap) => heap.string.capacity(),
         }
     }
@@ -423,7 +350,6 @@ impl<'a> StrongRepr<'a> {
     pub fn into_str(self) -> &'a str {
         match self {
             Self::Inline(inline) => inline.as_str(),
-            Self::Packed(packed) => packed.as_str(),
             Self::Heap(heap) => heap.string.as_str(),
         }
     }
@@ -432,7 +358,6 @@ impl<'a> StrongRepr<'a> {
     pub fn into_slice(self) -> &'a [u8] {
         match self {
             Self::Inline(inline) => inline.as_slice(),
-            Self::Packed(packed) => packed.as_slice(),
             Self::Heap(heap) => heap.string.as_slice(),
         }
     }
@@ -440,9 +365,8 @@ impl<'a> StrongRepr<'a> {
 
 #[derive(Debug)]
 enum MutStrongRepr<'a> {
-    Heap(&'a mut ManuallyDrop<HeapString>),
     Inline(&'a mut InlineString),
-    Packed(&'a mut PackedString),
+    Heap(&'a mut ManuallyDrop<HeapString>),
 }
 
 impl<'a> MutStrongRepr<'a> {
@@ -450,7 +374,6 @@ impl<'a> MutStrongRepr<'a> {
     pub unsafe fn into_mut_slice(self) -> &'a mut [u8] {
         match self {
             Self::Inline(inline) => inline.as_mut_slice(),
-            Self::Packed(packed) => packed.as_mut_slice(),
             Self::Heap(heap) => heap.make_mut_slice(),
         }
     }
@@ -459,7 +382,6 @@ impl<'a> MutStrongRepr<'a> {
     pub unsafe fn set_len(self, length: usize) {
         match self {
             Self::Inline(inline) => inline.set_len(length),
-            Self::Packed(packed) => packed.set_len(length),
             Self::Heap(heap) => heap.set_len(length),
         }
     }
@@ -508,13 +430,13 @@ mod tests {
         let short = "abc";
         let mut repr = Repr::new(&short);
 
-        assert_eq!(repr.capacity(), word * 3 - 1);
+        assert_eq!(repr.capacity(), word * 3);
         assert_eq!(repr.as_str(), short);
 
-        // Reserve < WORD * 3 - 1
+        // Reserve < WORD * 3
         repr.reserve(word);
         // This shouldn't cause a resize
-        assert_eq!(repr.capacity(), word * 3 - 1);
+        assert_eq!(repr.capacity(), word * 3);
         // We should not be heap allocated
         assert!(!repr.is_heap_allocated());
 
diff --git a/compact_str/src/repr/packed.rs b/compact_str/src/repr/packed.rs
deleted file mode 100644
index e2b78188..00000000
--- a/compact_str/src/repr/packed.rs
+++ /dev/null
@@ -1,95 +0,0 @@
-use super::MAX_SIZE;
-
-#[repr(C)]
-#[derive(Debug, Copy, Clone)]
-pub struct PackedString {
-    buffer: [u8; MAX_SIZE],
-}
-
-impl PackedString {
-    #[inline]
-    pub fn new(text: &str) -> Self {
-        debug_assert_eq!(text.len(), MAX_SIZE);
-        debug_assert!(text.as_bytes()[0] <= 127);
-
-        let mut buffer = [0u8; MAX_SIZE];
-        buffer[..text.len()].copy_from_slice(text.as_bytes());
-
-        PackedString { buffer }
-    }
-
-    #[inline]
-    pub const fn new_const(text: &str) -> Self {
-        if text.len() != MAX_SIZE {
-            // HACK: This allows us to make assertions within a `const fn` without requiring
-            // nightly, see unstable `const_panic` feature. This results in a build
-            // failure, not a runtime panic
-            #[allow(clippy::no_effect)]
-            #[allow(unconditional_panic)]
-            ["Provided string has a length greater than MAX_SIZE!"][42];
-        }
-        if text.as_bytes()[0] > 127 {
-            // HACK: This allows us to make assertions within a `const fn` without requiring
-            // nightly, see unstable `const_panic` feature. This results in a build
-            // failure, not a runtime panic
-            #[allow(clippy::no_effect)]
-            #[allow(unconditional_panic)]
-            ["leading character of packed string isn't ASCII!"][42];
-        }
-
-        let mut buffer = [0u8; MAX_SIZE];
-        let mut i = 0;
-        while i < text.len() {
-            buffer[i] = text.as_bytes()[i];
-            i += 1;
-        }
-
-        PackedString { buffer }
-    }
-
-    /// Creates a `PackedString` from raw parts without checking that it's valid UTF-8, or if the
-    /// first character is <= 127 (aka ASCII)
-    #[inline]
-    pub const unsafe fn from_parts(buffer: [u8; MAX_SIZE]) -> Self {
-        PackedString { buffer }
-    }
-
-    #[inline]
-    pub const fn len(&self) -> usize {
-        MAX_SIZE
-    }
-
-    #[inline]
-    pub const fn capacity(&self) -> usize {
-        MAX_SIZE
-    }
-
-    #[inline]
-    pub fn as_str(&self) -> &str {
-        // SAFETY: You can only construct a PackedString via a &str
-        unsafe { ::std::str::from_utf8_unchecked(self.as_slice()) }
-    }
-
-    #[inline(always)]
-    pub fn as_slice(&self) -> &[u8] {
-        &self.buffer[..]
-    }
-
-    /// Provides a mutable reference to the underlying buffer
-    ///
-    /// # Invariants
-    /// * Please see `super::Repr` for all invariants
-    #[inline]
-    pub unsafe fn as_mut_slice(&mut self) -> &mut [u8] {
-        &mut self.buffer[..]
-    }
-
-    #[inline]
-    pub unsafe fn set_len(&mut self, length: usize) {
-        // An invariant of the Packed representation is that the size always equals MAX_SIZE, so
-        // theres no work to do here, other than assert the length we're trying to set is MAX_SIZE
-        debug_assert_eq!(length, MAX_SIZE);
-    }
-}
-
-crate::asserts::assert_size_eq!(PackedString, String);


Test patch:
diff --git a/compact_str/src/tests.rs b/compact_str/src/tests.rs
index 180afb7f..d98baa6e 100644
--- a/compact_str/src/tests.rs
+++ b/compact_str/src/tests.rs
@@ -6,15 +6,29 @@ use proptest::strategy::Strategy;
 use crate::CompactStr;
 
 #[cfg(target_pointer_width = "64")]
-const MAX_INLINED_SIZE: usize = 24;
+const MAX_SIZE: usize = 24;
 #[cfg(target_pointer_width = "32")]
-const MAX_INLINED_SIZE: usize = 12;
+const MAX_SIZE: usize = 12;
 
 // generates random unicode strings, upto 80 chars long
 fn rand_unicode() -> impl Strategy<Value = String> {
     proptest::collection::vec(proptest::char::any(), 0..80).prop_map(|v| v.into_iter().collect())
 }
 
+/// `proptest::Strategy` that generates `String`s with up to `len` bytes
+pub fn rand_unicode_bytes(len: usize) -> impl Strategy<Value = String> {
+    proptest::collection::vec(proptest::char::any(), 0..len).prop_map(move |chars| {
+        let mut len_utf8 = 0;
+        chars
+            .into_iter()
+            .take_while(|c| {
+                len_utf8 += c.len_utf8();
+                len_utf8 <= len
+            })
+            .collect::<String>()
+    })
+}
+
 // generates groups upto 40 strings long of random unicode strings, upto 80 chars long
 fn rand_unicode_collection() -> impl Strategy<Value = Vec<String>> {
     proptest::collection::vec(rand_unicode(), 0..40)
@@ -22,9 +36,7 @@ fn rand_unicode_collection() -> impl Strategy<Value = Vec<String>> {
 
 /// Asserts a CompactStr is allocated properly
 fn assert_allocated_properly(compact: &CompactStr) {
-    if compact.len() < MAX_INLINED_SIZE {
-        assert!(!compact.is_heap_allocated())
-    } else if compact.len() == MAX_INLINED_SIZE && compact.as_bytes()[0] <= 127 {
+    if compact.len() <= MAX_SIZE {
         assert!(!compact.is_heap_allocated())
     } else {
         assert!(compact.is_heap_allocated())
@@ -101,10 +113,10 @@ proptest! {
 
         prop_assert_eq!(compact.len(), word.len());
 
-        // The string should be heap allocated if `word` was >= MAX_INLINED_SIZE
+        // The string should be heap allocated if `word` was > MAX_SIZE
         //
         // NOTE: The reserve and write API's don't currently support the Packed representation
-        prop_assert_eq!(compact.is_heap_allocated(), word.len() >= MAX_INLINED_SIZE);
+        prop_assert_eq!(compact.is_heap_allocated(), word.len() > MAX_SIZE);
     }
 
     #[test]
@@ -158,11 +170,8 @@ fn test_short_unicode() {
     let strs = vec![
         ("🦀", false),
         ("🌧☀️", false),
-        #[cfg(target_pointer_width = "64")]
+        // str is 12 bytes long, and leading character is non-ASCII
         ("咬𓅈ꁈ:_", false),
-        // str is 12 bytes long, and leading character is non-ASCII, so it gets heap allocated
-        #[cfg(target_pointer_width = "32")]
-        ("咬𓅈ꁈ:_", true),
     ];
 
     for (s, is_heap) in strs {
@@ -199,8 +208,8 @@ fn test_medium_ascii() {
 fn test_medium_unicode() {
     let strs = vec![
         ("☕️👀😁🎉", false),
-        // str is 24 bytes long, and leading character is non-ASCII, so it gets heap allocated
-        ("🦀😀😃😄😁🦀", true),
+        // str is 24 bytes long, and leading character is non-ASCII
+        ("🦀😀😃😄😁🦀", false),
     ];
 
     #[allow(unused_variables)]
@@ -229,12 +238,13 @@ fn test_from_str_trait() {
 }
 
 #[test]
+#[cfg_attr(target_pointer_width = "32", ignore)]
 fn test_from_char_iter() {
     let s = "\u{0} 0 \u{0}a𐀀𐀀 𐀀a𐀀";
     println!("{}", s.len());
     let compact: CompactStr = s.chars().into_iter().collect();
 
-    assert!(compact.is_heap_allocated());
+    assert!(!compact.is_heap_allocated());
     assert_eq!(s, compact);
 }