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
69impl Clone for SeqString {
70    /// Clone always allocates from global allocator for Send safety
71    ///
72    /// This ensures that when a String is sent through a channel,
73    /// the receiving strand gets an independent copy that doesn't
74    /// depend on the sender's arena.
75    fn clone(&self) -> Self {
76        let s = self.as_str().to_string();
77        global_string(s)
78    }
79}
80
81impl Drop for SeqString {
82    fn drop(&mut self) {
83        if self.global {
84            // Reconstruct String and drop it
85            // Safety: We created this from String in global_string() and stored
86            // the original ptr, len, and capacity. This ensures correct deallocation.
87            unsafe {
88                let _s = String::from_raw_parts(
89                    self.ptr as *mut u8,
90                    self.len,
91                    self.capacity, // Use original capacity for correct deallocation
92                );
93                // _s is dropped here, freeing the memory with correct size
94            }
95        }
96        // Arena strings don't need explicit drop - arena reset frees them
97    }
98}
99
100impl fmt::Debug for SeqString {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(f, "SeqString({:?}, global={})", self.as_str(), self.global)
103    }
104}
105
106impl fmt::Display for SeqString {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        write!(f, "{}", self.as_str())
109    }
110}
111
112/// Create arena-allocated string (fast path for temporaries)
113///
114/// # Performance
115/// ~5ns vs ~100ns for global allocator (20x faster)
116///
117/// # Lifetime
118/// Valid until arena_reset() is called (typically when strand exits)
119pub fn arena_string(s: &str) -> SeqString {
120    arena::with_arena(|arena| {
121        let arena_str = arena.alloc_str(s);
122        SeqString {
123            ptr: arena_str.as_ptr(),
124            len: arena_str.len(),
125            capacity: 0, // Not used for arena strings
126            global: false,
127        }
128    })
129}
130
131/// Create globally-allocated string (persists across arena resets)
132///
133/// # Usage
134/// For strings that need to outlive the current strand, or be sent through channels.
135///
136/// # Performance
137/// Same as regular String allocation
138pub fn global_string(s: String) -> SeqString {
139    let len = s.len();
140    let capacity = s.capacity();
141    let ptr = s.as_ptr();
142    std::mem::forget(s); // Transfer ownership, don't drop
143
144    SeqString {
145        ptr,
146        len,
147        capacity, // Store original capacity for correct deallocation
148        global: true,
149    }
150}
151
152/// Convert &str to SeqString using arena allocation
153impl From<&str> for SeqString {
154    fn from(s: &str) -> Self {
155        arena_string(s)
156    }
157}
158
159/// Convert String to SeqString using global allocation
160impl From<String> for SeqString {
161    fn from(s: String) -> Self {
162        global_string(s)
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_arena_string() {
172        let s = arena_string("Hello, arena!");
173        assert_eq!(s.as_str(), "Hello, arena!");
174        assert_eq!(s.len(), 13);
175        assert!(!s.is_global());
176    }
177
178    #[test]
179    fn test_global_string() {
180        let s = global_string("Hello, global!".to_string());
181        assert_eq!(s.as_str(), "Hello, global!");
182        assert_eq!(s.len(), 14);
183        assert!(s.is_global());
184    }
185
186    #[test]
187    fn test_clone_creates_global() {
188        // Clone an arena string
189        let s1 = arena_string("test");
190        let s2 = s1.clone();
191
192        assert_eq!(s1.as_str(), s2.as_str());
193        assert!(!s1.is_global());
194        assert!(s2.is_global()); // Clone is always global!
195    }
196
197    #[test]
198    fn test_clone_global() {
199        let s1 = global_string("test".to_string());
200        let s2 = s1.clone();
201
202        assert_eq!(s1.as_str(), s2.as_str());
203        assert!(s1.is_global());
204        assert!(s2.is_global());
205    }
206
207    #[test]
208    fn test_drop_global() {
209        // Create and drop a global string
210        {
211            let s = global_string("Will be dropped".to_string());
212            assert_eq!(s.as_str(), "Will be dropped");
213        }
214        // If we get here without crashing, drop worked
215    }
216
217    #[test]
218    fn test_drop_arena() {
219        // Create and drop an arena string
220        {
221            let s = arena_string("Will be dropped (no-op)");
222            assert_eq!(s.as_str(), "Will be dropped (no-op)");
223        }
224        // Arena strings don't need explicit drop
225    }
226
227    #[test]
228    fn test_equality() {
229        let s1 = arena_string("test");
230        let s2 = arena_string("test");
231        let s3 = global_string("test".to_string());
232        let s4 = arena_string("different");
233
234        assert_eq!(s1, s2); // Same content, both arena
235        assert_eq!(s1, s3); // Same content, different allocation
236        assert_ne!(s1, s4); // Different content
237    }
238
239    #[test]
240    fn test_from_str() {
241        let s: SeqString = "test".into();
242        assert_eq!(s.as_str(), "test");
243        assert!(!s.is_global()); // from &str uses arena
244    }
245
246    #[test]
247    fn test_from_string() {
248        let s: SeqString = "test".to_string().into();
249        assert_eq!(s.as_str(), "test");
250        assert!(s.is_global()); // from String uses global
251    }
252
253    #[test]
254    fn test_debug_format() {
255        let s = arena_string("debug");
256        let debug_str = format!("{:?}", s);
257        assert!(debug_str.contains("debug"));
258        assert!(debug_str.contains("global=false"));
259    }
260
261    #[test]
262    fn test_display_format() {
263        let s = global_string("display".to_string());
264        let display_str = format!("{}", s);
265        assert_eq!(display_str, "display");
266    }
267
268    #[test]
269    fn test_empty_string() {
270        let s = arena_string("");
271        assert_eq!(s.len(), 0);
272        assert!(s.is_empty());
273        assert_eq!(s.as_str(), "");
274    }
275
276    #[test]
277    fn test_unicode() {
278        let s = arena_string("Hello, δΈ–η•Œ! πŸ¦€");
279        assert_eq!(s.as_str(), "Hello, δΈ–η•Œ! πŸ¦€");
280        assert!(s.len() > 10); // UTF-8 bytes, not chars
281    }
282
283    #[test]
284    fn test_global_string_preserves_capacity() {
285        // PR #11 Critical fix: Verify capacity is preserved for correct deallocation
286        let mut s = String::with_capacity(100);
287        s.push_str("hi");
288
289        assert_eq!(s.len(), 2);
290        assert_eq!(s.capacity(), 100);
291
292        let cem = global_string(s);
293
294        // Verify the SeqString captured the original capacity
295        assert_eq!(cem.len(), 2);
296        assert_eq!(cem.capacity, 100); // Critical: Must be 100, not 2!
297        assert_eq!(cem.as_str(), "hi");
298        assert!(cem.is_global());
299
300        // Drop cem - if capacity was wrong, this would cause heap corruption
301        drop(cem);
302
303        // If we get here without crash/UB, the fix worked
304    }
305
306    #[test]
307    fn test_arena_string_capacity_zero() {
308        // Arena strings don't use capacity field
309        let s = arena_string("test");
310        assert_eq!(s.capacity, 0); // Arena strings have capacity=0
311        assert!(!s.is_global());
312    }
313}