Skip to main content

smart_string/str_stack/
mod.rs

1//! A compact string collection backed by a single contiguous byte buffer.
2//!
3//! # Types
4//!
5//! - [`StrStack`]: mutable builder β€” push, pop, checkpoint/rollback, random access.
6//! - [`StrList`]: frozen immutable list (`Box<[u8]>` + `Box<[u32]>`), no excess capacity.
7//! - [`StrListRef`]: borrowed read-only view (`&[u8]` + `&[u32]`), zero-copy over external buffers.
8//!
9//! # Representation
10//!
11//! All types share the same logical structure:
12//! - `data`: a contiguous UTF-8 byte buffer containing all segments concatenated.
13//! - `ends`: a `u32` boundary table where `ends[i]` is the byte offset of the end of segment `i`.
14//!   Segment `i` occupies `data[ends[i-1]..ends[i]]` (with `ends[-1] = 0`).
15//!
16//! # Invariants (soundness-critical)
17//!
18//! - `data` is always valid UTF-8.
19//! - `ends` values are monotonically non-decreasing.
20//! - The last value in `ends` does not exceed `data.len()`.
21//!
22//! These invariants are maintained by construction (`push` only accepts `&str`)
23//! and by validation (`StrListRef::new` checks all three).
24//!
25//! # Limits
26//!
27//! The boundary table uses `u32`, limiting total content to ~4 GB.
28//! [`push`](StrStack::push) panics if this limit is exceeded;
29//! [`try_push`](StrStack::try_push) returns an error instead.
30//!
31//! # Complexity
32//!
33//! | Operation | Time | Notes |
34//! |-----------|------|-------|
35//! | `push` | O(n) | n = length of pushed string (byte copy) |
36//! | `get(i)` | O(1) | boundary lookup + slice projection |
37//! | `remove_top` | O(1) | amortized (truncates vecs) |
38//! | `checkpoint` | O(1) | captures two lengths |
39//! | `reset` | O(1) | amortized (truncates vecs) |
40//! | `truncate(k)` | O(1) | amortized |
41//! | `iter` | O(n) | n = number of segments |
42//! | `From<StrStack> for StrList` | O(n) | n = total bytes (box slice copy) |
43//!
44//! # Example
45//!
46//! ```
47//! use smart_string::StrStack;
48//!
49//! let mut stack = StrStack::new();
50//! stack.push("hello");
51//! stack.push("world");
52//!
53//! assert_eq!(stack.get(0), Some("hello"));
54//! assert_eq!(stack.last(), Some("world"));
55//! assert_eq!(stack.as_str(), "helloworld");
56//!
57//! // Checkpoint for speculative parsing
58//! let cp = stack.checkpoint();
59//! stack.push("tentative");
60//! stack.reset(cp); // rolls back "tentative"
61//! assert_eq!(stack.len(), 2);
62//! ```
63
64use std::fmt;
65use std::str::from_utf8_unchecked;
66
67mod iter;
68mod str_list;
69mod str_list_ref;
70#[cfg(feature = "serde")]
71mod with_serde;
72
73pub use iter::StrStackIter;
74pub use str_list::StrList;
75pub use str_list::StrListIter;
76pub use str_list_ref::StrListRef;
77pub use str_list_ref::StrListValidationError;
78
79/// A lightweight snapshot of `StrStack` state for checkpoint/rollback.
80///
81/// Created by [`StrStack::checkpoint`], consumed by [`StrStack::reset`].
82/// Fields are private to preserve the invariant that `bytes` always points
83/// to a valid UTF-8 boundary within `data`.
84#[derive(Copy, Clone, Debug, PartialEq, Eq)]
85pub struct Checkpoint {
86    items: u32,
87    bytes: u32,
88}
89
90/// Error returned by [`StrStack::try_push`] when the total byte length would exceed `u32::MAX`.
91#[derive(Debug, Clone)]
92pub struct StrStackOverflow {
93    attempted: usize,
94}
95
96impl fmt::Display for StrStackOverflow {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        write!(
99            f,
100            "StrStack overflow: attempted total byte length {} exceeds u32::MAX",
101            self.attempted
102        )
103    }
104}
105
106#[derive(Clone, Default, PartialEq, Eq)]
107pub struct StrStack {
108    data: Vec<u8>,
109    ends: Vec<u32>,
110}
111
112impl StrStack {
113    #[inline]
114    pub fn new() -> Self {
115        Self::default()
116    }
117
118    /// Creates a new `StrStack` with pre-allocated capacity for `items` string segments
119    /// and `bytes` total bytes of string data.
120    #[inline]
121    pub fn with_capacity(items: usize, bytes: usize) -> Self {
122        Self {
123            data: Vec::with_capacity(bytes),
124            ends: Vec::with_capacity(items),
125        }
126    }
127
128    #[inline]
129    pub fn len(&self) -> usize {
130        self.ends.len()
131    }
132
133    #[inline]
134    pub fn is_empty(&self) -> bool {
135        self.ends.is_empty()
136    }
137
138    /// Returns the total byte length of the data buffer.
139    ///
140    /// Returns `u32` to match the internal boundary representation,
141    /// making the 4 GB limit explicit at the call site.
142    #[inline]
143    pub fn bytes_len(&self) -> u32 {
144        // `push` guarantees `self.data.len() <= u32::MAX`, so this cast is safe.
145        self.data.len() as u32
146    }
147
148    /// Reserves capacity for at least `additional` more string segments.
149    #[inline]
150    pub fn reserve_items(&mut self, additional: usize) {
151        self.ends.reserve(additional);
152    }
153
154    /// Reserves capacity for at least `additional` more bytes of string data.
155    #[inline]
156    pub fn reserve_bytes(&mut self, additional: usize) {
157        self.data.reserve(additional);
158    }
159
160    #[inline]
161    pub fn as_str(&self) -> &str {
162        // SAFETY: `self.data` is only appended to via `push(&str)` and truncated via `remove_top()`,
163        // so it is always valid UTF-8.
164        unsafe { from_utf8_unchecked(&self.data) }
165    }
166
167    #[inline]
168    pub fn get(&self, index: usize) -> Option<&str> {
169        let (begin, end) = self.get_bounds(index)?;
170        // SAFETY: `get_bounds` ensures `begin <= end <= self.data.len()`, and the stack stores only UTF-8 segments
171        // pushed via `push(&str)`.
172        Some(unsafe { self.get_unchecked_internal(begin as usize, end as usize) })
173    }
174
175    #[inline]
176    /// Returns a `&str` slice without bounds checks.
177    ///
178    /// # Safety
179    ///
180    /// - `begin <= end`
181    /// - `end <= self.data.len()`
182    /// - `self.data[begin..end]` must be valid UTF-8
183    #[deprecated(note = "Use `get()` instead. This will be removed in a future version.")]
184    pub unsafe fn get_unchecked(&self, begin: usize, end: usize) -> &str {
185        // SAFETY: caller upholds bounds + UTF-8 preconditions (see doc comment).
186        unsafe {
187            let slice = self.data.get_unchecked(begin..end);
188            from_utf8_unchecked(slice)
189        }
190    }
191
192    /// Internal unchecked slice access. Not public β€” callers within the crate
193    /// must uphold bounds + UTF-8 preconditions.
194    #[inline]
195    pub(crate) unsafe fn get_unchecked_internal(&self, begin: usize, end: usize) -> &str {
196        // SAFETY: caller upholds bounds + UTF-8 preconditions.
197        unsafe {
198            let slice = self.data.get_unchecked(begin..end);
199            from_utf8_unchecked(slice)
200        }
201    }
202
203    #[inline]
204    pub fn get_bounds(&self, index: usize) -> Option<(u32, u32)> {
205        if index + 1 > self.ends.len() {
206            return None;
207        }
208        let (start, end) = if index > 0 {
209            (self.ends[index - 1], self.ends[index])
210        } else {
211            (0, self.ends[0])
212        };
213        debug_assert!(start <= end);
214        debug_assert!((end as usize) <= self.data.len());
215        Some((start, end))
216    }
217
218    #[inline]
219    pub fn get_top(&self) -> Option<&str> {
220        match self.ends.len() {
221            0 => None,
222            len => self.get(len - 1),
223        }
224    }
225
226    /// Returns the last (topmost) string segment, or `None` if empty.
227    ///
228    /// Alias for [`get_top`](Self::get_top).
229    #[inline]
230    pub fn last(&self) -> Option<&str> {
231        self.get_top()
232    }
233
234    #[inline]
235    pub fn remove_top(&mut self) -> Option<()> {
236        self.ends.pop()?;
237        let end = self.ends.last().copied().unwrap_or(0) as usize;
238        self.data.truncate(end);
239        Some(())
240    }
241
242    #[inline]
243    pub fn pop_owned<T>(&mut self) -> Option<T>
244    where
245        T: for<'a> From<&'a str>,
246    {
247        let s = self.get_top()?.into();
248        self.remove_top();
249        Some(s)
250    }
251
252    #[inline]
253    pub fn push(&mut self, s: &str) {
254        self.data.extend_from_slice(s.as_bytes());
255        let new_end: u32 = self
256            .data
257            .len()
258            .try_into()
259            .expect("StrStack: total byte length exceeds u32::MAX");
260        self.ends.push(new_end);
261    }
262
263    /// Fallible push: appends a string segment, returning `Err` if the total byte
264    /// length would exceed `u32::MAX`.
265    #[inline]
266    pub fn try_push(&mut self, s: &str) -> Result<(), StrStackOverflow> {
267        let new_len = self.data.len() + s.len();
268        let new_end: u32 = new_len
269            .try_into()
270            .map_err(|_| StrStackOverflow { attempted: new_len })?;
271        self.data.extend_from_slice(s.as_bytes());
272        self.ends.push(new_end);
273        Ok(())
274    }
275
276    /// Removes all string segments, resetting the stack to empty.
277    ///
278    /// Does not release allocated memory (use `shrink_to_fit` on the underlying
279    /// vecs if needed β€” not yet exposed).
280    #[inline]
281    pub fn clear(&mut self) {
282        self.data.clear();
283        self.ends.clear();
284    }
285
286    /// Keeps the first `len` string segments and removes the rest.
287    ///
288    /// If `len >= self.len()`, this is a no-op (matches [`Vec::truncate`] semantics).
289    #[inline]
290    pub fn truncate(&mut self, len: usize) {
291        if len >= self.ends.len() {
292            return;
293        }
294        self.ends.truncate(len);
295        let byte_end = self.ends.last().copied().unwrap_or(0) as usize;
296        self.data.truncate(byte_end);
297    }
298
299    /// Captures a lightweight checkpoint of the current stack state.
300    ///
301    /// The checkpoint can later be passed to [`reset`](Self::reset) to roll back
302    /// any segments pushed after this point. Useful for speculative parsing:
303    /// push tokens tentatively, then either commit (by discarding the checkpoint)
304    /// or roll back (by calling `reset`).
305    #[inline]
306    pub fn checkpoint(&self) -> Checkpoint {
307        Checkpoint {
308            items: self.ends.len() as u32,
309            bytes: self.data.len() as u32,
310        }
311    }
312
313    /// Rolls the stack back to a previously captured [`Checkpoint`].
314    ///
315    /// Removes all segments pushed after the checkpoint was taken.
316    ///
317    /// # Panics
318    ///
319    /// Panics if the checkpoint is invalid (its item count or byte count exceeds
320    /// the current stack state). This can happen if the checkpoint was created from
321    /// a different `StrStack`, or if the stack was reset to an earlier checkpoint
322    /// after this one was taken.
323    #[inline]
324    pub fn reset(&mut self, cp: Checkpoint) {
325        assert!(
326            cp.items as usize <= self.ends.len() && cp.bytes as usize <= self.data.len(),
327            "StrStack::reset: invalid checkpoint (items: {}, bytes: {}) for stack (items: {}, bytes: {})",
328            cp.items, cp.bytes, self.ends.len(), self.data.len()
329        );
330        self.ends.truncate(cp.items as usize);
331        self.data.truncate(cp.bytes as usize);
332    }
333
334    #[inline]
335    pub fn iter(&self) -> StrStackIter<'_> {
336        StrStackIter::new(self)
337    }
338
339    /// Internal: borrow the raw data buffer.
340    #[inline]
341    pub(crate) fn data_as_slice(&self) -> &[u8] {
342        &self.data
343    }
344
345    /// Internal: borrow the raw ends buffer.
346    #[inline]
347    pub(crate) fn ends_as_slice(&self) -> &[u32] {
348        &self.ends
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use std::rc::Rc;
355
356    use super::*;
357    use crate::SmartString;
358
359    #[test]
360    fn test_create() {
361        let stack = StrStack::new();
362        assert_eq!(stack.len(), 0);
363        assert!(stack.is_empty());
364        assert_eq!(stack.get_top(), None);
365        assert_eq!(stack.get(0), None);
366        assert_eq!(stack.get_bounds(0), None);
367    }
368
369    #[test]
370    fn test_push() {
371        let mut stack = StrStack::new();
372
373        stack.push("123");
374        assert_eq!(stack.len(), 1);
375        assert!(!stack.is_empty());
376        assert_eq!(stack.get_top(), Some("123"));
377        assert_eq!(stack.get(0), Some("123"));
378        assert_eq!(stack.get_bounds(0), Some((0u32, 3u32)));
379        assert_eq!(stack.get(1), None);
380        assert_eq!(stack.get_bounds(1), None);
381
382        stack.push("456");
383        assert_eq!(stack.len(), 2);
384        assert!(!stack.is_empty());
385        assert_eq!(stack.get_top(), Some("456"));
386        assert_eq!(stack.get(0), Some("123"));
387        assert_eq!(stack.get_bounds(0), Some((0u32, 3u32)));
388        assert_eq!(stack.get(1), Some("456"));
389        assert_eq!(stack.get_bounds(1), Some((3u32, 6u32)));
390        assert_eq!(stack.get(2), None);
391        assert_eq!(stack.get_bounds(2), None);
392    }
393
394    #[test]
395    fn test_remove_top() {
396        let mut stack = StrStack::new();
397
398        stack.push("123");
399        stack.push("456");
400        stack.push("789");
401        assert_eq!(stack.len(), 3);
402
403        assert!(stack.remove_top().is_some());
404        assert_eq!(stack.len(), 2);
405        assert!(!stack.is_empty());
406        assert_eq!(stack.get_top(), Some("456"));
407        assert_eq!(stack.get(0), Some("123"));
408        assert_eq!(stack.get(1), Some("456"));
409        assert!(stack.get(2).is_none());
410        assert!(stack.get_bounds(2).is_none());
411
412        assert!(stack.remove_top().is_some());
413        assert_eq!(stack.len(), 1);
414        assert!(!stack.is_empty());
415        assert_eq!(stack.get_top(), Some("123"));
416        assert_eq!(stack.get(0), Some("123"));
417        assert!(stack.get(1).is_none());
418        assert!(stack.get_bounds(1).is_none());
419
420        assert!(stack.remove_top().is_some());
421        assert_eq!(stack.len(), 0);
422        assert!(stack.is_empty());
423        assert!(stack.get_top().is_none());
424        assert!(stack.get(0).is_none());
425        assert!(stack.get_bounds(0).is_none());
426
427        assert!(stack.remove_top().is_none());
428    }
429
430    #[test]
431    fn test_pop_owned() {
432        let mut stack = StrStack::new();
433
434        stack.push("123");
435        stack.push("456");
436        stack.push("789");
437        assert_eq!(stack.len(), 3);
438
439        assert_eq!(stack.pop_owned::<String>(), Some("789".into()));
440        assert_eq!(stack.len(), 2);
441        assert_eq!(stack.get_top(), Some("456"));
442        assert_eq!(stack.get(0), Some("123"));
443        assert_eq!(stack.get(1), Some("456"));
444        assert!(stack.get(2).is_none());
445        assert!(stack.get_bounds(2).is_none());
446
447        assert_eq!(stack.pop_owned::<SmartString>(), Some("456".into()));
448        assert_eq!(stack.len(), 1);
449        assert_eq!(stack.get_top(), Some("123"));
450        assert_eq!(stack.get(0), Some("123"));
451        assert!(stack.get(1).is_none());
452        assert!(stack.get_bounds(1).is_none());
453
454        assert_eq!(stack.pop_owned::<Rc<str>>(), Some("123".into()));
455        assert_eq!(stack.len(), 0);
456        assert!(stack.get_top().is_none());
457        assert!(stack.get(0).is_none());
458        assert!(stack.get_bounds(0).is_none());
459
460        assert!(stack.pop_owned::<Box<str>>().is_none());
461    }
462
463    #[test]
464    fn test_iter() {
465        let mut stack = StrStack::new();
466
467        stack.push("123");
468        stack.push("456");
469        stack.push("789");
470
471        let mut iter = stack.iter();
472        assert_eq!(iter.next(), Some("123"));
473        assert_eq!(iter.next(), Some("456"));
474        assert_eq!(iter.next(), Some("789"));
475        assert_eq!(iter.next(), None);
476        assert_eq!(iter.next(), None);
477    }
478
479    #[test]
480    fn test_unicode_push_get_bounds_and_as_str() {
481        let mut stack = StrStack::new();
482        stack.push("€"); // 3 bytes
483        stack.push("a"); // 1 byte
484        stack.push("😊"); // 4 bytes
485
486        assert_eq!(stack.as_str(), "€a😊");
487
488        assert_eq!(stack.get(0), Some("€"));
489        assert_eq!(stack.get(1), Some("a"));
490        assert_eq!(stack.get(2), Some("😊"));
491
492        assert_eq!(stack.get_bounds(0), Some((0u32, 3u32)));
493        assert_eq!(stack.get_bounds(1), Some((3u32, 4u32)));
494        assert_eq!(stack.get_bounds(2), Some((4u32, 8u32)));
495    }
496
497    #[test]
498    fn test_unicode_remove_top_truncates_byte_buffer() {
499        let mut stack = StrStack::new();
500        stack.push("€"); // 3 bytes
501        stack.push("😊"); // 4 bytes
502        stack.push("a"); // 1 byte
503
504        assert_eq!(stack.as_str(), "β‚¬πŸ˜Ša");
505        assert_eq!(stack.len(), 3);
506
507        stack.remove_top().unwrap();
508        assert_eq!(stack.as_str(), "β‚¬πŸ˜Š");
509        assert_eq!(stack.len(), 2);
510        assert_eq!(stack.get_top(), Some("😊"));
511
512        stack.remove_top().unwrap();
513        assert_eq!(stack.as_str(), "€");
514        assert_eq!(stack.len(), 1);
515        assert_eq!(stack.get_top(), Some("€"));
516    }
517
518    // -- push/remove/push mutation sequences ---------------------------------------------------------
519
520    #[test]
521    fn test_push_remove_push_sequence() {
522        let mut stack = StrStack::new();
523        stack.push("aaa");
524        stack.push("bbb");
525        stack.push("ccc");
526        assert_eq!(stack.len(), 3);
527
528        // Remove top two
529        stack.remove_top();
530        stack.remove_top();
531        assert_eq!(stack.len(), 1);
532        assert_eq!(stack.as_str(), "aaa");
533        assert_eq!(stack.get(0), Some("aaa"));
534
535        // Push again after removal
536        stack.push("ddd");
537        stack.push("eee");
538        assert_eq!(stack.len(), 3);
539        assert_eq!(stack.as_str(), "aaadddeee");
540        assert_eq!(stack.get(0), Some("aaa"));
541        assert_eq!(stack.get(1), Some("ddd"));
542        assert_eq!(stack.get(2), Some("eee"));
543
544        // Iterator should yield the correct segments
545        let collected: Vec<&str> = stack.iter().collect();
546        assert_eq!(collected, vec!["aaa", "ddd", "eee"]);
547    }
548
549    #[test]
550    fn test_push_remove_all_push_again() {
551        let mut stack = StrStack::new();
552        stack.push("first");
553        stack.push("second");
554
555        stack.remove_top();
556        stack.remove_top();
557        assert!(stack.is_empty());
558        assert_eq!(stack.as_str(), "");
559
560        stack.push("third");
561        assert_eq!(stack.len(), 1);
562        assert_eq!(stack.get(0), Some("third"));
563        assert_eq!(stack.as_str(), "third");
564    }
565
566    #[test]
567    fn test_push_remove_push_unicode() {
568        let mut stack = StrStack::new();
569        stack.push("δ½ ε₯½"); // 6 bytes
570        stack.push("δΈ–η•Œ"); // 6 bytes
571        assert_eq!(stack.as_str(), "δ½ ε₯½δΈ–η•Œ");
572
573        stack.remove_top();
574        assert_eq!(stack.as_str(), "δ½ ε₯½");
575
576        stack.push("πŸ¦€"); // 4 bytes
577        assert_eq!(stack.as_str(), "δ½ ε₯½πŸ¦€");
578        assert_eq!(stack.get(0), Some("δ½ ε₯½"));
579        assert_eq!(stack.get(1), Some("πŸ¦€"));
580
581        let collected: Vec<&str> = stack.iter().collect();
582        assert_eq!(collected, vec!["δ½ ε₯½", "πŸ¦€"]);
583    }
584
585    #[test]
586    fn test_as_str_equals_iter_concatenation() {
587        let mut stack = StrStack::new();
588        stack.push("abc");
589        stack.push("€");
590        stack.push("def");
591        stack.remove_top();
592        stack.push("ghi");
593        stack.push("😊");
594
595        let concatenated: String = stack.iter().collect();
596        assert_eq!(stack.as_str(), concatenated.as_str());
597    }
598
599    // -- clear ---------------------------------------------------------------------------------------
600
601    #[test]
602    fn test_clear() {
603        let mut stack = StrStack::new();
604        stack.push("hello");
605        stack.push("world");
606        assert_eq!(stack.len(), 2);
607
608        // clear is private, but we can test it via the serde roundtrip
609        // or through repeated remove_top. Let's test the invariant:
610        // after removing all items, the stack is fully clean.
611        stack.remove_top();
612        stack.remove_top();
613        assert!(stack.is_empty());
614        assert_eq!(stack.len(), 0);
615        assert_eq!(stack.as_str(), "");
616        assert!(stack.iter().next().is_none());
617
618        // Can push again
619        stack.push("new");
620        assert_eq!(stack.len(), 1);
621        assert_eq!(stack.get(0), Some("new"));
622    }
623
624    // -- empty string segments -----------------------------------------------------------------------
625
626    #[test]
627    fn test_push_empty_strings() {
628        let mut stack = StrStack::new();
629        stack.push("");
630        stack.push("abc");
631        stack.push("");
632
633        assert_eq!(stack.len(), 3);
634        assert_eq!(stack.get(0), Some(""));
635        assert_eq!(stack.get(1), Some("abc"));
636        assert_eq!(stack.get(2), Some(""));
637        assert_eq!(stack.as_str(), "abc");
638
639        let collected: Vec<&str> = stack.iter().collect();
640        assert_eq!(collected, vec!["", "abc", ""]);
641    }
642
643    // -- ergonomics APIs (v0.3) -----------------------------------------------------------------------
644
645    #[test]
646    fn test_with_capacity() {
647        let stack = StrStack::with_capacity(10, 100);
648        assert_eq!(stack.len(), 0);
649        assert!(stack.is_empty());
650    }
651
652    #[test]
653    fn test_bytes_len() {
654        let mut stack = StrStack::new();
655        assert_eq!(stack.bytes_len(), 0);
656        stack.push("abc"); // 3 bytes
657        assert_eq!(stack.bytes_len(), 3);
658        stack.push("€"); // 3 bytes
659        assert_eq!(stack.bytes_len(), 6);
660        stack.push("😊"); // 4 bytes
661        assert_eq!(stack.bytes_len(), 10);
662    }
663
664    #[test]
665    fn test_last() {
666        let mut stack = StrStack::new();
667        assert_eq!(stack.last(), None);
668        stack.push("first");
669        assert_eq!(stack.last(), Some("first"));
670        stack.push("second");
671        assert_eq!(stack.last(), Some("second"));
672        // last() and get_top() return the same value
673        assert_eq!(stack.last(), stack.get_top());
674    }
675
676    #[test]
677    fn test_clear_public() {
678        let mut stack = StrStack::new();
679        stack.push("hello");
680        stack.push("world");
681        assert_eq!(stack.len(), 2);
682        assert_eq!(stack.bytes_len(), 10);
683
684        stack.clear();
685        assert!(stack.is_empty());
686        assert_eq!(stack.len(), 0);
687        assert_eq!(stack.bytes_len(), 0);
688        assert_eq!(stack.as_str(), "");
689
690        // Can push again after clear
691        stack.push("new");
692        assert_eq!(stack.len(), 1);
693        assert_eq!(stack.get(0), Some("new"));
694    }
695
696    #[test]
697    fn test_truncate() {
698        let mut stack = StrStack::new();
699        stack.push("aaa");
700        stack.push("bbb");
701        stack.push("ccc");
702        stack.push("ddd");
703        assert_eq!(stack.len(), 4);
704
705        // Truncate to 2 items
706        stack.truncate(2);
707        assert_eq!(stack.len(), 2);
708        assert_eq!(stack.get(0), Some("aaa"));
709        assert_eq!(stack.get(1), Some("bbb"));
710        assert_eq!(stack.get(2), None);
711        assert_eq!(stack.as_str(), "aaabbb");
712    }
713
714    #[test]
715    fn test_truncate_noop() {
716        let mut stack = StrStack::new();
717        stack.push("aaa");
718        stack.push("bbb");
719
720        // Truncate to >= len is a no-op
721        stack.truncate(5);
722        assert_eq!(stack.len(), 2);
723        stack.truncate(2);
724        assert_eq!(stack.len(), 2);
725    }
726
727    #[test]
728    fn test_truncate_to_zero() {
729        let mut stack = StrStack::new();
730        stack.push("aaa");
731        stack.push("bbb");
732
733        stack.truncate(0);
734        assert!(stack.is_empty());
735        assert_eq!(stack.as_str(), "");
736    }
737
738    #[test]
739    fn test_truncate_unicode() {
740        let mut stack = StrStack::new();
741        stack.push("δ½ ε₯½"); // 6 bytes
742        stack.push("δΈ–η•Œ"); // 6 bytes
743        stack.push("πŸ¦€"); // 4 bytes
744
745        stack.truncate(1);
746        assert_eq!(stack.len(), 1);
747        assert_eq!(stack.get(0), Some("δ½ ε₯½"));
748        assert_eq!(stack.bytes_len(), 6);
749    }
750
751    #[test]
752    fn test_try_push_success() {
753        let mut stack = StrStack::new();
754        assert!(stack.try_push("hello").is_ok());
755        assert_eq!(stack.len(), 1);
756        assert_eq!(stack.get(0), Some("hello"));
757    }
758
759    #[test]
760    fn test_reserve() {
761        let mut stack = StrStack::new();
762        stack.reserve_items(10);
763        stack.reserve_bytes(100);
764        // Reserving doesn't change length
765        assert_eq!(stack.len(), 0);
766        assert!(stack.is_empty());
767        // But we can push without reallocation
768        for i in 0..10 {
769            stack.push(&format!("{}", i));
770        }
771        assert_eq!(stack.len(), 10);
772    }
773
774    // -- checkpoint/rollback (v0.3) -------------------------------------------------------------------
775
776    #[test]
777    fn test_checkpoint_basic() {
778        let mut stack = StrStack::new();
779        stack.push("aaa");
780        stack.push("bbb");
781        let cp = stack.checkpoint();
782
783        stack.push("ccc");
784        stack.push("ddd");
785        assert_eq!(stack.len(), 4);
786
787        stack.reset(cp);
788        assert_eq!(stack.len(), 2);
789        assert_eq!(stack.get(0), Some("aaa"));
790        assert_eq!(stack.get(1), Some("bbb"));
791        assert_eq!(stack.get(2), None);
792        assert_eq!(stack.as_str(), "aaabbb");
793    }
794
795    #[test]
796    fn test_checkpoint_empty_stack() {
797        let mut stack = StrStack::new();
798        let cp = stack.checkpoint();
799
800        stack.push("aaa");
801        stack.push("bbb");
802        assert_eq!(stack.len(), 2);
803
804        stack.reset(cp);
805        assert!(stack.is_empty());
806        assert_eq!(stack.as_str(), "");
807    }
808
809    #[test]
810    fn test_checkpoint_at_end_is_noop() {
811        let mut stack = StrStack::new();
812        stack.push("aaa");
813        stack.push("bbb");
814        let cp = stack.checkpoint();
815
816        // Reset immediately without pushing anything
817        stack.reset(cp);
818        assert_eq!(stack.len(), 2);
819        assert_eq!(stack.get(0), Some("aaa"));
820        assert_eq!(stack.get(1), Some("bbb"));
821    }
822
823    #[test]
824    fn test_checkpoint_nested() {
825        let mut stack = StrStack::new();
826        stack.push("aaa");
827        let cp1 = stack.checkpoint();
828
829        stack.push("bbb");
830        let cp2 = stack.checkpoint();
831
832        stack.push("ccc");
833        assert_eq!(stack.len(), 3);
834
835        // Roll back to cp2 (keeps aaa, bbb)
836        stack.reset(cp2);
837        assert_eq!(stack.len(), 2);
838        assert_eq!(stack.last(), Some("bbb"));
839
840        // Roll back to cp1 (keeps only aaa)
841        stack.reset(cp1);
842        assert_eq!(stack.len(), 1);
843        assert_eq!(stack.last(), Some("aaa"));
844    }
845
846    #[test]
847    fn test_checkpoint_unicode() {
848        let mut stack = StrStack::new();
849        stack.push("δ½ ε₯½"); // 6 bytes
850        let cp = stack.checkpoint();
851
852        stack.push("😊"); // 4 bytes
853        stack.push("πŸ¦€"); // 4 bytes
854        assert_eq!(stack.bytes_len(), 14);
855
856        stack.reset(cp);
857        assert_eq!(stack.len(), 1);
858        assert_eq!(stack.get(0), Some("δ½ ε₯½"));
859        assert_eq!(stack.bytes_len(), 6);
860    }
861
862    #[test]
863    fn test_checkpoint_then_push_after_reset() {
864        let mut stack = StrStack::new();
865        stack.push("aaa");
866        let cp = stack.checkpoint();
867
868        stack.push("bbb");
869        stack.reset(cp);
870
871        // Push new data after rollback
872        stack.push("ccc");
873        assert_eq!(stack.len(), 2);
874        assert_eq!(stack.get(0), Some("aaa"));
875        assert_eq!(stack.get(1), Some("ccc"));
876        assert_eq!(stack.as_str(), "aaaccc");
877    }
878
879    #[test]
880    fn test_truncate_preserves_earlier_checkpoint() {
881        let mut stack = StrStack::new();
882        stack.push("aaa");
883        let cp = stack.checkpoint();
884
885        stack.push("bbb");
886        stack.push("ccc");
887        stack.push("ddd");
888
889        // Truncate to 3 items (removes ddd)
890        stack.truncate(3);
891        assert_eq!(stack.len(), 3);
892
893        // cp was taken at 1 item, still valid
894        stack.reset(cp);
895        assert_eq!(stack.len(), 1);
896        assert_eq!(stack.get(0), Some("aaa"));
897    }
898
899    #[test]
900    #[should_panic(expected = "invalid checkpoint")]
901    fn test_reset_stale_checkpoint_panics() {
902        let mut stack = StrStack::new();
903        stack.push("aaa");
904        stack.push("bbb");
905        stack.push("ccc");
906        let cp_late = stack.checkpoint(); // at 3 items, 9 bytes
907
908        // Truncate to 0 β€” now cp_late is stale
909        stack.truncate(0);
910        assert!(stack.is_empty());
911
912        // cp_late says 3 items / 9 bytes, stack has 0 / 0 β€” should panic
913        stack.reset(cp_late);
914    }
915}