Repository: ParkMyCar/compact_str
Pull request #45 (2022-01-14 17:32:30)

feature: impl the Extend trait for CompactStr


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 794f6e6b..ed60cdf6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -75,8 +75,8 @@ jobs:
         run: |
           cargo miri test
 
-  fuzz:
-    name: cargo fuzz
+  fuzz-creation:
+    name: fuzz creation
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
@@ -95,7 +95,29 @@ jobs:
       - name: Fuzz!
         run: |
           cd compact_str
-          cargo fuzz run creation -- -max_total_time=120
+          cargo fuzz run --debug-assertions creation -- -max_total_time=120
+
+  fuzz-modify:
+    name: fuzz modify
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+        name: Checkout repo
+      - uses: actions-rs/toolchain@v1
+        name: Install Rust and friends
+        with:
+          profile: minimal
+          toolchain: nightly
+          override: true
+      - uses: actions-rs/install@v0.1
+        name: Install cargo-fuzz
+        with:
+          crate: cargo-fuzz
+          version: latest
+      - name: Fuzz!
+        run: |
+          cd compact_str
+          cargo fuzz run --debug-assertions modify -- -max_total_time=120
 
   example-bytes:
     name: example - bytes
diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml
index 1b0cd5cf..960d671d 100644
--- a/.github/workflows/fuzz.yml
+++ b/.github/workflows/fuzz.yml
@@ -29,6 +29,29 @@ jobs:
           command: fuzz
           args: run --debug-assertions --fuzz-dir compact_str/fuzz creation -- -max_total_time=900
 
+  modify_x86_64:
+    name: modify [x86_64]
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+        name: Checkout compact_str
+      - uses: actions-rs/toolchain@v1
+        name: Install Rust
+        with:
+          profile: minimal
+          toolchain: nightly
+          override: true
+      - uses: actions-rs/install@v0.1
+        name: Install cargo-fuzz
+        with:
+          crate: cargo-fuzz
+          version: latest
+      - uses: actions-rs/cargo@v1
+        name: Fuzz
+        with:
+          command: fuzz
+          args: run --debug-assertions --fuzz-dir compact_str/fuzz modify -- -max_total_time=900
+
   # creation_aarch64:
   #   name: creation [aarch64]
   #   runs-on: ubuntu-latest
diff --git a/.gitignore b/.gitignore
index c5c55a0d..daaeea06 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,7 @@
 # Fuzzer
 **/artifacts
 **/corpus
+**/fuzz*.log
+
+# macOS
+**/.DS_Store
diff --git a/compact_str/benches/apis.rs b/compact_str/benches/apis.rs
index 9bc5768f..99f4b4c6 100644
--- a/compact_str/benches/apis.rs
+++ b/compact_str/benches/apis.rs
@@ -60,6 +60,60 @@ fn compact_str_reserve_large(c: &mut Criterion) {
     });
 }
 
+fn compact_str_clone_small(c: &mut Criterion) {
+    let compact = CompactStr::new("i am short");
+    c.bench_function("clone small", |b| b.iter(|| compact.clone()));
+}
+
+fn compact_str_clone_large_and_modify(c: &mut Criterion) {
+    let compact = CompactStr::new("I am a very long string that will get allocated on the heap");
+    c.bench_function("clone large", |b| {
+        b.iter(|| {
+            let mut clone = compact.clone();
+            clone.push('!');
+            clone.push(' ');
+            clone.push_str("And that is quite cool~");
+        })
+    });
+}
+
+fn compact_str_extend_chars_empty(c: &mut Criterion) {
+    c.bench_function("extend chars empty", |b| {
+        b.iter(|| {
+            let mut compact =
+                CompactStr::new("I am a very long string that will get allocated on the heap");
+            compact.extend("".chars());
+        })
+    });
+}
+
+fn compact_str_extend_chars_short(c: &mut Criterion) {
+    c.bench_function("extend chars short", |b| {
+        b.iter(|| {
+            let mut compact = CompactStr::new("hello");
+            compact.extend((0..10).map(|_| '!'));
+        })
+    });
+}
+
+fn compact_str_extend_chars_inline_to_heap_20(c: &mut Criterion) {
+    c.bench_function("extend chars inline to heap, 20", |b| {
+        b.iter(|| {
+            let mut compact = CompactStr::new("hello world");
+            compact.extend((0..20).map(|_| '!'));
+        })
+    });
+}
+
+fn compact_str_extend_chars_heap_20(c: &mut Criterion) {
+    c.bench_function("extend chars heap, 20", |b| {
+        b.iter(|| {
+            let mut compact = CompactStr::new("this is a long string that will start on the heap");
+            compact.extend((0..20).map(|_| '!'));
+        })
+    });
+}
+
 fn std_string_short_length(c: &mut Criterion) {
     let word = "i am short";
     let string = String::from(word);
@@ -110,6 +164,50 @@ fn std_str_reserve_large(c: &mut Criterion) {
     });
 }
 
