/home/noah/src/ruchy/src/runtime/safe_arena.rs
Line | Count | Source |
1 | | //! Safe arena allocator without unsafe code |
2 | | //! |
3 | | //! Provides bounded memory allocation using safe Rust abstractions. |
4 | | |
5 | | use anyhow::{Result, anyhow}; |
6 | | use std::cell::RefCell; |
7 | | use std::rc::Rc; |
8 | | |
9 | | // ============================================================================ |
10 | | // Safe Arena Allocator |
11 | | // ============================================================================ |
12 | | |
13 | | /// Safe arena allocator using Rc for memory management |
14 | | #[derive(Debug)] |
15 | | pub struct SafeArena { |
16 | | /// Storage for allocated values |
17 | | storage: RefCell<Vec<Box<dyn std::any::Any>>>, |
18 | | /// Current memory usage estimate |
19 | | used: RefCell<usize>, |
20 | | /// Maximum allowed memory |
21 | | max_size: usize, |
22 | | } |
23 | | |
24 | | impl SafeArena { |
25 | | /// Create a new arena with the given size limit |
26 | 0 | pub fn new(max_size: usize) -> Self { |
27 | 0 | Self { |
28 | 0 | storage: RefCell::new(Vec::new()), |
29 | 0 | used: RefCell::new(0), |
30 | 0 | max_size, |
31 | 0 | } |
32 | 0 | } |
33 | | |
34 | | /// Allocate a value in the arena |
35 | 0 | pub fn alloc<T: 'static>(&self, value: T) -> Result<ArenaRef<'_, T>> { |
36 | 0 | let size = std::mem::size_of::<T>(); |
37 | | |
38 | | // Check memory limit |
39 | 0 | if *self.used.borrow() + size > self.max_size { |
40 | 0 | return Err(anyhow!("Arena memory limit exceeded")); |
41 | 0 | } |
42 | | |
43 | | // Store value in Rc |
44 | 0 | let rc_value = Rc::new(value); |
45 | 0 | self.storage.borrow_mut().push(Box::new(rc_value.clone()) as Box<dyn std::any::Any>); |
46 | 0 | *self.used.borrow_mut() += size; |
47 | | |
48 | 0 | Ok(ArenaRef { |
49 | 0 | value: rc_value, |
50 | 0 | _arena: self, |
51 | 0 | }) |
52 | 0 | } |
53 | | |
54 | | /// Reset the arena, clearing all allocations |
55 | 0 | pub fn reset(&self) { |
56 | 0 | self.storage.borrow_mut().clear(); |
57 | 0 | *self.used.borrow_mut() = 0; |
58 | 0 | } |
59 | | |
60 | | /// Get current memory usage |
61 | 0 | pub fn used(&self) -> usize { |
62 | 0 | *self.used.borrow() |
63 | 0 | } |
64 | | } |
65 | | |
66 | | /// Reference to a value in the arena |
67 | | #[derive(Debug)] |
68 | | pub struct ArenaRef<'a, T> { |
69 | | value: Rc<T>, |
70 | | _arena: &'a SafeArena, |
71 | | } |
72 | | |
73 | | impl<T> std::ops::Deref for ArenaRef<'_, T> { |
74 | | type Target = T; |
75 | | |
76 | 0 | fn deref(&self) -> &Self::Target { |
77 | 0 | &self.value |
78 | 0 | } |
79 | | } |
80 | | |
81 | | // ============================================================================ |
82 | | // Transactional Arena |
83 | | // ============================================================================ |
84 | | |
85 | | /// Arena with checkpoint/rollback support |
86 | | #[derive(Debug)] |
87 | | pub struct TransactionalArena { |
88 | | /// Current values |
89 | | current: Rc<SafeArena>, |
90 | | /// Checkpoints |
91 | | checkpoints: Vec<ArenaCheckpoint>, |
92 | | } |
93 | | |
94 | | #[derive(Clone, Debug)] |
95 | | struct ArenaCheckpoint { |
96 | | storage_size: usize, |
97 | | used: usize, |
98 | | } |
99 | | |
100 | | impl TransactionalArena { |
101 | 0 | pub fn new(max_size: usize) -> Self { |
102 | 0 | Self { |
103 | 0 | current: Rc::new(SafeArena::new(max_size)), |
104 | 0 | checkpoints: Vec::new(), |
105 | 0 | } |
106 | 0 | } |
107 | | |
108 | 0 | pub fn checkpoint(&mut self) -> usize { |
109 | 0 | let checkpoint = ArenaCheckpoint { |
110 | 0 | storage_size: self.current.storage.borrow().len(), |
111 | 0 | used: self.current.used(), |
112 | 0 | }; |
113 | 0 | self.checkpoints.push(checkpoint); |
114 | 0 | self.checkpoints.len() - 1 |
115 | 0 | } |
116 | | |
117 | 0 | pub fn rollback(&mut self, checkpoint_id: usize) -> Result<()> { |
118 | 0 | if checkpoint_id >= self.checkpoints.len() { |
119 | 0 | return Err(anyhow!("Invalid checkpoint")); |
120 | 0 | } |
121 | | |
122 | 0 | let checkpoint = &self.checkpoints[checkpoint_id]; |
123 | | |
124 | | // Truncate storage to checkpoint size |
125 | 0 | self.current.storage.borrow_mut().truncate(checkpoint.storage_size); |
126 | 0 | *self.current.used.borrow_mut() = checkpoint.used; |
127 | | |
128 | | // Remove later checkpoints |
129 | 0 | self.checkpoints.truncate(checkpoint_id + 1); |
130 | | |
131 | 0 | Ok(()) |
132 | 0 | } |
133 | | |
134 | 0 | pub fn commit(&mut self) -> Result<()> { |
135 | 0 | if self.checkpoints.is_empty() { |
136 | 0 | return Err(anyhow!("No checkpoint to commit")); |
137 | 0 | } |
138 | 0 | self.checkpoints.pop(); |
139 | 0 | Ok(()) |
140 | 0 | } |
141 | | |
142 | 0 | pub fn arena(&self) -> &SafeArena { |
143 | 0 | &self.current |
144 | 0 | } |
145 | | |
146 | 0 | pub fn reset(&mut self) { |
147 | 0 | self.current.reset(); |
148 | 0 | self.checkpoints.clear(); |
149 | 0 | } |
150 | | } |
151 | | |
152 | | #[cfg(test)] |
153 | | mod tests { |
154 | | use super::*; |
155 | | |
156 | | #[test] |
157 | | fn test_safe_arena() { |
158 | | let arena = SafeArena::new(1024); |
159 | | |
160 | | let v1 = arena.alloc(42).unwrap(); |
161 | | let v2 = arena.alloc("hello".to_string()).unwrap(); |
162 | | |
163 | | assert_eq!(*v1, 42); |
164 | | assert_eq!(*v2, "hello"); |
165 | | |
166 | | arena.reset(); |
167 | | assert_eq!(arena.used(), 0); |
168 | | } |
169 | | |
170 | | #[test] |
171 | | fn test_transactional() { |
172 | | let mut arena = TransactionalArena::new(1024); |
173 | | |
174 | | arena.arena().alloc(1).unwrap(); |
175 | | let checkpoint = arena.checkpoint(); |
176 | | |
177 | | arena.arena().alloc(2).unwrap(); |
178 | | let used_before = arena.arena().used(); |
179 | | |
180 | | arena.rollback(checkpoint).unwrap(); |
181 | | let used_after = arena.arena().used(); |
182 | | |
183 | | assert!(used_after < used_before); |
184 | | } |
185 | | |
186 | | #[test] |
187 | | fn test_arena_memory_limit() { |
188 | | let arena = SafeArena::new(16); // Very small limit |
189 | | |
190 | | // First allocation should succeed |
191 | | let _val1 = arena.alloc([0u8; 8]).unwrap(); |
192 | | |
193 | | // Second allocation should fail due to memory limit |
194 | | let result = arena.alloc([0u8; 16]); |
195 | | assert!(result.is_err()); |
196 | | assert!(result.unwrap_err().to_string().contains("memory limit exceeded")); |
197 | | } |
198 | | |
199 | | #[test] |
200 | | fn test_arena_used_tracking() { |
201 | | let arena = SafeArena::new(1024); |
202 | | assert_eq!(arena.used(), 0); |
203 | | |
204 | | let _val1 = arena.alloc(42i32).unwrap(); |
205 | | let used_after_int = arena.used(); |
206 | | assert!(used_after_int >= 4); // At least size of i32 |
207 | | |
208 | | let _val2 = arena.alloc("test".to_string()).unwrap(); |
209 | | let used_after_string = arena.used(); |
210 | | assert!(used_after_string > used_after_int); |
211 | | } |
212 | | |
213 | | #[test] |
214 | | fn test_arena_ref_deref() { |
215 | | let arena = SafeArena::new(1024); |
216 | | let val = arena.alloc(vec![1, 2, 3, 4]).unwrap(); |
217 | | |
218 | | // Test Deref trait |
219 | | assert_eq!(val.len(), 4); |
220 | | assert_eq!(val[0], 1); |
221 | | assert_eq!(val[3], 4); |
222 | | } |
223 | | |
224 | | #[test] |
225 | | fn test_transactional_arena_new() { |
226 | | let arena = TransactionalArena::new(2048); |
227 | | assert_eq!(arena.arena().used(), 0); |
228 | | assert!(arena.checkpoints.is_empty()); |
229 | | } |
230 | | |
231 | | #[test] |
232 | | fn test_transactional_arena_multiple_checkpoints() { |
233 | | let mut arena = TransactionalArena::new(1024); |
234 | | |
235 | | // Initial allocation |
236 | | arena.arena().alloc(100).unwrap(); |
237 | | |
238 | | // First checkpoint |
239 | | let cp1 = arena.checkpoint(); |
240 | | arena.arena().alloc(200).unwrap(); |
241 | | |
242 | | // Second checkpoint |
243 | | let _cp2 = arena.checkpoint(); |
244 | | arena.arena().alloc(300).unwrap(); |
245 | | |
246 | | // Rollback to first checkpoint |
247 | | arena.rollback(cp1).unwrap(); |
248 | | |
249 | | // Should only have allocations up to first checkpoint |
250 | | let used = arena.arena().used(); |
251 | | assert!(used >= 4); // At least the first allocation |
252 | | } |
253 | | |
254 | | #[test] |
255 | | fn test_transactional_arena_invalid_checkpoint() { |
256 | | let mut arena = TransactionalArena::new(1024); |
257 | | |
258 | | // Try to rollback to invalid checkpoint |
259 | | let result = arena.rollback(999); |
260 | | assert!(result.is_err()); |
261 | | assert!(result.unwrap_err().to_string().contains("Invalid checkpoint")); |
262 | | } |
263 | | |
264 | | #[test] |
265 | | fn test_transactional_arena_commit() { |
266 | | let mut arena = TransactionalArena::new(1024); |
267 | | |
268 | | // Create checkpoint |
269 | | let _cp = arena.checkpoint(); |
270 | | assert!(!arena.checkpoints.is_empty()); |
271 | | |
272 | | // Commit should remove the checkpoint |
273 | | arena.commit().unwrap(); |
274 | | assert!(arena.checkpoints.is_empty()); |
275 | | } |
276 | | |
277 | | #[test] |
278 | | fn test_transactional_arena_commit_without_checkpoint() { |
279 | | let mut arena = TransactionalArena::new(1024); |
280 | | |
281 | | // Try to commit without checkpoint |
282 | | let result = arena.commit(); |
283 | | assert!(result.is_err()); |
284 | | assert!(result.unwrap_err().to_string().contains("No checkpoint to commit")); |
285 | | } |
286 | | |
287 | | #[test] |
288 | | fn test_transactional_arena_reset() { |
289 | | let mut arena = TransactionalArena::new(1024); |
290 | | |
291 | | // Add some data and checkpoints |
292 | | arena.arena().alloc(42).unwrap(); |
293 | | arena.checkpoint(); |
294 | | arena.arena().alloc(84).unwrap(); |
295 | | |
296 | | assert!(arena.arena().used() > 0); |
297 | | assert!(!arena.checkpoints.is_empty()); |
298 | | |
299 | | // Reset should clear everything |
300 | | arena.reset(); |
301 | | assert_eq!(arena.arena().used(), 0); |
302 | | assert!(arena.checkpoints.is_empty()); |
303 | | } |
304 | | |
305 | | #[test] |
306 | | fn test_checkpoint_clone() { |
307 | | let checkpoint1 = ArenaCheckpoint { |
308 | | storage_size: 10, |
309 | | used: 100, |
310 | | }; |
311 | | let checkpoint2 = checkpoint1.clone(); |
312 | | |
313 | | assert_eq!(checkpoint1.storage_size, checkpoint2.storage_size); |
314 | | assert_eq!(checkpoint1.used, checkpoint2.used); |
315 | | } |
316 | | |
317 | | #[test] |
318 | | fn test_arena_with_different_types() { |
319 | | let arena = SafeArena::new(1024); |
320 | | |
321 | | // Allocate different types |
322 | | let int_val = arena.alloc(42i32).unwrap(); |
323 | | let string_val = arena.alloc("hello".to_string()).unwrap(); |
324 | | let vec_val = arena.alloc(vec![1, 2, 3]).unwrap(); |
325 | | let bool_val = arena.alloc(true).unwrap(); |
326 | | |
327 | | // Verify all values |
328 | | assert_eq!(*int_val, 42); |
329 | | assert_eq!(*string_val, "hello"); |
330 | | assert_eq!(*vec_val, vec![1, 2, 3]); |
331 | | assert!(*bool_val); |
332 | | } |
333 | | |
334 | | #[test] |
335 | | fn test_transactional_arena_nested_operations() { |
336 | | let mut arena = TransactionalArena::new(1024); |
337 | | |
338 | | // Initial state |
339 | | arena.arena().alloc(1).unwrap(); |
340 | | let initial_used = arena.arena().used(); |
341 | | |
342 | | // Start transaction |
343 | | let cp = arena.checkpoint(); |
344 | | arena.arena().alloc(2).unwrap(); |
345 | | arena.arena().alloc(3).unwrap(); |
346 | | |
347 | | let mid_used = arena.arena().used(); |
348 | | assert!(mid_used > initial_used); |
349 | | |
350 | | // Rollback |
351 | | arena.rollback(cp).unwrap(); |
352 | | let final_used = arena.arena().used(); |
353 | | |
354 | | assert_eq!(final_used, initial_used); |
355 | | } |
356 | | |
357 | | #[test] |
358 | | fn test_arena_large_allocation() { |
359 | | let arena = SafeArena::new(1024); |
360 | | |
361 | | // Try to allocate something larger than limit |
362 | | let result = arena.alloc([0u8; 2048]); |
363 | | assert!(result.is_err()); |
364 | | } |
365 | | |
366 | | #[test] |
367 | | fn test_transactional_checkpoint_return_value() { |
368 | | let mut arena = TransactionalArena::new(1024); |
369 | | |
370 | | let cp1 = arena.checkpoint(); |
371 | | assert_eq!(cp1, 0); |
372 | | |
373 | | let cp2 = arena.checkpoint(); |
374 | | assert_eq!(cp2, 1); |
375 | | |
376 | | let cp3 = arena.checkpoint(); |
377 | | assert_eq!(cp3, 2); |
378 | | } |
379 | | } |