Skip to main content

oxihuman_core/
arena_allocator.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Bump/arena allocator with alignment support.
5
6/// Configuration for an arena allocator.
7#[derive(Debug, Clone)]
8pub struct ArenaConfig {
9    pub chunk_size: usize,
10}
11
12/// A bump/arena allocator backed by a `Vec<u8>`.
13#[derive(Debug, Clone)]
14pub struct ArenaAllocator {
15    pub buf: Vec<u8>,
16    pub used: usize,
17    alloc_count: usize,
18}
19
20/// Statistics about the arena.
21#[derive(Debug, Clone)]
22pub struct ArenaStats {
23    pub used: usize,
24    pub capacity: usize,
25    pub alloc_count: usize,
26}
27
28/// Return the default arena configuration.
29pub fn default_arena_config() -> ArenaConfig {
30    ArenaConfig { chunk_size: 4096 }
31}
32
33/// Create a new arena allocator.
34pub fn new_arena(config: &ArenaConfig) -> ArenaAllocator {
35    ArenaAllocator {
36        buf: vec![0u8; config.chunk_size],
37        used: 0,
38        alloc_count: 0,
39    }
40}
41
42/// Bump-allocate `size` bytes. Returns the byte offset into `buf`, or `None` if out of space.
43pub fn arena_alloc_bytes(arena: &mut ArenaAllocator, size: usize) -> Option<usize> {
44    if arena.used + size > arena.buf.len() {
45        return None;
46    }
47    let offset = arena.used;
48    arena.used += size;
49    arena.alloc_count += 1;
50    Some(offset)
51}
52
53/// Bump-allocate `size` bytes with the given alignment.
54///
55/// Returns the aligned byte offset into `buf`, or `None` if out of space.
56/// The `align` argument is rounded up to at least 1.
57pub fn arena_alloc_bytes_aligned(
58    arena: &mut ArenaAllocator,
59    size: usize,
60    align: usize,
61) -> Option<usize> {
62    let align = align.max(1);
63    let aligned_offset = (arena.used + align - 1) & !(align - 1);
64    if aligned_offset + size > arena.buf.len() {
65        return None;
66    }
67    arena.used = aligned_offset + size;
68    arena.alloc_count += 1;
69    Some(aligned_offset)
70}
71
72/// Reset the arena (frees all allocations at once).
73pub fn arena_reset(arena: &mut ArenaAllocator) {
74    arena.used = 0;
75    arena.alloc_count = 0;
76}
77
78/// Return arena statistics.
79pub fn arena_stats(arena: &ArenaAllocator) -> ArenaStats {
80    ArenaStats {
81        used: arena.used,
82        capacity: arena.buf.len(),
83        alloc_count: arena.alloc_count,
84    }
85}
86
87/// Return the number of bytes remaining.
88pub fn arena_remaining(arena: &ArenaAllocator) -> usize {
89    arena.buf.len().saturating_sub(arena.used)
90}
91
92/// Return the usage ratio (0.0 = empty, 1.0 = full).
93pub fn arena_usage_ratio(arena: &ArenaAllocator) -> f32 {
94    if arena.buf.is_empty() {
95        return 1.0;
96    }
97    arena.used as f32 / arena.buf.len() as f32
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use proptest::prelude::*;
104
105    // ---------- existing unit tests ----------
106
107    #[test]
108    fn test_default_config() {
109        let cfg = default_arena_config();
110        assert!(cfg.chunk_size > 0);
111    }
112
113    #[test]
114    fn test_new_arena_empty() {
115        let cfg = ArenaConfig { chunk_size: 1024 };
116        let arena = new_arena(&cfg);
117        assert_eq!(arena.used, 0);
118        assert_eq!(arena.buf.len(), 1024);
119    }
120
121    #[test]
122    fn test_alloc_bytes() {
123        let cfg = ArenaConfig { chunk_size: 256 };
124        let mut arena = new_arena(&cfg);
125        let off = arena_alloc_bytes(&mut arena, 64);
126        assert_eq!(off, Some(0));
127        assert_eq!(arena.used, 64);
128    }
129
130    #[test]
131    fn test_alloc_sequential() {
132        let cfg = ArenaConfig { chunk_size: 256 };
133        let mut arena = new_arena(&cfg);
134        let off1 = arena_alloc_bytes(&mut arena, 32).expect("should succeed");
135        let off2 = arena_alloc_bytes(&mut arena, 32).expect("should succeed");
136        assert_eq!(off1, 0);
137        assert_eq!(off2, 32);
138    }
139
140    #[test]
141    fn test_alloc_out_of_space() {
142        let cfg = ArenaConfig { chunk_size: 10 };
143        let mut arena = new_arena(&cfg);
144        assert!(arena_alloc_bytes(&mut arena, 20).is_none());
145    }
146
147    #[test]
148    fn test_reset() {
149        let cfg = ArenaConfig { chunk_size: 64 };
150        let mut arena = new_arena(&cfg);
151        arena_alloc_bytes(&mut arena, 32);
152        arena_reset(&mut arena);
153        assert_eq!(arena.used, 0);
154        let stats = arena_stats(&arena);
155        assert_eq!(stats.alloc_count, 0);
156    }
157
158    #[test]
159    fn test_remaining() {
160        let cfg = ArenaConfig { chunk_size: 100 };
161        let mut arena = new_arena(&cfg);
162        arena_alloc_bytes(&mut arena, 40);
163        assert_eq!(arena_remaining(&arena), 60);
164    }
165
166    #[test]
167    fn test_usage_ratio() {
168        let cfg = ArenaConfig { chunk_size: 100 };
169        let mut arena = new_arena(&cfg);
170        arena_alloc_bytes(&mut arena, 50);
171        let ratio = arena_usage_ratio(&arena);
172        assert!((ratio - 0.5).abs() < 1e-6);
173    }
174
175    #[test]
176    fn test_stats() {
177        let cfg = ArenaConfig { chunk_size: 128 };
178        let mut arena = new_arena(&cfg);
179        arena_alloc_bytes(&mut arena, 10);
180        arena_alloc_bytes(&mut arena, 20);
181        let stats = arena_stats(&arena);
182        assert_eq!(stats.alloc_count, 2);
183        assert_eq!(stats.used, 30);
184        assert_eq!(stats.capacity, 128);
185    }
186
187    // ---------- new aligned-allocation tests ----------
188
189    #[test]
190    fn test_alloc_aligned() {
191        // Allocate 1 byte, then 4 bytes with align=4; second offset must be 4.
192        let cfg = ArenaConfig { chunk_size: 256 };
193        let mut arena = new_arena(&cfg);
194        let off1 = arena_alloc_bytes_aligned(&mut arena, 1, 1).expect("first alloc");
195        assert_eq!(off1, 0);
196        let off2 = arena_alloc_bytes_aligned(&mut arena, 4, 4).expect("second alloc");
197        assert_eq!(off2, 4);
198    }
199
200    #[test]
201    fn test_alloc_large_alignment() {
202        let cfg = ArenaConfig { chunk_size: 256 };
203        let mut arena = new_arena(&cfg);
204        // Misalign by 1 byte first.
205        arena_alloc_bytes(&mut arena, 1);
206        // Request 8 bytes with align=16; offset must be multiple of 16.
207        let off = arena_alloc_bytes_aligned(&mut arena, 8, 16).expect("aligned alloc");
208        assert_eq!(off % 16, 0);
209    }
210
211    #[test]
212    fn test_aligned_out_of_space() {
213        let cfg = ArenaConfig { chunk_size: 8 };
214        let mut arena = new_arena(&cfg);
215        assert!(arena_alloc_bytes_aligned(&mut arena, 16, 1).is_none());
216    }
217
218    // ---------- proptest: alignment + no-overlap ----------
219
220    proptest! {
221        #[test]
222        fn prop_aligned_alloc_invariants(
223            allocs in proptest::collection::vec(
224                (1usize..=64usize,
225                 proptest::sample::select(vec![1usize, 2, 4, 8, 16, 32])),
226                0..50
227            )
228        ) {
229            // Use a large enough arena (50 * 64 + max padding ~32 per entry).
230            let cfg = ArenaConfig { chunk_size: 8192 };
231            let mut arena = new_arena(&cfg);
232            let mut prev_end: usize = 0;
233
234            for (size, align) in &allocs {
235                let size = *size;
236                let align = *align;
237                let off = match arena_alloc_bytes_aligned(&mut arena, size, align) {
238                    Some(o) => o,
239                    None => break, // arena full — ok, stop checking
240                };
241                // Returned offset must satisfy the alignment.
242                prop_assert_eq!(off % align, 0, "offset {} not aligned to {}", off, align);
243                // Returned offset must not overlap the previous allocation.
244                prop_assert!(off >= prev_end, "overlap: off={} prev_end={}", off, prev_end);
245                prev_end = off + size;
246            }
247        }
248    }
249}