+fn std_str_clone_small(c: &mut Criterion) {
+    let std_str = String::from("i am short");
+    c.bench_function("std str clone small", |b| b.iter(|| std_str.clone()));
+}
+
+fn std_str_clone_large_and_modify(c: &mut Criterion) {
+    let std_str = String::from("I am a very long string that will get allocated on the heap");
+    c.bench_function("std str clone large", |b| {
+        b.iter(|| {
+            let mut clone = std_str.clone();
+            clone.push('!');
+            clone.push(' ');
+            clone.push_str("And that is quite cool~");
+        })
+    });
+}
+
+fn std_str_extend_chars_empty(c: &mut Criterion) {
+    c.bench_function("std str extend chars empty", |b| {
+        b.iter(|| {
+            let mut std_str = String::from("hello");
+            std_str.extend("".chars());
+        })
+    });
+}
+
+fn std_str_extend_chars_short(c: &mut Criterion) {
+    c.bench_function("std str extend chars short", |b| {
+        b.iter(|| {
+            let mut std_str = String::from("hello");
+            std_str.extend((0..10).map(|_| '!'));
+        })
+    });
+}
+
+fn std_str_str_extend_chars_20(c: &mut Criterion) {
+    c.bench_function("std str extend chars 20", |b| {
+        b.iter(|| {
+            let mut std_str = String::from("hello");
+            std_str.extend((0..20).map(|_| '!'));
+        })
+    });
+}
+
 criterion_group!(
     compact_str,
     compact_str_inline_length,
@@ -117,6 +215,12 @@ criterion_group!(
     compact_str_very_big_heap_length,
     compact_str_reserve_small,
     compact_str_reserve_large,
+    compact_str_clone_small,
+    compact_str_clone_large_and_modify,
+    compact_str_extend_chars_empty,
+    compact_str_extend_chars_short,
+    compact_str_extend_chars_inline_to_heap_20,
+    compact_str_extend_chars_heap_20,
 );
 criterion_group!(
     std_string,
@@ -124,7 +228,12 @@ criterion_group!(
     std_string_long_length,
     std_string_very_long_length,
     std_str_reserve_small,
-    std_str_reserve_large
+    std_str_reserve_large,
+    std_str_clone_small,
+    std_str_clone_large_and_modify,
+    std_str_extend_chars_empty,
+    std_str_extend_chars_short,
+    std_str_str_extend_chars_20,
 );
 
 criterion_main!(compact_str, std_string);
diff --git a/compact_str/benches/compact_str.rs b/compact_str/benches/compact_str.rs
index 4e381e84..e543ba73 100644
--- a/compact_str/benches/compact_str.rs
+++ b/compact_str/benches/compact_str.rs
@@ -25,5 +25,10 @@ fn heap(c: &mut Criterion) {
     c.bench_function("heap", |b| b.iter(|| CompactStr::new(word)));
 }
 
-criterion_group!(compact_str, empty, inline, packed, heap);
+fn std_string(c: &mut Criterion) {
+    let word = "I am a very long string that will get allocated on the heap";
+    c.bench_function("std_string", |b| b.iter(|| String::from(word)));
+}
+
+criterion_group!(compact_str, empty, inline, packed, heap, std_string);
 criterion_main!(compact_str);
diff --git a/compact_str/benches/comparison.rs b/compact_str/benches/comparison.rs
index 1c324c00..dad1c642 100644
--- a/compact_str/benches/comparison.rs
+++ b/compact_str/benches/comparison.rs
@@ -11,8 +11,7 @@ use smol_str::SmolStr;
 fn creation(c: &mut Criterion) {
     let mut group = c.benchmark_group("String Creation");
 
-    // generate a list of 30 strings, with each string being length i and composed of all 'a's
-    let words: Vec<String> = (0..30)
+    let words: Vec<String> = vec![0, 11, 12, 22, 23, 24, 25, 50]
         .into_iter()
         .map(|len| (0..len).into_iter().map(|_| 'a').collect())
         .collect();
@@ -43,8 +42,7 @@ criterion_group!(string_creation, creation);
 fn cloning(c: &mut Criterion) {
     let mut group = c.benchmark_group("String Cloning");
 
-    // generate a list of 30 strings, with each string being length i and composed of all 'a's
-    let words: Vec<String> = (0..30)
+    let words: Vec<String> = vec![0, 11, 12, 22, 23, 24, 25, 50]
         .into_iter()
         .map(|len| (0..len).into_iter().map(|_| 'a').collect())
         .collect();
@@ -82,8 +80,7 @@ criterion_group!(string_cloning, cloning);
 fn access(c: &mut Criterion) {
     let mut group = c.benchmark_group("String Access");
 
-    // generate a list of 30 strings, with each string being length i and composed of all 'a's
-    let words: Vec<String> = (0..30)
+    let words: Vec<String> = vec![0, 11, 12, 22, 23, 24, 25, 50]
         .into_iter()
         .map(|len| (0..len).into_iter().map(|_| 'a').collect())
         .collect();
diff --git a/compact_str/fuzz/Cargo.toml b/compact_str/fuzz/Cargo.toml
index d2f99964..80a4e74e 100644
--- a/compact_str/fuzz/Cargo.toml
+++ b/compact_str/fuzz/Cargo.toml
@@ -21,3 +21,9 @@ name = "creation"
 path = "fuzz_targets/creation.rs"
 test = false
 doc = false
+
+[[bin]]
+name = "modify"
+path = "fuzz_targets/modify.rs"
+test = false
+doc = false
diff --git a/compact_str/fuzz/fuzz_targets/modify.rs b/compact_str/fuzz/fuzz_targets/modify.rs
new file mode 100644
index 00000000..ea7006b4
--- /dev/null
+++ b/compact_str/fuzz/fuzz_targets/modify.rs
@@ -0,0 +1,78 @@
+#![no_main]
+
+use arbitrary::Arbitrary;
+use compact_str::CompactStr;
+use libfuzzer_sys::fuzz_target;
+
+#[derive(Arbitrary, Debug)]
+struct Scenario<'a> {
+    word: String,
+    actions: Vec<Modification<'a>>,
+}
+
+fuzz_target!(|scenario: Scenario| {
+    let actions = scenario.actions;
+
+    let mut word = scenario.word;
+    let mut compact = CompactStr::new(&word);
+
+    actions
+        .into_iter()
+        .for_each(|a| a.perform(&mut word, &mut compact));
+});
+
+#[derive(Arbitrary, Debug)]
+enum Modification<'a> {
+    Push(char),
+    Pop(usize),
+    PushStr(&'a str),
+    ExtendChars(Vec<char>),
+    ExtendStr(Vec<&'a str>),
+}
+
+impl Modification<'_> {
+    pub fn perform(self, control: &mut String, compact: &mut CompactStr) {
+        use Modification::*;
+
+        match self {
+            Push(c) => {
+                control.push(c);
+                compact.push(c);
+
+                assert_eq!(control, compact);
+                assert_eq!(control.len(), compact.len());
+            }
+            Pop(count) => {
+                (0..count).for_each(|_| {
+                    let a = control.pop();
+                    let b = compact.pop();
+                    assert_eq!(a, b);
+                });
+                assert_eq!(control, compact);
+                assert_eq!(control.len(), compact.len());
+                assert_eq!(control.is_empty(), compact.is_empty());
+            }
+            PushStr(s) => {
+                control.push_str(s);
+                compact.push_str(s);
+
+                assert_eq!(control, compact);
+                assert_eq!(control.len(), compact.len());
+            }
+            ExtendChars(chs) => {
+                control.extend(chs.iter());
+                compact.extend(chs.iter());
+
+                assert_eq!(control, compact);
+                assert_eq!(control.len(), compact.len());
+            }
+            ExtendStr(strs) => {
+                control.extend(strs.iter().copied());
+                compact.extend(strs.iter().copied());
+
+                assert_eq!(control, compact);
+                assert_eq!(control.len(), compact.len());
+            }
+        }
+    }
+}
diff --git a/compact_str/src/lib.rs b/compact_str/src/lib.rs
index 30967d27..e280766b 100644
--- a/compact_str/src/lib.rs
+++ b/compact_str/src/lib.rs
@@ -161,6 +161,21 @@ impl CompactStr {
         self.repr.as_mut_slice()
     }
 
+    #[inline]
+    pub fn push(&mut self, ch: char) {
+        self.repr.push(ch)
+    }
+
+    #[inline]
+    pub fn pop(&mut self) -> Option<char> {
+        self.repr.pop()
+    }
+
+    #[inline]
+    pub fn push_str(&mut self, s: &str) {
+        self.repr.push_str(s)
+    }
+
     /// # Safety
     /// * TODO: Document safety here
     #[inline]
@@ -314,4 +329,34 @@ impl FromIterator<String> for CompactStr {
     }
 }
 
+impl Extend<char> for CompactStr {
+    fn extend<T: IntoIterator<Item = char>>(&mut self, iter: T) {
+        self.repr.extend(iter)
+    }
+}
+
+impl<'a> Extend<&'a char> for CompactStr {
+    fn extend<T: IntoIterator<Item = &'a char>>(&mut self, iter: T) {
+        self.repr.extend(iter)
+    }
+}
+
+impl<'a> Extend<&'a str> for CompactStr {
+    fn extend<T: IntoIterator<Item = &'a str>>(&mut self, iter: T) {
+        self.repr.extend(iter)
+    }
+}
+
+impl Extend<Box<str>> for CompactStr {
+    fn extend<T: IntoIterator<Item = Box<str>>>(&mut self, iter: T) {
+        self.repr.extend(iter)
+    }
+}
+
+impl Extend<String> for CompactStr {
+    fn extend<T: IntoIterator<Item = String>>(&mut self, iter: T) {
+        self.repr.extend(iter)
+    }
+}
+
 static_assertions::assert_eq_size!(CompactStr, String);
diff --git a/compact_str/src/repr/arc/inner.rs b/compact_str/src/repr/arc/inner.rs
new file mode 100644
index 00000000..eef6aee6
--- /dev/null
+++ b/compact_str/src/repr/arc/inner.rs
@@ -0,0 +1,90 @@
+use core::sync::atomic::AtomicUsize;
+use core::{
+    ptr,
+    slice,
+};
+use std::alloc;
+
+const UNKNOWN: usize = 0;
+pub type StrBuffer = [u8; UNKNOWN];
+
+#[repr(C)]
+pub struct ArcStringInner {
+    pub ref_count: AtomicUsize,
+    pub capacity: usize,
+    pub str_buffer: StrBuffer,
+}
+
+impl ArcStringInner {
+    pub fn with_capacity(capacity: usize) -> ptr::NonNull<ArcStringInner> {
+        let mut ptr = Self::alloc(capacity);
+
+        // SAFETY: We just allocated an instance of `ArcStringInner` and checked to make sure it
+        // wasn't null, so we know it's aligned properly, that it points to an instance of
+        // `ArcStringInner` and that the "lifetime" is valid
+        unsafe { ptr.as_mut().ref_count = AtomicUsize::new(1) };
+        // SAFTEY: Same as above
+        unsafe { ptr.as_mut().capacity = capacity };
+
+        ptr
+    }
+
+    #[inline]
+    pub fn as_bytes(&self) -> &[u8] {
+        // SAFETY: Since we have an instance of `ArcStringInner` so we know the buffer is still
+        // valid, and we track the capacity with the creation and adjustment of the buffer
+        unsafe { slice::from_raw_parts(self.str_buffer.as_ptr(), self.capacity) }
+    }
+
+    /// Returns a mutable reference to the underlying buffer of bytes
+    ///
+    /// # Invariants
+    /// * The caller must assert that no other references, or instances of `ArcString` exist before
+    /// calling this method. Otherwise multiple threads could race writing to the underlying buffer.
+    /// * The caller must assert that the underlying buffer is still valid UTF-8
+    #[inline]
+    pub unsafe fn as_mut_bytes(&mut self) -> &mut [u8] {
+        // SAFETY: Since we have an instance of `ArcStringInner` so we know the buffer is still
+        // valid, and we track the capacity with the creation and adjustment of the buffer
+        //
+        // Note: In terms of mutability, it's up to the caller to assert the provided bytes are
+        // value UTF-8
+        slice::from_raw_parts_mut(self.str_buffer.as_mut_ptr(), self.capacity)
+    }
+
+    fn layout(capacity: usize) -> alloc::Layout {
+        let buffer_layout = alloc::Layout::array::<u8>(capacity).unwrap();
+        alloc::Layout::new::<Self>()
+            .extend(buffer_layout)
+            .unwrap()
+            .0
+            .pad_to_align()
+    }
+
+    pub fn alloc(capacity: usize) -> ptr::NonNull<ArcStringInner> {
+        let layout = Self::layout(capacity);
+        debug_assert!(layout.size() > 0);
+
+        // SAFETY: `alloc(...)` has undefined behavior if the layout is zero-sized, but we know the
+        // size of the layout is greater than 0 because we define it (and check for it above)
+        let raw_ptr = unsafe { alloc::alloc(layout) as *mut ArcStringInner };
+
+        // Check to make sure our pointer is non-null, some allocators return null pointers instead
+        // of panicking
+        match ptr::NonNull::new(raw_ptr) {
+            Some(ptr) => ptr,
+            None => alloc::handle_alloc_error(layout),
+        }
+    }
+
+    pub fn dealloc(ptr: ptr::NonNull<ArcStringInner>) {
+        // SAFETY: We know the pointer is non-null and it is properly aligned
+        let capacity = unsafe { ptr.as_ref().capacity };
+        let layout = Self::layout(capacity);
+
+        // SAFETY: There is only one way to allocate an ArcStringInner, and it uses the same layout
+        // we defined above. Also we know the pointer is non-null and we use the same global
+        // allocator as we did in `Self::alloc(...)`
+        unsafe { alloc::dealloc(ptr.as_ptr() as *mut u8, layout) };
+    }
+}
diff --git a/compact_str/src/repr/heap/arc.rs b/compact_str/src/repr/arc/mod.rs
similarity index 53%
rename from compact_str/src/repr/heap/arc.rs
rename to compact_str/src/repr/arc/mod.rs
index 949b6f8c..60b08781 100644
--- a/compact_str/src/repr/heap/arc.rs
+++ b/compact_str/src/repr/arc/mod.rs
@@ -1,16 +1,17 @@
-use std::sync::atomic::{
-    AtomicUsize,
-    Ordering,
-};
+use std::iter::Extend;
+use std::sync::atomic::Ordering;
 use std::{
-    alloc,
     fmt,
     mem,
     ptr,
-    slice,
     str,
 };
 
+mod inner;
+use inner::ArcStringInner;
+mod writer;
+use writer::ArcStringWriter;
+
 /// A soft limit on the amount of references that may be made to an `Arc`.
 ///
 /// Going above this limit will abort your program (although not
@@ -29,8 +30,15 @@ impl ArcString {
     #[inline]
     pub fn new(text: &str, additional: usize) -> Self {
         let len = text.len();
-        let capacity = len + additional;
-        let mut ptr = ArcStringInner::with_capacity(capacity);
+
+        let required = len + additional;
+        let amortized = 3 * len / 2;
+        let new_capacity = core::cmp::max(amortized, required);
+
+        // TODO: Handle overflows in the case of __very__ large Strings
+        debug_assert!(new_capacity >= len);
+
+        let mut ptr = ArcStringInner::with_capacity(new_capacity);
 
         // SAFETY: We just created the `ArcStringInner` so we know the pointer is properly aligned,
         // it is non-null, points to an instance of `ArcStringInner`, and the `str_buffer`
@@ -54,6 +62,11 @@ impl ArcString {
         self.inner().capacity
     }
 
+    #[inline]
+    pub fn push(&mut self, ch: char) {
+        self.writer().push(ch);
+    }
+
     #[inline]
     pub fn as_str(&self) -> &str {
         // SAFETY: The only way you can construct an `ArcString` is via a `&str` so it must be valid
@@ -66,33 +79,13 @@ impl ArcString {
         &self.inner().as_bytes()[..self.len]
     }
 
+    /// Returns a mutable reference to the underlying buffer of bytes
+    ///
+    /// # SAFETY:
+    /// * The caller must guarantee any modifications made to the buffer are valid UTF-8
     #[inline]
     pub unsafe fn make_mut_slice(&mut self) -> &mut [u8] {
-        if self
-            .inner()
-            .ref_count
-            .compare_exchange(1, 0, Ordering::Acquire, Ordering::Relaxed)
-            .is_err()
-        {
-            // There is more than one reference to this underlying buffer, so we need to make a new
-            // instance and decrement the count of the original by one
-
-            // Make a new instance with the same capacity as self
-            let additional = self.capacity() - self.len();
-            let new = Self::new(self.as_str(), additional);
-
-            // Assign self to our new instsance
-            *self = new;
-        } else {
-            // We were the sole reference of either kind; bump back up the strong ref count.
-            self.inner().ref_count.store(1, Ordering::Release);
-        }
-
-        // Return a mutable slice to the underlying buffer
-        //
-        // SAFETY: If we still have an instance of `ArcString` then we know the pointer to
-        // `ArcStringInner` is valid for at least as long as the provided ref to `self`
-        self.ptr.as_mut().as_mut_bytes()
+        self.writer().into_mut_slice()
     }
 
     #[inline]
@@ -100,6 +93,11 @@ impl ArcString {
         self.len = length;
     }
 
+    #[inline]
+    fn writer(&mut self) -> ArcStringWriter<'_> {
+        ArcStringWriter::new(self)
+    }
+
     /// Returns a shared reference to the heap allocated `ArcStringInner`
     #[inline]
     fn inner(&self) -> &ArcStringInner {
@@ -149,92 +147,44 @@ impl fmt::Debug for ArcString {
 }
 
 impl From<&str> for ArcString {
+    #[inline]
     fn from(text: &str) -> Self {
         ArcString::new(text, 0)
     }
 }
 
-const UNKNOWN: usize = 0;
-pub type StrBuffer = [u8; UNKNOWN];
-
-#[repr(C)]
-pub struct ArcStringInner {
-    pub ref_count: AtomicUsize,
-    capacity: usize,
-    pub str_buffer: StrBuffer,
-}
-
-impl ArcStringInner {
-    pub fn with_capacity(capacity: usize) -> ptr::NonNull<ArcStringInner> {
-        let mut ptr = Self::alloc(capacity);
-
-        // SAFETY: We just allocated an instance of `ArcStringInner` and checked to make sure it
-        // wasn't null, so we know it's aligned properly, that it points to an instance of
-        // `ArcStringInner` and that the "lifetime" is valid
-        unsafe { ptr.as_mut().ref_count = AtomicUsize::new(1) };
-        // SAFTEY: Same as above
-        unsafe { ptr.as_mut().capacity = capacity };
-
-        ptr
-    }
-
+impl Extend<char> for ArcString {
     #[inline]
-    pub fn as_bytes(&self) -> &[u8] {
-        // SAFETY: Since we have an instance of `ArcStringInner` so we know the buffer is still
-        // valid, and we track the capacity with the creation and adjustment of the buffer
-        unsafe { slice::from_raw_parts(self.str_buffer.as_ptr(), self.capacity) }
+    fn extend<T: IntoIterator<Item = char>>(&mut self, iter: T) {
+        self.writer().extend(iter);
     }
+}
 
-    /// Returns a mutable reference to the underlying buffer of bytes
-    ///
-    /// # Invariants
-    /// * The caller must assert that no other references, or instances of `ArcString` exist before
-    /// calling this method. Otherwise multiple threads could race writing to the underlying buffer.
-    /// * The caller must assert that the underlying buffer is still valid UTF-8
+impl<'c> Extend<&'c char> for ArcString {
     #[inline]
-    pub unsafe fn as_mut_bytes(&mut self) -> &mut [u8] {
-        // SAFETY: Since we have an instance of `ArcStringInner` so we know the buffer is still
-        // valid, and we track the capacity with the creation and adjustment of the buffer
-        //
-        // Note: In terms of mutability, it's up to the caller to assert the provided bytes are
-        // value UTF-8
-        slice::from_raw_parts_mut(self.str_buffer.as_mut_ptr(), self.capacity)
+    fn extend<T: IntoIterator<Item = &'c char>>(&mut self, iter: T) {
+        self.writer().extend(iter);
     }
+}
 
-    fn layout(capacity: usize) -> alloc::Layout {
-        let buffer_layout = alloc::Layout::array::<u8>(capacity).unwrap();
-        alloc::Layout::new::<Self>()
-            .extend(buffer_layout)
-            .unwrap()
-            .0
-            .pad_to_align()
+impl<'s> Extend<&'s str> for ArcString {
+    #[inline]
+    fn extend<T: IntoIterator<Item = &'s str>>(&mut self, iter: T) {
+        self.writer().extend(iter);
     }
+}
 
-    pub fn alloc(capacity: usize) -> ptr::NonNull<ArcStringInner> {
-        let layout = Self::layout(capacity);
-        debug_assert!(layout.size() > 0);
-
-        // SAFETY: `alloc(...)` has undefined behavior if the layout is zero-sized, but we know the
-        // size of the layout is greater than 0 because we define it (and check for it above)
-        let raw_ptr = unsafe { alloc::alloc(layout) as *mut ArcStringInner };
-
-        // Check to make sure our pointer is non-null, some allocators return null pointers instead
-        // of panicking
-        match ptr::NonNull::new(raw_ptr) {
-            Some(ptr) => ptr,
-            None => alloc::handle_alloc_error(layout),
-        }
+impl Extend<Box<str>> for ArcString {
+    #[inline]
+    fn extend<T: IntoIterator<Item = Box<str>>>(&mut self, iter: T) {
+        self.writer().extend(iter);
     }
+}
 
-    pub fn dealloc(ptr: ptr::NonNull<ArcStringInner>) {
-        // SAFETY: We know the pointer is non-null and it is properly aligned
-        let capacity = unsafe { ptr.as_ref().capacity };
-        let layout = Self::layout(capacity);
-
-        // SAFETY: There is only one way to allocate an ArcStringInner, and it uses the same layout
-        // we defined above. Also we know the pointer is non-null and we use the same global
-        // allocator as we did in `Self::alloc(...)`
-        unsafe { alloc::dealloc(ptr.as_ptr() as *mut u8, layout) };
+impl Extend<String> for ArcString {
+    #[inline]
+    fn extend<T: IntoIterator<Item = String>>(&mut self, iter: T) {
+        self.writer().extend(iter);
     }
 }
 
@@ -280,6 +230,29 @@ mod test {
         assert_eq!(arc_str_2.len, example.len());
     }
 
+    #[test]
+    fn test_extend_chars() {
+        let example = "hello";
+        let mut arc = ArcString::from(example);
+
+        arc.extend(" world!".chars());
+
+        assert_eq!(arc.as_str(), "hello world!");
+        assert_eq!(arc.len(), 12);
+    }
+
+    #[test]
+    fn test_extend_strs() {
+        let example = "hello";
+        let mut arc = ArcString::from(example);
+
+        let words = vec![" ", "world!", "my name is", " compact", "_str"];
+        arc.extend(words);
+
+        assert_eq!(arc.as_str(), "hello world!my name is compact_str");
+        assert_eq!(arc.len(), 34);
+    }
+
     #[test]
     fn test_sanity() {
         let example = "hello world!";
@@ -306,10 +279,3 @@ mod test {
 }
 
 static_assertions::const_assert_eq!(mem::size_of::<ArcString>(), 2 * mem::size_of::<usize>());
-// Note: Although the compiler sees `ArcStringInner` as being 16 bytes, it's technically unsized
-// because it contains a buffer of size `capacity`. We manually track the size of this buffer so
-// `ArcString` can only be two words long
-static_assertions::const_assert_eq!(
-    mem::size_of::<ArcStringInner>(),
-    2 * mem::size_of::<usize>()
-);
diff --git a/compact_str/src/repr/arc/writer.rs b/compact_str/src/repr/arc/writer.rs
new file mode 100644
index 00000000..377386a0
--- /dev/null
+++ b/compact_str/src/repr/arc/writer.rs
@@ -0,0 +1,156 @@
+use core::iter::Extend;
+use core::sync::atomic::Ordering;
+
+use super::ArcString;
+
+/// An `ArcStringWriter` provides safe mutable access to the underlying buffer of an `ArcString`.
+///
+/// To create an `ArcStringWriter`, we first check to make sure we have a unique reference to the
+/// underlying buffer of an `ArcString`, if we don't then we create one. Knowing we hold a unique
+/// reference to the `ArcString` allows us to make multiple modifications to the buffer, this is
+/// particularly beneficial for something like the `core::iter::Extend` trait
+pub struct ArcStringWriter<'a> {
+    arc_string: &'a mut ArcString,
+}
+
+impl<'a> ArcStringWriter<'a> {
+    #[inline]
+    pub fn new(arc_string: &'a mut ArcString) -> Self {
+        if arc_string
+            .inner()
+            .ref_count
+            .compare_exchange(1, 0, Ordering::Acquire, Ordering::Relaxed)
+            .is_err()
+        {
+            // There is more than one reference to this underlying buffer, so we need to make a new
+            // instance and decrement the count of the original by one
+
+            // Make a new instance with the same capacity as self
+            let additional = arc_string.capacity() - arc_string.len();
+            let new = ArcString::new(arc_string.as_str(), additional);
+
+            // Assign arc_string to our new instsance, this drops the old ArcString, which
+            // decrements its ref count
+            *arc_string = new;
+        } else {
+            // We were the sole reference of either kind; bump back up the strong ref count.
+            arc_string.inner().ref_count.store(1, Ordering::Release);
+        }
+
+        Self { arc_string }
+    }
+
+    /// Reserve space for at least `additional` bytes
+    #[inline]
+    pub fn reserve(&mut self, additional: usize) {
+        // We need at least this much space
+        let new_capacity = self.arc_string.len() + additional;
+
+        // We have enough space, so there is no work to do
+        if self.arc_string.capacity() >= new_capacity {
+            return;
+        }
+
+        // Create a new `ArcString` with enough space for at least `additional` bytes, dropping the
+        // old one
+        *self.arc_string = ArcString::new(self.arc_string.as_str(), additional);
+    }
+
+    #[inline]
+    pub fn push(&mut self, ch: char) {
+        let len = self.arc_string.len();
+        let char_len = ch.len_utf8();
+
+        // Reserve at least enough space for the new char
+        self.reserve(char_len);
+
+        // SAFETY: We're writing a char into the slice, which is valid UTF-8
+        let slice = unsafe { self.as_mut_slice() };
+
+        // Write the char into the slice
+        ch.encode_utf8(&mut slice[len..]);
+        // Increment our length
+        //
+        // SAFETY: We just wrote `char_len` bytes into the buffer, so we know this new length is
+        // valid
+        unsafe { self.arc_string.set_len(len + char_len) };
+    }
+
+    #[inline]
+    pub fn push_str(&mut self, s: &str) {
+        let len = self.arc_string.len();
+        let str_len = s.len();
+
+        // Reserve at least enough space for the new str
+        self.reserve(str_len);
+
+        // SAFETY: We're writing a &str into the slice, which is valid UTF-8
+        let slice = unsafe { self.as_mut_slice() };
+        let buffer = &mut slice[len..len + str_len];
+
+        debug_assert_eq!(buffer.len(), s.as_bytes().len());
+
+        // Copy the string into our buffer
+        buffer.copy_from_slice(s.as_bytes());
+        // Incrament the length of our string
+        unsafe { self.arc_string.set_len(len + str_len) };
+    }
+
+    /// Returns a reference to a mutable slice of bytes with lifetime `'a`
+    ///
+    /// # SAFETY
+    /// * Callers must guarantee that any modifications they make to the slice are valid UTF-8
+    #[inline]
+    pub unsafe fn as_mut_slice(&mut self) -> &mut [u8] {
+        self.arc_string.ptr.as_mut().as_mut_bytes()
+    }
+
+    /// Transforms the `ArcStringWriter<'a>` into a mutable slice of bytes with lifetime `'a`
+    ///
+    /// # SAFETY
+    /// * Callers must guarantee that any modifications they make to the slice are valid UTF-8
+    #[inline]
+    pub unsafe fn into_mut_slice(self) -> &'a mut [u8] {
+        // SAFETY: If we still have an instance of `ArcString` then we know the pointer to
+        // `ArcStringInner` is valid for at least as long as the provided ref to `self`
+        self.arc_string.ptr.as_mut().as_mut_bytes()
+    }
+}
+
+impl<'a> Extend<char> for ArcStringWriter<'a> {
+    #[inline]
+    fn extend<T: IntoIterator<Item = char>>(&mut self, iter: T) {
+        let iterator = iter.into_iter();
+        let (lower_bound, _) = iterator.size_hint();
+        self.reserve(lower_bound);
+        iterator.for_each(|c| self.push(c));
+    }
+}
+
+impl<'c, 'a> Extend<&'c char> for ArcStringWriter<'a> {
+    #[inline]
+    fn extend<T: IntoIterator<Item = &'c char>>(&mut self, iter: T) {
+        self.extend(iter.into_iter().copied());
+    }
+}
+
+impl<'s, 'a> Extend<&'s str> for ArcStringWriter<'a> {
+    #[inline]
+    fn extend<T: IntoIterator<Item = &'s str>>(&mut self, iter: T) {
+        iter.into_iter().for_each(|s| self.push_str(s));
+    }
+}
+
+impl<'a> Extend<Box<str>> for ArcStringWriter<'a> {
+    #[inline]
+    fn extend<T: IntoIterator<Item = Box<str>>>(&mut self, iter: T) {
+        iter.into_iter().for_each(move |s| self.push_str(&s));
+    }
+}
+
+impl<'a> Extend<String> for ArcStringWriter<'a> {
+    #[inline]
+    fn extend<T: IntoIterator<Item = String>>(&mut self, iter: T) {
+        iter.into_iter().for_each(move |s| self.push_str(&s));
+    }
+}
diff --git a/compact_str/src/repr/heap/mod.rs b/compact_str/src/repr/heap.rs
similarity index 89%
rename from compact_str/src/repr/heap/mod.rs
rename to compact_str/src/repr/heap.rs
index f0244b9d..09cfe496 100644
--- a/compact_str/src/repr/heap/mod.rs
+++ b/compact_str/src/repr/heap.rs
@@ -1,13 +1,11 @@
-use std::mem;
+use core::mem;
 
+use super::arc::ArcString;
 use super::{
     HEAP_MASK,
     MAX_SIZE,
 };
 
-mod arc;
-use arc::ArcString;
-
 const PADDING_SIZE: usize = MAX_SIZE - mem::size_of::<ArcString>();
 const PADDING: [u8; PADDING_SIZE] = [HEAP_MASK; PADDING_SIZE];
 
@@ -23,15 +21,17 @@ impl HeapString {
     ///
     /// If you need to allocate a `HeapString` with additional capacity, see
     /// `HeapString::with_additional(...)`
+    #[inline]
     pub fn new(text: &str) -> Self {
         let padding = PADDING;
-        let string = text.into();
+        let string = ArcString::new(text, 0);
 
         HeapString { padding, string }
     }
 
     /// Creates a `HeapString` from the provided `text` and allocates the underlying buffer with
     /// `additional` capacity
+    #[inline]
     pub fn with_additional(text: &str, additional: usize) -> Self {
         let padding = PADDING;
         let string = ArcString::new(text, additional);
@@ -56,9 +56,10 @@ impl HeapString {
 }
 
 impl From<String> for HeapString {
+    #[inline]
     fn from(s: String) -> Self {
         let padding = PADDING;
-        let string = s.as_str().into();
+        let string = ArcString::from(s.as_str());
 
         HeapString { padding, string }
     }
diff --git a/compact_str/src/repr/mod.rs b/compact_str/src/repr/mod.rs
index 6e7ef784..05a2c9e6 100644
--- a/compact_str/src/repr/mod.rs
+++ b/compact_str/src/repr/mod.rs
@@ -1,3 +1,4 @@
+use std::iter::Extend;
 use std::mem::ManuallyDrop;
 
 use static_assertions::{
@@ -10,6 +11,7 @@ mod bytes;
 
 mod iter;
 
+mod arc;
 mod discriminant;
 mod heap;
 mod inline;
@@ -126,6 +128,66 @@ impl Repr {
         self.cast_mut().into_mut_slice()
     }
 
+    #[inline]
+    pub fn push(&mut self, ch: char) {
+        let len = self.len();
+        let char_len = ch.len_utf8();
+
+        // Reserve at least enough space for our char, possibly causing a heap allocation
+        self.reserve(char_len);
+
+        // Get a mutable reference to the underlying memory buffer
+        let slice = unsafe { self.as_mut_slice() };
+
+        // Write our character into the underlying buffer
+        ch.encode_utf8(&mut slice[len..]);
+        // Incrament our length
+        unsafe { self.set_len(len + char_len) };
+    }
+
+    #[inline]
+    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()) };
+            }
+        }
+
+        Some(ch)
+    }
+
+    #[inline]
+    pub fn push_str(&mut self, s: &str) {
+        let len = self.len();
+        let str_len = s.len();
+
+        // Reserve at least enough space for our str, possibly causing a heap allocation
+        self.reserve(str_len);
+
+        let slice = unsafe { self.as_mut_slice() };
+        let buffer = &mut slice[len..len + str_len];
+
+        debug_assert_eq!(buffer.len(), s.as_bytes().len());
+
+        // Copy the string into our buffer
+        buffer.copy_from_slice(s.as_bytes());
+        // Incrament the length of our string
+        unsafe { self.set_len(len + str_len) };
+    }
+
     #[inline]
     pub unsafe fn set_len(&mut self, length: usize) {
         self.cast_mut().set_len(length)
@@ -202,6 +264,130 @@ impl Drop for Repr {
     }
 }
 
+impl Extend<char> for Repr {
+    #[inline]
+    fn extend<T: IntoIterator<Item = char>>(&mut self, iter: T) {
+        let mut iterator = iter.into_iter().peekable();
+
+        // if the iterator is empty, no work needs to be done!
+        if iterator.peek().is_none() {
+            return;
+        }
+        let (lower_bound, _) = iterator.size_hint();
+
+        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) => {
+                if lower_bound + inline.len() > MAX_SIZE {
+                    let mut heap = HeapString::with_additional(inline.as_str(), lower_bound);
+                    heap.string.extend(iterator);
+
+                    // Replace `self` with the new Repr
+                    let heap = ManuallyDrop::new(heap);
+                    *self = Repr { heap };
+                    return;
+                }
+
+                while let Some(ch) = iterator.next() {
+                    let inline_len = inline.len();
+                    let char_len = ch.len_utf8();
+
+                    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() };
+                        // Write the character into our buffer
+                        ch.encode_utf8(&mut buffer[inline_len..]);
+                        // SAFETY: We just wrote `char_len` bytes into the buffer, so we know this
+                        // is a valid length
+                        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 };
+                        }
+
+                        // All done!
+                        return;
+                    }
+                }
+            }
+        }
+    }
+}
+
+impl<'a> Extend<&'a char> for Repr {
+    fn extend<T: IntoIterator<Item = &'a char>>(&mut self, iter: T) {
+        self.extend(iter.into_iter().copied());
+    }
+}
+
+impl<'a> Extend<&'a str> for Repr {
+    fn extend<T: IntoIterator<Item = &'a str>>(&mut self, iter: T) {
+        iter.into_iter().for_each(|s| self.push_str(s));
+    }
+}
+
+impl Extend<Box<str>> for Repr {
+    fn extend<T: IntoIterator<Item = Box<str>>>(&mut self, iter: T) {
+        iter.into_iter().for_each(move |s| self.push_str(&s));
+    }
+}
+
+impl Extend<String> for Repr {
+    fn extend<T: IntoIterator<Item = String>>(&mut self, iter: T) {
+        iter.into_iter().for_each(move |s| self.push_str(&s));
+    }
+}
+
 #[derive(Debug)]
 enum StrongRepr<'a> {
     Heap(&'a ManuallyDrop<HeapString>),
@@ -372,4 +558,135 @@ mod tests {
 
         assert_eq!(repr.as_str(), long_word);
     }
+
+    #[test]
+    fn test_push() {
+        let example = "hello world";
+        let mut repr = Repr::new(example);
+        repr.push('!');
+
+        assert_eq!(repr.as_str(), "hello world!");
+        assert_eq!(repr.len(), 12);
+    }
+
+    #[test]
+    fn test_pop() {
+        let example = "hello";
+        let mut repr = Repr::new(example);
+
+        assert_eq!(repr.pop(), Some('o'));
+        assert_eq!(repr.pop(), Some('l'));
+        assert_eq!(repr.pop(), Some('l'));
+
+        assert_eq!(repr.as_str(), "he");
+        assert_eq!(repr.len(), 2);
+    }
+
+    #[test]
+    fn test_push_str() {
+        let example = "hello";
+        let mut repr = Repr::new(example);
+
+        repr.push_str(" world!");
+
+        assert_eq!(repr.as_str(), "hello world!");
+        assert_eq!(repr.len(), 12);
+    }
+
+    #[test]
+    fn test_extend_chars() {
+        let example = "hello";
+        let mut repr = Repr::new(example);
+
+        repr.extend(" world".chars());
+
+        assert_eq!(repr.len(), 11);
+        assert_eq!(repr.as_str(), "hello world");
+        assert!(!repr.is_heap_allocated());
+    }
+
+    #[test]
+    fn test_extend_chars_can_heap_allocate() {
+        let start = "hello";
+        let mut repr = Repr::new(start);
+
+        let chars = (0..100).map(|_| '!');
+        repr.extend(chars);
+
+        let mut control = String::from(start);
+        let chars2 = (0..100).map(|_| '!');
+        control.extend(chars2);
+
+        assert_eq!(repr.as_str(), control.as_str());
+        assert_eq!(repr.len(), 105);
+    }
+
+    #[test]
+    fn test_extend_chars_can_make_packed_repr() {
+        let start = "nyc";
+        let mut repr = Repr::new(start);
+
+        let num_chars = super::MAX_SIZE - start.len();
+        let chars = (0..num_chars).map(|_| '!');
+
+        repr.extend(chars);
+        assert_eq!(repr.len(), super::MAX_SIZE);
+        assert!(!repr.is_heap_allocated());
+    }
+
+    #[test]
+    fn test_extend_short_heap_variant() {
+        let start = "this is a long string that will be heap allocated";
+        let mut repr = Repr::new(start);
+        assert!(repr.is_heap_allocated());
+
+        while repr.len() > 10 {
+            assert!(repr.pop().is_some());
+        }
+        // we don't (yet?) re-inline when characters are popped
+        assert!(repr.is_heap_allocated());
+
+        // extend with one character
+        repr.extend(vec!['!']);
+        // should still be heap allocated
+        assert!(repr.is_heap_allocated());
+
+        // extend with a lot of characters
+        repr.extend((0..100).map(|_| '😊'));
+        // should still be heap allocated
+        assert!(repr.is_heap_allocated());
+    }
+
+    #[test]
+    fn test_extend_strs() {
+        let example = "hello";
+        let mut repr = Repr::new(example);
+
+        let words = vec![" ", "world!", "my name is", " compact", "_str"];
+        repr.extend(words);
+
+        assert_eq!(repr.as_str(), "hello world!my name is compact_str");
+        assert_eq!(repr.len(), 34);
+    }
+
+    #[test]
+    fn test_pop_packed() {
+        let mut repr = Repr::new("i am 24 characters long!");
+        repr.pop();
+
+        assert_eq!(repr.len(), 23);
+        assert_eq!(repr.as_str(), "i am 24 characters long");
+    }
+
+    #[test]
+    fn test_pop_does_not_reinline() {
+        let mut repr = Repr::new("i am a long string that will be heap allocated");
+        assert!(repr.is_heap_allocated());
+
+        while repr.len() > 10 {
+            repr.pop();
+        }
+        // should not have re-inlined the string
+        assert!(repr.is_heap_allocated());
+    }
 }
