Skip to content

Commit de2af0d

Browse files
committed
Expose SmolStrBuilder
1 parent 593d89f commit de2af0d

File tree

2 files changed

+110
-58
lines changed

2 files changed

+110
-58
lines changed

src/lib.rs

Lines changed: 73 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -620,15 +620,13 @@ pub trait StrExt: private::Sealed {
620620
/// potentially without allocating.
621621
///
622622
/// See [`str::replace`].
623-
// TODO: Use `Pattern` when stable.
624623
#[must_use = "this returns a new SmolStr without modifying the original"]
625624
fn replace_smolstr(&self, from: &str, to: &str) -> SmolStr;
626625

627626
/// Replaces first N matches of a &str with another &str returning a new [`SmolStr`],
628627
/// potentially without allocating.
629628
///
630629
/// See [`str::replacen`].
631-
// TODO: Use `Pattern` when stable.
632630
#[must_use = "this returns a new SmolStr without modifying the original"]
633631
fn replacen_smolstr(&self, from: &str, to: &str, count: usize) -> SmolStr;
634632
}
@@ -661,7 +659,7 @@ impl StrExt for str {
661659

662660
#[inline]
663661
fn replacen_smolstr(&self, from: &str, to: &str, count: usize) -> SmolStr {
664-
let mut result = Writer::new();
662+
let mut result = SmolStrBuilder::new();
665663
let mut last_end = 0;
666664
for (start, part) in self.match_indices(from).take(count) {
667665
// SAFETY: `start` is guaranteed to be within the bounds of `self` as per
@@ -677,6 +675,15 @@ impl StrExt for str {
677675
}
678676
}
679677

678+
impl<T> ToSmolStr for T
679+
where
680+
T: fmt::Display + ?Sized,
681+
{
682+
fn to_smolstr(&self) -> SmolStr {
683+
format_smolstr!("{}", self)
684+
}
685+
}
686+
680687
mod private {
681688
/// No downstream impls allowed.
682689
pub trait Sealed {}
@@ -689,85 +696,94 @@ mod private {
689696
#[macro_export]
690697
macro_rules! format_smolstr {
691698
($($tt:tt)*) => {{
692-
use ::core::fmt::Write;
693-
let mut w = $crate::Writer::new();
694-
w.write_fmt(format_args!($($tt)*)).expect("a formatting trait implementation returned an error");
695-
$crate::SmolStr::from(w)
699+
let mut w = $crate::SmolStrBuilder::new();
700+
::core::fmt::Write::write_fmt(&mut w, format_args!($($tt)*)).expect("a formatting trait implementation returned an error");
701+
w.finish()
696702
}};
697703
}
698704

699-
#[doc(hidden)]
700-
pub struct Writer {
701-
inline: [u8; INLINE_CAP],
702-
heap: String,
703-
len: usize,
705+
/// A builder that can be used to efficiently build a [`SmolStr`].
706+
///
707+
/// This won't allocate if the final string fits into the inline buffer.
708+
#[derive(Clone, Debug, PartialEq, Eq)]
709+
pub enum SmolStrBuilder {
710+
Inline { len: usize, buf: [u8; INLINE_CAP] },
711+
Heap(String),
712+
}
713+
714+
impl Default for SmolStrBuilder {
715+
#[inline]
716+
fn default() -> Self {
717+
Self::new()
718+
}
704719
}
705720

706-
impl Writer {
721+
impl SmolStrBuilder {
722+
/// Creates a new empty [`SmolStrBuilder`].
707723
#[must_use]
708724
pub const fn new() -> Self {
709-
Writer {
710-
inline: [0; INLINE_CAP],
711-
heap: String::new(),
725+
SmolStrBuilder::Inline {
726+
buf: [0; INLINE_CAP],
712727
len: 0,
713728
}
714729
}
715730

716-
fn push_str(&mut self, s: &str) {
717-
// if currently on the stack
718-
if self.len <= INLINE_CAP {
719-
let old_len = self.len;
720-
self.len += s.len();
721-
722-
// if the new length will fit on the stack (even if it fills it entirely)
723-
if self.len <= INLINE_CAP {
724-
self.inline[old_len..self.len].copy_from_slice(s.as_bytes());
725-
return; // skip the heap push below
731+
/// Builds a [`SmolStr`] from `self`.
732+
#[must_use]
733+
pub fn finish(&self) -> SmolStr {
734+
SmolStr(match self {
735+
&SmolStrBuilder::Inline { len, buf } => {
736+
debug_assert!(len <= INLINE_CAP);
737+
Repr::Inline {
738+
// SAFETY: We know that `value.len` is less than or equal to the maximum value of `InlineSize`
739+
len: unsafe { InlineSize::transmute_from_u8(len as u8) },
740+
buf,
741+
}
726742
}
743+
SmolStrBuilder::Heap(heap) => Repr::new(heap),
744+
})
745+
}
727746

728-
self.heap.reserve(self.len);
729-
730-
// copy existing inline bytes over to the heap
731-
// SAFETY: inline data is guaranteed to be valid utf8 for `old_len` bytes
732-
unsafe {
733-
self.heap
734-
.as_mut_vec()
735-
.extend_from_slice(&self.inline[..old_len]);
747+
/// Appends a given string slice onto the end of `self`'s buffer.
748+
pub fn push_str(&mut self, s: &str) {
749+
// if currently on the stack
750+
match self {
751+
Self::Inline { len, buf } => {
752+
let old_len = *len;
753+
*len += s.len();
754+
755+
// if the new length will fit on the stack (even if it fills it entirely)
756+
if *len <= INLINE_CAP {
757+
buf[old_len..*len].copy_from_slice(s.as_bytes());
758+
return; // skip the heap push below
759+
}
760+
761+
let mut heap = String::with_capacity(*len);
762+
763+
// copy existing inline bytes over to the heap
764+
// SAFETY: inline data is guaranteed to be valid utf8 for `old_len` bytes
765+
unsafe {
766+
heap.as_mut_vec().extend_from_slice(&buf[..old_len]);
767+
}
768+
heap.push_str(s);
769+
*self = SmolStrBuilder::Heap(heap);
736770
}
771+
SmolStrBuilder::Heap(heap) => heap.push_str(s),
737772
}
738-
739-
self.heap.push_str(s);
740773
}
741774
}
742775

743-
impl fmt::Write for Writer {
776+
impl fmt::Write for SmolStrBuilder {
744777
#[inline]
745778
fn write_str(&mut self, s: &str) -> fmt::Result {
746779
self.push_str(s);
747780
Ok(())
748781
}
749782
}
750783

751-
impl From<Writer> for SmolStr {
752-
fn from(value: Writer) -> Self {
753-
SmolStr(if value.len <= INLINE_CAP {
754-
Repr::Inline {
755-
// SAFETY: We know that `value.len` is less than or equal to the maximum value of `InlineSize`
756-
len: unsafe { InlineSize::transmute_from_u8(value.len as u8) },
757-
buf: value.inline,
758-
}
759-
} else {
760-
Repr::new(&value.heap)
761-
})
762-
}
763-
}
764-
765-
impl<T> ToSmolStr for T
766-
where
767-
T: fmt::Display + ?Sized,
768-
{
769-
fn to_smolstr(&self) -> SmolStr {
770-
format_smolstr!("{}", self)
784+
impl From<SmolStrBuilder> for SmolStr {
785+
fn from(value: SmolStrBuilder) -> Self {
786+
value.finish()
771787
}
772788
}
773789

tests/test.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::sync::Arc;
33
#[cfg(not(miri))]
44
use proptest::{prop_assert, prop_assert_eq, proptest};
55

6-
use smol_str::SmolStr;
6+
use smol_str::{SmolStr, SmolStrBuilder};
77

88
#[test]
99
#[cfg(target_pointer_width = "64")]
@@ -255,6 +255,42 @@ fn test_to_smolstr() {
255255
assert_eq!(a, smol_str::format_smolstr!("{}", a));
256256
}
257257
}
258+
#[test]
259+
fn test_builder() {
260+
//empty
261+
let builder = SmolStrBuilder::new();
262+
assert_eq!("", builder.finish());
263+
264+
// inline push
265+
let mut builder = SmolStrBuilder::new();
266+
builder.push_str("a");
267+
builder.push_str("b");
268+
let s = builder.finish();
269+
assert!(!s.is_heap_allocated());
270+
assert_eq!("ab", s);
271+
272+
// inline max push
273+
let mut builder = SmolStrBuilder::new();
274+
builder.push_str(&"a".repeat(23));
275+
let s = builder.finish();
276+
assert!(!s.is_heap_allocated());
277+
assert_eq!("a".repeat(23), s);
278+
279+
// heap push immediate
280+
let mut builder = SmolStrBuilder::new();
281+
builder.push_str(&"a".repeat(24));
282+
let s = builder.finish();
283+
assert!(s.is_heap_allocated());
284+
assert_eq!("a".repeat(24), s);
285+
286+
// heap push succession
287+
let mut builder = SmolStrBuilder::new();
288+
builder.push_str(&"a".repeat(23));
289+
builder.push_str(&"a".repeat(23));
290+
let s = builder.finish();
291+
assert!(s.is_heap_allocated());
292+
assert_eq!("a".repeat(46), s);
293+
}
258294

259295
#[cfg(test)]
260296
mod test_str_ext {

0 commit comments

Comments
 (0)