ida_rs/
lib.rs

1#![cfg_attr(not(test), no_std)]
2
3//!
4//! # An ID Allocator for Sparse ID Spaces
5//!
6//! `ida-rs` provides a thread-safe, `no_std` compatible ID allocator suitable for
7//! systems-level programming, such as in OS kernels or embedded environments.
8//!
9//! It is implemented as a radix tree, which makes it highly memory-efficient
10//! when dealing with sparse ID allocations (e.g., allocating ID 5 and ID 5,000,000
11//! without allocating the space in between).
12//!
13//! ## Features
14//! - **`no_std` compatible:** Usable in bare-metal environments.
15//! - **Thread-Safe:** All public methods are thread-safe, using a spinlock for synchronization.
16//! - **Memory-Efficient for Sparse Sets:** Ideal when allocated IDs are far apart.
17//!
18//! ## Example
19//! ```
20//! use ida_rs::Ida;
21//!
22//! let ida = Ida::new();
23//!
24//! // Allocate new IDs
25//! let id1 = ida.alloc().unwrap();
26//! let id2 = ida.alloc().unwrap();
27//!
28//! assert_eq!(id1, 0);
29//! assert_eq!(id2, 1);
30//!
31//! // Free an ID
32//! ida.free(id1);
33//!
34//! // The next allocation reuses the freed ID
35//! let id3 = ida.alloc().unwrap();
36//! assert_eq!(id3, 0);
37//! ```
38
39extern crate alloc;
40
41use alloc::{boxed::Box, collections::btree_map::BTreeMap};
42use core::fmt::Debug;
43use spin::Mutex;
44
45const IDA_SHIFT: usize = 6;
46const IDA_BITMAP_BITS: usize = 1 << IDA_SHIFT;
47// This calculation is the integer division equivalent of `ceil(64 / IDA_SHIFT)`
48// and ensures that we have enough levels to cover the entire 64-bit ID space.
49// We intentionally use this arithmetic to maintain compatibility with older Rust versions
50// that do not have the `div_ceil` function stabilized.
51#[allow(clippy::manual_div_ceil)]
52const IDA_MAX_LEVELS: usize = (64 + IDA_SHIFT - 1) / IDA_SHIFT;
53
54/// A thread-safe ID allocator for sparse ID spaces.
55///
56/// `Ida` (ID Allocator) manages a pool of unique integer IDs, implemented as a
57/// radix tree for memory efficiency. It's particularly well-suited for scenarios
58/// where allocated IDs may be far apart (e.g., allocating IDs 5 and 5,000,000).
59///
60/// # Thread Safety
61///
62/// All methods are thread-safe and can be safely shared across threads using `Arc`.
63/// The implementation uses a spinlock internally for synchronization.
64///
65/// # Memory Efficiency
66///
67/// The radix tree structure ensures that only allocated regions of the ID space
68/// consume memory. Sparse allocations do not waste space on unallocated ranges.
69///
70/// # Examples
71///
72/// Basic usage:
73/// ```
74/// use ida_rs::Ida;
75///
76/// let ida = Ida::new();
77/// let id1 = ida.alloc().unwrap();
78/// let id2 = ida.alloc().unwrap();
79/// ida.free(id1);
80/// let id3 = ida.alloc().unwrap(); // Reuses id1
81/// ```
82///
83/// Multi-threaded usage:
84/// ```
85/// use ida_rs::Ida;
86/// use std::sync::Arc;
87/// use std::thread;
88///
89/// let ida = Arc::new(Ida::new());
90/// let mut handles = vec![];
91///
92/// for _ in 0..4 {
93///     let ida_clone = Arc::clone(&ida);
94///     let handle = thread::spawn(move || {
95///         ida_clone.alloc()
96///     });
97///     handles.push(handle);
98/// }
99///
100/// for handle in handles {
101///     let id = handle.join().unwrap();
102///     println!("Allocated ID: {:?}", id);
103/// }
104/// ```
105#[derive(Debug)]
106pub struct Ida {
107    root: Mutex<IdaNode>,
108}
109
110#[derive(Debug)]
111struct IdaNode {
112    bitmap: u64,
113    children: BTreeMap<usize, Box<IdaNode>>,
114}
115
116impl IdaNode {
117    pub fn new() -> Self {
118        Self {
119            bitmap: 0,
120            children: BTreeMap::new(),
121        }
122    }
123
124    pub fn alloc(&mut self, level: usize) -> Option<usize> {
125        // CASE: We are at a leaf node
126        // The bitmap here represents individual IDs
127        if level == 0 {
128            // All ones means no free IDs
129            if self.bitmap == u64::MAX {
130                return None;
131            }
132            // Using trailing_ones to find the first zero bit,
133            // which is an unallocated ID
134            let bit = self.bitmap.trailing_ones() as usize;
135            self.bitmap |= 1 << bit;
136            return Some(bit);
137        }
138
139        // CASE: We are at an internal node
140        // The bitmap here represents child nodes. We iterate through the unset bits
141        // (0s), which correspond to children that are not full.
142        while self.bitmap != u64::MAX {
143            let i = self.bitmap.trailing_ones() as usize; // Find index of first 0 bit.
144
145            // The child node is either unallocated or not fully allocated, get it.
146            let child = self
147                .children
148                .entry(i)
149                .or_insert_with(|| Box::new(IdaNode::new()));
150
151            // Recursively allocate in the child node.
152            if let Some(id_in_child) = child.alloc(level - 1) {
153                // After the allocation, check if the child is now fully allocated.
154                // If so, set the corresponding bit in this node's bitmap.
155                if child.bitmap == u64::MAX {
156                    self.bitmap |= 1 << i;
157                }
158                // Compute the full ID by combining the index and the child's ID.
159                let id = (i << (level * IDA_SHIFT)) | id_in_child;
160                return Some(id);
161            } else {
162                // The child was marked as having space in our bitmap, but the recursive
163                // alloc returned None, implying it's actually full. We fix this
164                // inconsistency here and continue the search in the next available child.
165                self.bitmap |= 1 << i;
166            }
167        }
168
169        None
170    }
171
172    pub fn free(&mut self, id: usize, level: usize) {
173        // Determine which bit index to clear at this level
174        let bit_index = (id >> (level * IDA_SHIFT)) & (IDA_BITMAP_BITS - 1);
175
176        // CASE: We are at a leaf node
177        if level == 0 {
178            // Simply clear the bit corresponding to the ID
179            self.bitmap &= !(1 << bit_index);
180            return;
181        }
182
183        // CASE: We are at an internal node
184        // Clear the bit in this node's bitmap,
185        // mark the child as not full or non-existent
186        self.bitmap &= !(1 << bit_index);
187        // Recurse into the appropriate child node
188        // if it exists, clearing the ID there
189        if let Some(child) = self.children.get_mut(&bit_index) {
190            // Recurse into the child node
191            child.free(id, level - 1);
192            // If the child is now empty, remove it to save space
193            if child.bitmap == 0 && child.children.is_empty() {
194                self.children.remove(&bit_index);
195            }
196        }
197    }
198
199    pub fn is_allocated(&self, id: usize, level: usize) -> bool {
200        let bit_index = (id >> (level * IDA_SHIFT)) & (IDA_BITMAP_BITS - 1);
201
202        if level == 0 {
203            return (self.bitmap >> bit_index) & 1 == 1;
204        }
205
206        if let Some(child) = self.children.get(&bit_index) {
207            child.is_allocated(id, level - 1)
208        } else {
209            // If the child node doesn't exist, no IDs in that range can be allocated.
210            false
211        }
212    }
213}
214
215impl Ida {
216    /// Creates a new, empty ID allocator.
217    ///
218    /// The allocator starts with no IDs allocated. The first call to [`alloc`](Self::alloc)
219    /// will return ID `0`.
220    ///
221    /// # Examples
222    ///
223    /// ```
224    /// use ida_rs::Ida;
225    ///
226    /// let ida = Ida::new();
227    /// assert_eq!(ida.alloc(), Some(0));
228    /// ```
229    pub fn new() -> Self {
230        Self {
231            root: Mutex::new(IdaNode::new()),
232        }
233    }
234
235    /// Allocates and returns the next available ID.
236    ///
237    /// This method always returns the lowest available ID. If an ID has been freed,
238    /// it will be reused before allocating higher IDs.
239    ///
240    /// # Returns
241    ///
242    /// - `Some(id)` - The allocated ID (a `usize` value)
243    /// - `None` - If the allocator has exhausted the available ID space
244    ///
245    /// # Thread Safety
246    ///
247    /// This method is thread-safe and can be called concurrently from multiple threads.
248    /// Each call is guaranteed to return a unique ID.
249    ///
250    /// # Examples
251    ///
252    /// ```
253    /// use ida_rs::Ida;
254    ///
255    /// let ida = Ida::new();
256    ///
257    /// // Allocate IDs sequentially
258    /// let id1 = ida.alloc().unwrap();
259    /// let id2 = ida.alloc().unwrap();
260    /// assert_eq!(id1, 0);
261    /// assert_eq!(id2, 1);
262    ///
263    /// // Free an ID and reallocate
264    /// ida.free(id1);
265    /// let id3 = ida.alloc().unwrap();
266    /// assert_eq!(id3, 0); // Reuses the freed ID
267    /// ```
268    pub fn alloc(&self) -> Option<usize> {
269        let mut root = self.root.lock();
270        root.alloc(IDA_MAX_LEVELS - 1)
271    }
272
273    /// Frees a previously allocated ID, making it available for reuse.
274    ///
275    /// Once freed, the ID becomes available for future allocations. The next call
276    /// to [`alloc`](Self::alloc) will prefer reusing freed IDs before allocating
277    /// new ones.
278    ///
279    /// # Parameters
280    ///
281    /// - `id` - The ID to free. Must be a value that was previously returned by
282    ///   [`alloc`](Self::alloc).
283    ///
284    /// # Behavior
285    ///
286    /// - Freeing an already-free ID is safe but has no effect
287    /// - Freeing an ID that was never allocated is safe but has no effect
288    /// - After freeing, the internal tree structure may be compacted to save memory
289    ///
290    /// # Thread Safety
291    ///
292    /// This method is thread-safe and can be called concurrently from multiple threads.
293    ///
294    /// # Examples
295    ///
296    /// ```
297    /// use ida_rs::Ida;
298    ///
299    /// let ida = Ida::new();
300    /// let id = ida.alloc().unwrap();
301    ///
302    /// // Use the ID...
303    ///
304    /// // Free it when done
305    /// ida.free(id);
306    ///
307    /// // The ID can now be reallocated
308    /// let reused_id = ida.alloc().unwrap();
309    /// assert_eq!(id, reused_id);
310    /// ```
311    pub fn free(&self, id: usize) {
312        let mut root = self.root.lock();
313        root.free(id, IDA_MAX_LEVELS - 1);
314    }
315
316    /// Checks if a given ID is currently allocated.
317    ///
318    /// This method queries whether a specific ID has been allocated and not yet freed.
319    /// It's useful for debugging, validation, or tracking ID states.
320    ///
321    /// # Parameters
322    ///
323    /// - `id` - The ID to check
324    ///
325    /// # Returns
326    ///
327    /// - `true` - If the ID is currently allocated
328    /// - `false` - If the ID is free or has never been allocated
329    ///
330    /// # Thread Safety
331    ///
332    /// This method is thread-safe and provides a consistent view at the time of the call.
333    /// Note that in concurrent scenarios, the status may change immediately after this
334    /// method returns.
335    ///
336    /// # Examples
337    ///
338    /// ```
339    /// use ida_rs::Ida;
340    ///
341    /// let ida = Ida::new();
342    ///
343    /// // Initially, no IDs are allocated
344    /// assert!(!ida.is_allocated(0));
345    /// assert!(!ida.is_allocated(100));
346    ///
347    /// // Allocate an ID
348    /// let id = ida.alloc().unwrap();
349    /// assert_eq!(id, 0);
350    ///
351    /// // Check allocation status
352    /// assert!(ida.is_allocated(0));
353    /// assert!(!ida.is_allocated(1));
354    ///
355    /// // Free the ID
356    /// ida.free(0);
357    /// assert!(!ida.is_allocated(0));
358    /// ```
359    pub fn is_allocated(&self, id: usize) -> bool {
360        let root = self.root.lock();
361        root.is_allocated(id, IDA_MAX_LEVELS - 1)
362    }
363}
364
365impl Default for Ida {
366    /// Creates a new ID allocator using the default configuration.
367    ///
368    /// This is equivalent to calling [`Ida::new()`](Self::new).
369    ///
370    /// # Examples
371    ///
372    /// ```
373    /// use ida_rs::Ida;
374    ///
375    /// let ida = Ida::default();
376    /// assert_eq!(ida.alloc(), Some(0));
377    /// ```
378    fn default() -> Self {
379        Self::new()
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386    use alloc::format;
387    use alloc::vec::Vec;
388    use std::sync::{Arc, Mutex};
389    use std::thread;
390
391    #[test]
392    fn test_alloc_and_free_simple() {
393        let ida = Ida::default();
394        assert_eq!(ida.alloc(), Some(0));
395        assert_eq!(ida.alloc(), Some(1));
396        assert_eq!(ida.alloc(), Some(2));
397
398        ida.free(1); // Free an ID in the middle.
399        assert_eq!(ida.alloc(), Some(1)); // Should be reused.
400
401        ida.free(0);
402        ida.free(2);
403        assert_eq!(ida.alloc(), Some(0));
404        assert_eq!(ida.alloc(), Some(2));
405    }
406
407    #[test]
408    fn test_first_level_boundary() {
409        let ida = Ida::default();
410        // Allocate the first 64 IDs to fill the first leaf.
411        for i in 0..64 {
412            assert_eq!(ida.alloc(), Some(i));
413        }
414
415        // The next allocation should cross the boundary into a new leaf.
416        assert_eq!(ida.alloc(), Some(64));
417        assert_eq!(ida.alloc(), Some(65));
418
419        // Free the boundary-crossing ID and ensure it's reused.
420        ida.free(64);
421        assert_eq!(ida.alloc(), Some(64));
422    }
423
424    #[test]
425    fn test_second_level_boundary() {
426        let ida = Ida::default();
427        let count = IDA_BITMAP_BITS * IDA_BITMAP_BITS; // 64 * 64 = 4096
428
429        // Allocate enough IDs to fill an entire first-level child node.
430        for i in 0..count {
431            assert_eq!(ida.alloc(), Some(i));
432        }
433
434        // The next allocation should cross a major boundary.
435        assert_eq!(ida.alloc(), Some(count));
436
437        // Free it and ensure it's reused.
438        ida.free(count);
439        assert_eq!(ida.alloc(), Some(count));
440    }
441
442    #[test]
443    fn test_free_unallocated() {
444        let ida = Ida::default();
445        ida.free(100); // Free an ID that was never allocated.
446        assert_eq!(ida.alloc(), Some(0)); // The first ID should still be 0.
447    }
448
449    #[test]
450    fn test_stress_and_random_free() {
451        let ida = Ida::default();
452        let mut ids = alloc::vec::Vec::new();
453        let count = 10_000;
454
455        // 1. Allocate a large number of IDs.
456        for _ in 0..count {
457            if let Some(id) = ida.alloc() {
458                ids.push(id);
459            } else {
460                panic!("Allocator ran out of space unexpectedly.");
461            }
462        }
463
464        // 2. Free a random subset of those IDs.
465        let mut freed_ids = alloc::vec::Vec::new();
466        for (i, &id) in ids.iter().enumerate() {
467            if i % 3 == 0 || i % 7 == 0 {
468                // Arbitrary condition for freeing
469                ida.free(id);
470                freed_ids.push(id);
471            }
472        }
473
474        // Sort the freed IDs to make reallocation predictable.
475        freed_ids.sort();
476
477        // 3. Allocate new IDs and check if they are the same as the freed ones.
478        for &expected_id in &freed_ids {
479            assert_eq!(ida.alloc(), Some(expected_id));
480        }
481
482        // 4. Free all remaining IDs.
483        for (i, &id) in ids.iter().enumerate() {
484            if !(i % 3 == 0 || i % 7 == 0) {
485                ida.free(id);
486            }
487        }
488        for id in freed_ids {
489            ida.free(id);
490        }
491
492        // 5. Check that the allocator is empty and starts from 0 again.
493        assert_eq!(ida.alloc(), Some(0));
494    }
495
496    #[test]
497    fn test_is_allocated() {
498        let ida = Ida::default();
499        assert!(!ida.is_allocated(0));
500        assert!(!ida.is_allocated(100));
501
502        let id1 = ida.alloc().unwrap();
503        let id2 = ida.alloc().unwrap();
504
505        assert_eq!(id1, 0);
506        assert_eq!(id2, 1);
507
508        assert!(ida.is_allocated(0));
509        assert!(ida.is_allocated(1));
510        assert!(!ida.is_allocated(2));
511        assert!(!ida.is_allocated(100));
512
513        ida.free(1);
514        assert!(ida.is_allocated(0));
515        assert!(!ida.is_allocated(1));
516        assert!(!ida.is_allocated(2));
517    }
518
519    #[test]
520    fn test_debug_output() {
521        let ida = Ida::default();
522        ida.alloc();
523        ida.alloc();
524        // The exact output is not asserted as it's complex and implementation-dependent,
525        // but this test ensures the Debug trait is implemented and doesn't panic.
526        let _ = format!("{ida:?}");
527    }
528
529    #[test]
530    fn test_multi_threaded_alloc() {
531        let ida = Arc::new(Ida::default());
532        let mut handles = Vec::new();
533        let results = Arc::new(Mutex::new(Vec::new()));
534        let num_threads = 4;
535        let ids_per_thread = 1000;
536
537        for _ in 0..num_threads {
538            let ida_clone = Arc::clone(&ida);
539            let results_clone = Arc::clone(&results);
540            let handle = thread::spawn(move || {
541                for _ in 0..ids_per_thread {
542                    if let Some(id) = ida_clone.alloc() {
543                        results_clone.lock().unwrap().push(id);
544                    }
545                }
546            });
547            handles.push(handle);
548        }
549
550        for handle in handles {
551            handle.join().unwrap();
552        }
553
554        let mut final_ids = results.lock().unwrap();
555        assert_eq!(final_ids.len(), num_threads * ids_per_thread);
556
557        final_ids.sort();
558        let original_len = final_ids.len();
559        final_ids.dedup();
560        assert_eq!(
561            original_len,
562            final_ids.len(),
563            "Duplicate IDs were allocated in a multi-threaded context!"
564        );
565    }
566}