diff --git a/compact_str/src/repr/packed.rs b/compact_str/src/repr/packed.rs
index cff2a970..e5ddc6c8 100644
--- a/compact_str/src/repr/packed.rs
+++ b/compact_str/src/repr/packed.rs
@@ -47,6 +47,13 @@ impl PackedString {
         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


Test patch:
diff --git a/compact_str/src/tests.rs b/compact_str/src/tests.rs
index 780a67e3..a2aacda7 100644
--- a/compact_str/src/tests.rs
+++ b/compact_str/src/tests.rs
@@ -20,6 +20,17 @@ fn rand_unicode_collection() -> impl Strategy<Value = Vec<String>> {
     proptest::collection::vec(rand_unicode(), 0..40)
 }
 
+/// 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 {
+        assert!(!compact.is_heap_allocated())
+    } else {
+        assert!(compact.is_heap_allocated())
+    }
+}
+
 proptest! {
     #[test]
     #[cfg_attr(miri, ignore)]
@@ -33,14 +44,7 @@ proptest! {
     #[cfg_attr(miri, ignore)]
     fn test_strings_allocated_properly(word in rand_unicode()) {
         let compact = CompactStr::new(&word);
-
-        if compact.len() < MAX_INLINED_SIZE {
-            prop_assert!(!compact.is_heap_allocated())
-        } else if compact.len() == MAX_INLINED_SIZE && compact.as_bytes()[0] <= 127 {
-            prop_assert!(!compact.is_heap_allocated())
-        } else {
-            prop_assert!(compact.is_heap_allocated())
-        }
+        assert_allocated_properly(&compact);
     }
 
     #[test]
@@ -102,6 +106,19 @@ proptest! {
         // 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);
     }
+
+    #[test]
+    #[cfg_attr(miri, ignore)]
+    fn test_extend_chars_allocated_properly((start, extend) in (rand_unicode(), rand_unicode())) {
+        let mut compact = CompactStr::new(&start);
+        compact.extend(extend.chars());
+
+        let mut control = start.clone();
+        control.extend(extend.chars());
+
+        prop_assert_eq!(&compact, &control);
+        assert_allocated_properly(&compact);
+    }
 }
 
 #[test]
@@ -221,6 +238,21 @@ fn test_from_char_iter() {
     assert_eq!(s, compact);
 }
 
+#[test]
+#[cfg_attr(target_pointer_width = "32", ignore)]
+fn test_extend_packed_from_empty() {
+    let s = "  0\u{80}A\u{0}𐀀 𐀀¡a𐀀0";
+
+    let mut compact = CompactStr::new(s);
+    assert!(!compact.is_heap_allocated());
+
+    // extend from an empty iterator
+    compact.extend("".chars());
+
+    // we should still be heap allocated
+    assert!(!compact.is_heap_allocated());
+}
+
 #[test]
 fn test_compact_str_is_send_and_sync() {
     fn is_send_and_sync<T: Send + Sync>() {}