seq_runtime/
seqstring.rs

1//! SeqString - Arena or Globally Allocated String
2//!
3//! Strings in Seq can be allocated from two sources:
4//! 1. Thread-local arena (fast, bulk-freed on strand exit)
5//! 2. Global allocator (persists across arena resets)
6//!
7//! This allows fast temporary string creation during strand execution
8//! while maintaining safety for channel communication (clone to global).
9
10use crate::arena;
11use std::fmt;
12
13/// String that tracks its allocation source
14///
15/// # Safety Invariants
16/// - If global=true: ptr points to global-allocated String, must be dropped
17/// - If global=false: ptr points to thread-local arena, no drop needed
18/// - ptr + len must form a valid UTF-8 string
19/// - For global strings: capacity must match the original String's capacity
20pub struct SeqString {
21    ptr: *const u8,
22    len: usize,
23    capacity: usize, // Only meaningful for global strings
24    global: bool,
25}
26
27// Implement PartialEq manually to compare string content, not pointers
28impl PartialEq for SeqString {
29    fn eq(&self, other: &Self) -> bool {
30        self.as_str() == other.as_str()
31    }
32}
33
34impl Eq for SeqString {}
35
36// Safety: SeqString is Send because:
37// - Global strings are truly independent (owned heap allocation)
38// - Arena strings are cloned to global on channel send (see Clone impl)
39// - We never send arena pointers across threads unsafely
40unsafe impl Send for SeqString {}
41
42impl SeqString {
43    /// Get string slice
44    ///
45    /// # Safety
46    /// ptr + len must point to valid UTF-8. This is guaranteed by constructors.
47    pub fn as_str(&self) -> &str {
48        unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(self.ptr, self.len)) }
49    }
50
51    /// Check if this string is globally allocated
52    #[allow(dead_code)]
53    pub fn is_global(&self) -> bool {
54        self.global
55    }
56
57    /// Get length in bytes
58    pub fn len(&self) -> usize {
59        self.len
60    }
61
62    /// Check if empty
63    #[allow(dead_code)]
64    pub fn is_empty(&self) -> bool {
65        self.len == 0
66    }
67
68    /// Consume self and return raw parts for storage in StackValue
69    ///
70    /// Returns (ptr, len, capacity, global)
71    ///
72    /// # Safety
73    /// The caller must either reconstruct using `from_raw_parts` or
74    /// properly handle drop (for global strings only).
75    pub fn into_raw_parts(self) -> (*const u8, usize, usize, bool) {
76        let parts = (self.ptr, self.len, self.capacity, self.global);
77        std::mem::forget(self); // Don't run Drop
78        parts
79    }
80
81    /// Reconstruct SeqString from raw parts
82    ///
83    /// # Safety
84    /// The parts must have come from `into_raw_parts` on a valid SeqString,
85    /// or be a new valid allocation matching the ptr/len/capacity/global invariants.
86    pub unsafe fn from_raw_parts(
87        ptr: *const u8,
88        len: usize,
89        capacity: usize,
90        global: bool,
91    ) -> Self {
92        SeqString {
93            ptr,
94            len,
95            capacity,
96            global,
97        }
98    }
99}
100
101impl Clone for SeqString {
102    /// Clone always allocates from global allocator for Send safety
103    ///
104    /// This ensures that when a String is sent through a channel,
105    /// the receiving strand gets an independent copy that doesn't
106    /// depend on the sender's arena.
107    fn clone(&self) -> Self {
108        let s = self.as_str().to_string();
109        global_string(s)
110    }
111}
112
113impl Drop for SeqString {
114    fn drop(&mut self) {
115        if self.global {
116            // Reconstruct String and drop it
117            // Safety: We created this from String in global_string() and stored
118            // the original ptr, len, and capacity. This ensures correct deallocation.
119            unsafe {
120                let _s = String::from_raw_parts(
121                    self.ptr as *mut u8,
122                    self.len,
123                    self.capacity, // Use original capacity for correct deallocation
124                );
125                // _s is dropped here, freeing the memory with correct size
126            }
127        }
128        // Arena strings don't need explicit drop - arena reset frees them
129    }
130}
131
132impl fmt::Debug for SeqString {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        write!(f, "SeqString({:?}, global={})", self.as_str(), self.global)
135    }
136}
137
138impl fmt::Display for SeqString {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        write!(f, "{}", self.as_str())
141    }
142}
143
144/// Create arena-allocated string (fast path for temporaries)
145///
146/// # Performance
147/// ~5ns vs ~100ns for global allocator (20x faster)
148///
149/// # Lifetime
150/// Valid until arena_reset() is called (typically when strand exits)
151pub fn arena_string(s: &str) -> SeqString {
152    arena::with_arena(|arena| {
153        let arena_str = arena.alloc_str(s);
154        SeqString {
155            ptr: arena_str.as_ptr(),
156            len: arena_str.len(),
157            capacity: 0, // Not used for arena strings
158            global: false,
159        }
160    })
161}
162
163/// Create globally-allocated string (persists across arena resets)
164///
165/// # Usage
166/// For strings that need to outlive the current strand, or be sent through channels.
167///
168/// # Performance
169/// Same as regular String allocation
170pub fn global_string(s: String) -> SeqString {
171    let len = s.len();
172    let capacity = s.capacity();
173    let ptr = s.as_ptr();
174    std::mem::forget(s); // Transfer ownership, don't drop
175
176    SeqString {
177        ptr,
178        len,
179        capacity, // Store original capacity for correct deallocation
180        global: true,
181    }
182}
183
184/// Convert &str to SeqString using arena allocation
185impl From<&str> for SeqString {
186    fn from(s: &str) -> Self {
187        arena_string(s)
188    }
189}
190
191/// Convert String to SeqString using global allocation
192impl From<String> for SeqString {
193    fn from(s: String) -> Self {
194        global_string(s)
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn test_arena_string() {
204        let s = arena_string("Hello, arena!");
205        assert_eq!(s.as_str(), "Hello, arena!");
206        assert_eq!(s.len(), 13);
207        assert!(!s.is_global());
208    }
209
210    #[test]
211    fn test_global_string() {
212        let s = global_string("Hello, global!".to_string());
213        assert_eq!(s.as_str(), "Hello, global!");
214        assert_eq!(s.len(), 14);
215        assert!(s.is_global());
216    }
217
218    #[test]
219    fn test_clone_creates_global() {
220        // Clone an arena string
221        let s1 = arena_string("test");
222        let s2 = s1.clone();
223
224        assert_eq!(s1.as_str(), s2.as_str());
225        assert!(!s1.is_global());
226        assert!(s2.is_global()); // Clone is always global!
227    }
228
229    #[test]
230    fn test_clone_global() {
231        let s1 = global_string("test".to_string());
232        let s2 = s1.clone();
233
234        assert_eq!(s1.as_str(), s2.as_str());
235        assert!(s1.is_global());
236        assert!(s2.is_global());
237    }
238
239    #[test]
240    fn test_drop_global() {
241        // Create and drop a global string
242        {
243            let s = global_string("Will be dropped".to_string());
244            assert_eq!(s.as_str(), "Will be dropped");
245        }
246        // If we get here without crashing, drop worked
247    }
248
249    #[test]
250    fn test_drop_arena() {
251        // Create and drop an arena string
252        {
253            let s = arena_string("Will be dropped (no-op)");
254            assert_eq!(s.as_str(), "Will be dropped (no-op)");
255        }
256        // Arena strings don't need explicit drop
257    }
258
259    #[test]
260    fn test_equality() {
261        let s1 = arena_string("test");
262        let s2 = arena_string("test");
263        let s3 = global_string("test".to_string());
264        let s4 = arena_string("different");
265
266        assert_eq!(s1, s2); // Same content, both arena
267        assert_eq!(s1, s3); // Same content, different allocation
268        assert_ne!(s1, s4); // Different content
269    }
270
271    #[test]
272    fn test_from_str() {
273        let s: SeqString = "test".into();
274        assert_eq!(s.as_str(), "test");
275        assert!(!s.is_global()); // from &str uses arena
276    }
277
278    #[test]
279    fn test_from_string() {
280        let s: SeqString = "test".to_string().into();
281        assert_eq!(s.as_str(), "test");
282        assert!(s.is_global()); // from String uses global
283    }
284
285    #[test]
286    fn test_debug_format() {
287        let s = arena_string("debug");
288        let debug_str = format!("{:?}", s);
289        assert!(debug_str.contains("debug"));
290        assert!(debug_str.contains("global=false"));
291    }
292
293    #[test]
294    fn test_display_format() {
295        let s = global_string("display".to_string());
296        let display_str = format!("{}", s);
297        assert_eq!(display_str, "display");
298    }
299
300    #[test]
301    fn test_empty_string() {
302        let s = arena_string("");
303        assert_eq!(s.len(), 0);
304        assert!(s.is_empty());
305        assert_eq!(s.as_str(), "");
306    }
307
308    #[test]
309    fn test_unicode() {
310        let s = arena_string("Hello, δΈ–η•Œ! πŸ¦€");
311        assert_eq!(s.as_str(), "Hello, δΈ–η•Œ! πŸ¦€");
312        assert!(s.len() > 10); // UTF-8 bytes, not chars
313    }
314
315    #[test]
316    fn test_global_string_preserves_capacity() {
317        // PR #11 Critical fix: Verify capacity is preserved for correct deallocation
318        let mut s = String::with_capacity(100);
319        s.push_str("hi");
320
321        assert_eq!(s.len(), 2);
322        assert_eq!(s.capacity(), 100);
323
324        let cem = global_string(s);
325
326        // Verify the SeqString captured the original capacity
327        assert_eq!(cem.len(), 2);
328        assert_eq!(cem.capacity, 100); // Critical: Must be 100, not 2!
329        assert_eq!(cem.as_str(), "hi");
330        assert!(cem.is_global());
331
332        // Drop cem - if capacity was wrong, this would cause heap corruption
333        drop(cem);
334
335        // If we get here without crash/UB, the fix worked
336    }
337
338    #[test]
339    fn test_arena_string_capacity_zero() {
340        // Arena strings don't use capacity field
341        let s = arena_string("test");
342        assert_eq!(s.capacity, 0); // Arena strings have capacity=0
343        assert!(!s.is_global());
344    }
345}