1#![allow(unsafe_code)]
2
3#[cfg(feature = "alloc")]
4use alloc::string::String;
5use core::fmt::{Debug, Display};
6use core::mem::MaybeUninit;
7use core::ops::{Deref, DerefMut};
8
9#[derive(Debug)]
15#[cfg_attr(feature = "alloc", derive(Clone))]
16pub struct NominalString(MaybeInline);
17
18impl NominalString {
19 pub const INLINE_CAPACITY: usize = MaybeInline::INLINE_CAPACITY as usize;
37
38 #[must_use]
40 pub const fn new() -> Self {
41 Self(MaybeInline::new())
42 }
43
44 #[must_use]
56 pub const fn new_reverse() -> Self {
57 Self(MaybeInline::reverse())
58 }
59
60 pub fn try_push(&mut self, ch: char) -> Result<(), OutOfMemoryError> {
67 self.0.push(ch)
68 }
69
70 pub fn try_push_str(&mut self, str: &str) -> Result<(), OutOfMemoryError> {
77 self.0.push_str(str)
78 }
79
80 pub fn try_push_front(&mut self, ch: char) -> Result<(), OutOfMemoryError> {
87 self.0.push_front(ch)
88 }
89
90 #[must_use]
92 pub fn is_inline(&self) -> bool {
93 matches!(self.0, MaybeInline::Inline(_))
94 }
95
96 #[cfg(feature = "alloc")]
103 pub fn try_into_string(self) -> Result<String, Self> {
104 match self.0 {
105 MaybeInline::Inline(inline) => Err(Self(MaybeInline::Inline(inline))),
106 MaybeInline::Heap(string) => Ok(string),
107 }
108 }
109}
110
111#[cfg(feature = "alloc")]
112impl From<NominalString> for String {
113 fn from(s: NominalString) -> Self {
114 match s.try_into_string() {
115 Ok(string) => string,
116 Err(s) => String::from(&*s),
117 }
118 }
119}
120
121impl Display for NominalString {
122 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
123 Display::fmt(&**self, f)
124 }
125}
126
127impl Default for NominalString {
128 fn default() -> Self {
129 Self::new()
130 }
131}
132
133#[cfg(feature = "alloc")]
134impl From<String> for NominalString {
135 fn from(value: String) -> Self {
136 NominalString(MaybeInline::Heap(value))
137 }
138}
139
140#[cfg(feature = "alloc")]
141impl From<&'_ String> for NominalString {
142 fn from(value: &'_ String) -> Self {
143 Self::from(value.as_str())
144 }
145}
146
147impl From<&'_ str> for NominalString {
148 fn from(value: &'_ str) -> Self {
149 match u8::try_from(value.len()) {
150 Ok(value_len) if value_len <= MaybeInline::INLINE_CAPACITY => {
151 let mut bytes = [MaybeUninit::uninit(); MaybeInline::INLINE_CAPACITY as usize];
152
153 for (dest, src) in bytes[0..value.len()].iter_mut().zip(value.as_bytes()) {
154 dest.write(*src);
155 }
156 NominalString(MaybeInline::Inline(InlineString {
157 length: value_len,
158 bytes,
159 }))
160 }
161 _ => {
162 #[cfg(feature = "alloc")]
163 {
164 Self::from(String::from(value))
165 }
166
167 #[cfg(not(feature = "alloc"))]
168 {
169 panic!("string too long to store on stack");
170 }
171 }
172 }
173 }
174}
175
176impl From<char> for NominalString {
177 fn from(ch: char) -> Self {
178 let mut s = Self::new();
179 s.try_push(ch).expect("at least one char fits inline");
180 s
181 }
182}
183
184impl Deref for NominalString {
185 type Target = str;
186
187 fn deref(&self) -> &Self::Target {
188 self.0.as_str()
189 }
190}
191
192impl DerefMut for NominalString {
193 fn deref_mut(&mut self) -> &mut Self::Target {
194 self.0.as_str_mut()
195 }
196}
197
198impl Eq for NominalString {}
199
200impl PartialEq<str> for NominalString {
201 fn eq(&self, other: &str) -> bool {
202 &**self == other
203 }
204}
205
206impl PartialEq for NominalString {
207 fn eq(&self, other: &Self) -> bool {
208 self == &**other
209 }
210}
211
212impl PartialEq<&'_ str> for NominalString {
213 fn eq(&self, other: &&'_ str) -> bool {
214 self == *other
215 }
216}
217#[cfg(feature = "alloc")]
218impl PartialEq<String> for NominalString {
219 fn eq(&self, other: &String) -> bool {
220 self == other.as_str()
221 }
222}
223
224impl PartialOrd<str> for NominalString {
225 fn partial_cmp(&self, other: &str) -> Option<core::cmp::Ordering> {
226 Some((**self).cmp(other))
227 }
228}
229
230#[cfg(feature = "alloc")]
231impl PartialOrd<String> for NominalString {
232 fn partial_cmp(&self, other: &String) -> Option<core::cmp::Ordering> {
233 self.partial_cmp(other.as_str())
234 }
235}
236
237impl Ord for NominalString {
238 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
239 (**self).cmp(&**other)
240 }
241}
242
243impl PartialOrd for NominalString {
244 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
245 Some(self.cmp(other))
246 }
247}
248
249#[derive(Clone)]
250enum MaybeInline {
251 Inline(InlineString),
252 #[cfg(feature = "alloc")]
253 Heap(String),
254}
255
256impl Debug for MaybeInline {
257 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
258 match self {
259 Self::Inline(_) => f.debug_tuple("Inline").field(&self.as_str()).finish(),
260 #[cfg(feature = "alloc")]
261 Self::Heap(_) => f.debug_tuple("Heap").field(&self.as_str()).finish(),
262 }
263 }
264}
265
266#[derive(Clone, Copy)]
267struct InlineString {
268 length: u8,
269 bytes: [MaybeUninit<u8>; MaybeInline::INLINE_CAPACITY as usize],
270}
271
272impl InlineString {
273 const fn len(&self) -> u8 {
274 self.length & 0x7F
275 }
276
277 const fn len_usize(&self) -> usize {
278 self.len() as usize
279 }
280
281 const fn is_reverse(&self) -> bool {
282 self.length & 0x80 != 0
283 }
284
285 fn data_offset(&self) -> usize {
286 if self.is_reverse() {
287 usize::from(MaybeInline::INLINE_CAPACITY) - self.len_usize()
288 } else {
289 0
290 }
291 }
292
293 fn as_bytes(&self) -> &[u8] {
294 unsafe {
297 core::slice::from_raw_parts(
298 self.bytes.as_ptr().cast::<u8>().add(self.data_offset()),
299 self.len_usize(),
300 )
301 }
302 }
303
304 fn as_bytes_mut(&mut self) -> &mut [u8] {
305 unsafe {
310 core::slice::from_raw_parts_mut(
311 self.bytes.as_mut_ptr().cast::<u8>().add(self.data_offset()),
312 self.len_usize(),
313 )
314 }
315 }
316
317 fn as_str(&self) -> &str {
318 unsafe { core::str::from_utf8_unchecked(self.as_bytes()) }
322 }
323
324 fn as_mut_str(&mut self) -> &mut str {
325 unsafe { core::str::from_utf8_unchecked_mut(self.as_bytes_mut()) }
329 }
330
331 fn set_length(&mut self, new_length: u8) {
332 self.length = self.length & 0x80 | new_length;
333 }
334
335 fn push(&mut self, ch: char, char_len: u8, inline_len: u8, new_length: u8) {
336 let mut utf8_bytes = [0; 4];
337 ch.encode_utf8(&mut utf8_bytes);
338 if self.is_reverse() {
339 let current_offset = self.data_offset();
341 let size_of_bytes = usize::from(MaybeInline::INLINE_CAPACITY);
342 self.bytes
343 .copy_within(current_offset.., size_of_bytes - usize::from(new_length));
344
345 for (dest, src) in self.bytes[size_of_bytes - usize::from(char_len)..]
346 .iter_mut()
347 .zip(&utf8_bytes[0..usize::from(char_len)])
348 {
349 dest.write(*src);
350 }
351 } else {
352 for (dest, src) in self.bytes[usize::from(inline_len)..usize::from(new_length)]
353 .iter_mut()
354 .zip(&utf8_bytes[0..usize::from(char_len)])
355 {
356 dest.write(*src);
357 }
358 }
359 self.set_length(new_length);
360 }
361
362 fn push_str(&mut self, str: &str, new_length: u8) {
363 if self.is_reverse() {
364 let current_offset = self.data_offset();
366 let size_of_bytes = usize::from(MaybeInline::INLINE_CAPACITY);
367 self.bytes
368 .copy_within(current_offset.., size_of_bytes - usize::from(new_length));
369
370 for (dest, src) in self.bytes[size_of_bytes - str.len()..]
371 .iter_mut()
372 .zip(str.as_bytes())
373 {
374 dest.write(*src);
375 }
376 } else {
377 unsafe {
382 self.bytes
383 .as_mut_ptr()
384 .cast::<u8>()
385 .add(self.len_usize())
386 .copy_from(str.as_bytes().as_ptr(), str.len());
387 };
388 }
389
390 self.set_length(new_length);
391 }
392
393 fn push_front(&mut self, ch: char, char_len: u8, inline_len: u8, new_length: u8) {
394 if self.is_reverse() {
395 let mut utf8_bytes = [0; 4];
396 ch.encode_utf8(&mut utf8_bytes);
397
398 let start = MaybeInline::INLINE_CAPACITY - new_length;
399 let end = MaybeInline::INLINE_CAPACITY - inline_len;
400 for (dest, src) in self.bytes[usize::from(start)..usize::from(end)]
401 .iter_mut()
402 .zip(&utf8_bytes[0..usize::from(char_len)])
403 {
404 dest.write(*src);
405 }
406 self.set_length(new_length);
407 } else {
408 self.bytes
409 .copy_within(0..usize::from(inline_len), usize::from(char_len));
410 self.set_length(new_length);
411
412 ch.encode_utf8(self.as_bytes_mut());
413 }
414 }
415}
416
417impl MaybeInline {
418 const INLINE_CAPACITY: u8 = 62;
419
420 const fn new() -> MaybeInline {
421 MaybeInline::Inline(InlineString {
422 length: 0,
423 bytes: [MaybeUninit::uninit(); Self::INLINE_CAPACITY as usize],
424 })
425 }
426
427 const fn reverse() -> MaybeInline {
428 MaybeInline::Inline(InlineString {
429 length: 0x80,
430 bytes: [MaybeUninit::uninit(); Self::INLINE_CAPACITY as usize],
431 })
432 }
433
434 #[allow(clippy::unnecessary_wraps)]
435 fn push(&mut self, ch: char) -> Result<(), OutOfMemoryError> {
436 match self {
437 MaybeInline::Inline(inline) => {
438 let char_len = u8::try_from(ch.len_utf8()).expect("4 < u8::MAX");
439 let inline_len = inline.len();
440 let new_length = inline_len + char_len;
441 if new_length <= Self::INLINE_CAPACITY {
442 inline.push(ch, char_len, inline_len, new_length);
443 } else {
444 #[cfg(feature = "alloc")]
445 {
446 let mut string = String::with_capacity(usize::from(new_length));
447 string.push_str(inline.as_str());
448 string.push(ch);
449 *self = MaybeInline::Heap(string);
450 }
451 #[cfg(not(feature = "alloc"))]
452 {
453 return Err(OutOfMemoryError);
454 }
455 }
456 }
457 #[cfg(feature = "alloc")]
458 MaybeInline::Heap(s) => s.push(ch),
459 }
460 Ok(())
461 }
462
463 #[allow(clippy::unnecessary_wraps)]
464 fn push_str(&mut self, str: &str) -> Result<(), OutOfMemoryError> {
465 match self {
466 MaybeInline::Inline(inline) => {
467 if let Ok(insert_len) = u8::try_from(str.len()) {
468 let new_length = inline.len().checked_add(insert_len);
469 match new_length {
470 Some(new_length) if new_length <= Self::INLINE_CAPACITY => {
471 inline.push_str(str, new_length);
472 return Ok(());
473 }
474 _ => {}
475 }
476 }
477
478 #[cfg(feature = "alloc")]
479 {
480 let new_length = inline.len_usize() + str.len();
481 let mut string = String::with_capacity(new_length);
482 string.push_str(inline.as_str());
483 string.push_str(str);
484 *self = MaybeInline::Heap(string);
485 Ok(())
486 }
487 #[cfg(not(feature = "alloc"))]
488 {
489 Err(OutOfMemoryError)
490 }
491 }
492 #[cfg(feature = "alloc")]
493 MaybeInline::Heap(s) => {
494 s.push_str(str);
495 Ok(())
496 }
497 }
498 }
499
500 #[allow(clippy::unnecessary_wraps)]
501 fn push_front(&mut self, ch: char) -> Result<(), OutOfMemoryError> {
502 match self {
503 MaybeInline::Inline(inline) => {
504 let char_len = u8::try_from(ch.len_utf8()).expect("4 < u8::MAX");
505 let inline_len = inline.len();
506 let new_length = inline_len + char_len;
507 if new_length <= Self::INLINE_CAPACITY {
508 inline.push_front(ch, char_len, inline_len, new_length);
509 } else {
510 #[cfg(feature = "alloc")]
511 {
512 let mut string = String::with_capacity(usize::from(new_length));
513 string.push(ch);
514 string.push_str(inline.as_str());
515 *self = MaybeInline::Heap(string);
516 }
517 #[cfg(not(feature = "alloc"))]
518 {
519 return Err(OutOfMemoryError);
520 }
521 }
522 }
523 #[cfg(feature = "alloc")]
524 MaybeInline::Heap(s) => s.insert(0, ch),
525 }
526 Ok(())
527 }
528
529 fn as_str(&self) -> &str {
530 match self {
531 MaybeInline::Inline(s) => s.as_str(),
532 #[cfg(feature = "alloc")]
533 MaybeInline::Heap(s) => s,
534 }
535 }
536
537 fn as_str_mut(&mut self) -> &mut str {
538 match self {
539 MaybeInline::Inline(s) => s.as_mut_str(),
540 #[cfg(feature = "alloc")]
541 MaybeInline::Heap(s) => s.as_mut_str(),
542 }
543 }
544}
545
546#[derive(Debug, Clone, Copy, Eq, PartialEq)]
548pub struct OutOfMemoryError;
549
550#[test]
551fn preconditions() {
552 assert!(MaybeInline::INLINE_CAPACITY.checked_add(4).is_some());
556}
557
558#[test]
559fn forward_ops() {
560 let mut s = NominalString::new();
561 s.try_push('a').unwrap();
562 assert_eq!(s, "a");
563 s.try_push_str("bc").unwrap();
564 assert_eq!(s, "abc");
565 s.try_push_front('_').unwrap();
566 assert_eq!(s, "_abc");
567
568 let init = "a".repeat(usize::from(MaybeInline::INLINE_CAPACITY));
569 let check_after = init.clone() + "b";
570 let check_before = String::from("b") + &init;
571
572 let borderline = NominalString::from(&init);
573 let mut s = borderline.clone();
574 assert!(s.is_inline());
575 s.try_push('b').unwrap();
576 assert!(!s.is_inline());
577 assert_eq!(s, check_after);
578
579 let borderline = NominalString::from(&init);
580 let mut s = borderline.clone();
581 assert!(s.is_inline());
582 s.try_push_str("b").unwrap();
583 assert!(!s.is_inline());
584 assert_eq!(s, check_after);
585
586 let borderline = NominalString::from(&init);
587 let mut s = borderline.clone();
588 assert!(s.is_inline());
589 s.try_push_front('b').unwrap();
590 assert!(!s.is_inline());
591 assert_eq!(s, check_before);
592}
593
594#[test]
595fn reverse_ops() {
596 let mut s = NominalString::new_reverse();
597 s.try_push('a').unwrap();
598 assert_eq!(s, "a");
599 s.try_push_str("bc").unwrap();
600 assert_eq!(s, "abc");
601 s.try_push_front('_').unwrap();
602 assert_eq!(s, "_abc");
603
604 let init = "a".repeat(usize::from(MaybeInline::INLINE_CAPACITY));
605 let check_after = init.clone() + "b";
606 let check_before = String::from("b") + &init;
607
608 let borderline = NominalString::from(&init);
609 let mut s = borderline.clone();
610 assert!(s.is_inline());
611 s.try_push('b').unwrap();
612 assert!(!s.is_inline());
613 assert_eq!(s, check_after);
614
615 let borderline = NominalString::from(&init);
616 let mut s = borderline.clone();
617 assert!(s.is_inline());
618 s.try_push_str("b").unwrap();
619 assert!(!s.is_inline());
620 assert_eq!(s, check_after);
621
622 let borderline = NominalString::from(&init);
623 let mut s = borderline.clone();
624 assert!(s.is_inline());
625 s.try_push_front('b').unwrap();
626 assert!(!s.is_inline());
627 assert_eq!(s, check_before);
628}