oxc_allocator/
allocator.rs

1use std::{
2    alloc::Layout,
3    ptr::{self, NonNull},
4    slice, str,
5};
6
7#[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
8use std::mem::offset_of;
9
10use bumpalo::Bump;
11
12use oxc_data_structures::assert_unchecked;
13
14#[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
15use crate::tracking::AllocationStats;
16
17/// A bump-allocated memory arena.
18///
19/// # Anatomy of an Allocator
20///
21/// [`Allocator`] is flexibly sized. It grows as required as you allocate data into it.
22///
23/// To do that, an [`Allocator`] consists of multiple memory chunks.
24///
25/// [`Allocator::new`] creates a new allocator without any chunks. When you first allocate an object
26/// into it, it will lazily create an initial chunk, the size of which is determined by the size of that
27/// first allocation.
28///
29/// As more data is allocated into the [`Allocator`], it will likely run out of capacity. At that point,
30/// a new memory chunk is added, and further allocations will use this new chunk (until it too runs out
31/// of capacity, and *another* chunk is added).
32///
33/// The data from the 1st chunk is not copied into the 2nd one. It stays where it is, which means
34/// `&` or `&mut` references to data in the first chunk remain valid. This is unlike e.g. `Vec` which
35/// copies all existing data when it grows.
36///
37/// Each chunk is at least double the size of the last one, so growth in capacity is exponential.
38///
39/// [`Allocator::reset`] keeps only the last chunk (the biggest one), and discards any other chunks,
40/// returning their memory to the global allocator. The last chunk has its cursor rewound back to
41/// the start, so it's empty, ready to be re-used for allocating more data.
42///
43/// # Recycling allocators
44///
45/// For good performance, it's ideal to create an [`Allocator`], and re-use it over and over, rather than
46/// repeatedly creating and dropping [`Allocator`]s.
47///
48/// ```
49/// // This is good!
50/// use oxc_allocator::Allocator;
51/// let mut allocator = Allocator::new();
52///
53/// # fn do_stuff(_n: usize, _allocator: &Allocator) {}
54/// for i in 0..100 {
55///     do_stuff(i, &allocator);
56///     // Reset the allocator, freeing the memory used by `do_stuff`
57///     allocator.reset();
58/// }
59/// ```
60///
61/// ```
62/// // DON'T DO THIS!
63/// # use oxc_allocator::Allocator;
64/// # fn do_stuff(_n: usize, _allocator: &Allocator) {}
65/// for i in 0..100 {
66///     let allocator = Allocator::new();
67///     do_stuff(i, &allocator);
68/// }
69/// ```
70///
71/// ```
72/// // DON'T DO THIS EITHER!
73/// # use oxc_allocator::Allocator;
74/// # let allocator = Allocator::new();
75/// # fn do_stuff(_n: usize, _allocator: &Allocator) {}
76/// for i in 0..100 {
77///     do_stuff(i, &allocator);
78///     // We haven't reset the allocator, so we haven't freed the memory used by `do_stuff`.
79///     // The allocator will grow and grow, consuming more and more memory.
80/// }
81/// ```
82///
83/// ## Why is re-using an [`Allocator`] good for performance?
84///
85/// 3 reasons:
86///
87/// #### 1. Avoid expensive system calls
88///
89/// Creating an [`Allocator`] is a fairly expensive operation as it involves a call into global allocator,
90/// which in turn will likely make a system call. Ditto when the [`Allocator`] is dropped.
91/// Re-using an existing [`Allocator`] avoids these costs.
92///
93/// #### 2. CPU cache
94///
95/// Re-using an existing allocator means you're re-using the same block of memory. If that memory was
96/// recently accessed, it's likely to be warm in the CPU cache, so memory accesses will be much faster
97/// than accessing "cold" sections of main memory.
98///
99/// This can have a very significant positive impact on performance.
100///
101/// #### 3. Capacity stabilization
102///
103/// The most efficient [`Allocator`] is one with only 1 chunk which has sufficient capacity for
104/// everything you're going to allocate into it.
105///
106/// Why?
107///
108/// 1. Every allocation will occur without the allocator needing to grow.
109///
110/// 2. This makes the "is there sufficient capacity to allocate this?" check in [`alloc`] completely
111///    predictable (the answer is always "yes"). The CPU's branch predictor swiftly learns this,
112///    speeding up operation.
113///
114/// 3. When the [`Allocator`] is reset, there are no excess chunks to discard, so no system calls.
115///
116/// Because [`reset`] keeps only the biggest chunk (see above), re-using the same [`Allocator`]
117/// for multiple similar workloads will result in the [`Allocator`] swiftly stabilizing at a capacity
118/// which is sufficient to service those workloads with a single chunk.
119///
120/// If workload is completely uniform, it reaches stable state on the 3rd round.
121///
122/// ```
123/// # use oxc_allocator::Allocator;
124/// let mut allocator = Allocator::new();
125///
126/// fn workload(allocator: &Allocator) {
127///     // Allocate 4 MB of data in small chunks
128///     for i in 0..1_000_000u32 {
129///         allocator.alloc(i);
130///     }
131/// }
132///
133/// // 1st round
134/// workload(&allocator);
135///
136/// // `allocator` has capacity for 4 MB data, but split into many chunks.
137/// // `reset` throws away all chunks except the last one which will be approx 2 MB.
138/// allocator.reset();
139///
140/// // 2nd round
141/// workload(&allocator);
142///
143/// // `workload` filled the 2 MB chunk, so a 2nd chunk was created of double the size (4 MB).
144/// // `reset` discards the smaller chunk, leaving only a single 4 MB chunk.
145/// allocator.reset();
146///
147/// // 3rd round
148/// // `allocator` now has sufficient capacity for all allocations in a single 4 MB chunk.
149/// workload(&allocator);
150///
151/// // `reset` has no chunks to discard. It keeps the single 4 MB chunk. No system calls.
152/// allocator.reset();
153///
154/// // More rounds
155/// // All serviced without needing to grow the allocator, and with no system calls.
156/// for _ in 0..100 {
157///   workload(&allocator);
158///   allocator.reset();
159/// }
160/// ```
161///
162/// # No `Drop`s
163///
164/// Objects allocated into Oxc memory arenas are never [`Dropped`](Drop).
165/// Memory is released in bulk when the allocator is dropped, without dropping the individual
166/// objects in the arena.
167///
168/// Therefore, it would produce a memory leak if you allocated [`Drop`] types into the arena
169/// which own memory allocations outside the arena.
170///
171/// Static checks make this impossible to do. [`Allocator::alloc`], [`Box::new_in`], [`Vec::new_in`],
172/// [`HashMap::new_in`], and all other methods which store data in the arena will refuse to compile
173/// if called with a [`Drop`] type.
174///
175/// ```compile_fail
176/// use oxc_allocator::{Allocator, Box};
177/// let allocator = Allocator::new();
178///
179/// struct Foo {
180///     pub a: i32
181/// }
182///
183/// impl std::ops::Drop for Foo {
184///     fn drop(&mut self) {}
185/// }
186///
187/// // This will fail to compile because `Foo` implements `Drop`
188/// let foo = Box::new_in(Foo { a: 0 }, &allocator);
189/// ```
190///
191/// ```compile_fail
192/// use oxc_allocator::{Allocator, Box};
193/// let allocator = Allocator::new();
194///
195/// struct Bar {
196///     v: std::vec::Vec<u8>,
197/// }
198///
199/// // This will fail to compile because `Bar` contains a `std::vec::Vec`, and it implements `Drop`
200/// let bar = Box::new_in(Bar { v: vec![1, 2, 3] }, &allocator);
201/// ```
202///
203/// # Examples
204///
205/// Consumers of the [`oxc` umbrella crate](https://crates.io/crates/oxc) pass
206/// [`Allocator`] references to other tools.
207///
208/// ```ignore
209/// use oxc::{allocator::Allocator, parser::Parser, span::SourceType};
210///
211/// let allocator = Allocator::default();
212/// let parsed = Parser::new(&allocator, "let x = 1;", SourceType::default());
213/// assert!(parsed.errors.is_empty());
214/// ```
215///
216/// [`reset`]: Allocator::reset
217/// [`alloc`]: Allocator::alloc
218/// [`Box::new_in`]: crate::Box::new_in
219/// [`Vec::new_in`]: crate::Vec::new_in
220/// [`HashMap::new_in`]: crate::HashMap::new_in
221#[derive(Default)]
222pub struct Allocator {
223    bump: Bump,
224    /// Used to track number of allocations made in this allocator when `track_allocations` feature is enabled
225    #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
226    pub(crate) stats: AllocationStats,
227}
228
229/// Offset of `stats` field, relative to `bump` field.
230/// Used in `tracking` module for allocation tracking.
231#[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
232#[expect(clippy::cast_possible_wrap)]
233pub const STATS_FIELD_OFFSET: isize =
234    (offset_of!(Allocator, stats) as isize) - (offset_of!(Allocator, bump) as isize);
235
236impl Allocator {
237    /// Create a new [`Allocator`] with no initial capacity.
238    ///
239    /// This method does not reserve any memory to back the allocator. Memory for allocator's initial
240    /// chunk will be reserved lazily, when you make the first allocation into this [`Allocator`]
241    /// (e.g. with [`Allocator::alloc`], [`Box::new_in`], [`Vec::new_in`], [`HashMap::new_in`]).
242    ///
243    /// If you can estimate the amount of memory the allocator will require to fit what you intend to
244    /// allocate into it, it is generally preferable to create that allocator with [`with_capacity`],
245    /// which reserves that amount of memory upfront. This will avoid further system calls to allocate
246    /// further chunks later on. This point is less important if you're re-using the allocator multiple
247    /// times.
248    ///
249    /// See [`Allocator`] docs for more information on efficient use of [`Allocator`].
250    ///
251    /// [`with_capacity`]: Allocator::with_capacity
252    /// [`Box::new_in`]: crate::Box::new_in
253    /// [`Vec::new_in`]: crate::Vec::new_in
254    /// [`HashMap::new_in`]: crate::HashMap::new_in
255    //
256    // `#[inline(always)]` because just delegates to `bumpalo` method
257    #[expect(clippy::inline_always)]
258    #[inline(always)]
259    pub fn new() -> Self {
260        Self {
261            bump: Bump::new(),
262            #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
263            stats: AllocationStats::default(),
264        }
265    }
266
267    /// Create a new [`Allocator`] with specified capacity.
268    ///
269    /// See [`Allocator`] docs for more information on efficient use of [`Allocator`].
270    //
271    // `#[inline(always)]` because just delegates to `bumpalo` method
272    #[expect(clippy::inline_always)]
273    #[inline(always)]
274    pub fn with_capacity(capacity: usize) -> Self {
275        Self {
276            bump: Bump::with_capacity(capacity),
277            #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
278            stats: AllocationStats::default(),
279        }
280    }
281
282    /// Allocate an object in this [`Allocator`] and return an exclusive reference to it.
283    ///
284    /// # Panics
285    /// Panics if reserving space for `T` fails.
286    ///
287    /// # Examples
288    /// ```
289    /// use oxc_allocator::Allocator;
290    ///
291    /// let allocator = Allocator::default();
292    /// let x = allocator.alloc([1u8; 20]);
293    /// assert_eq!(x, &[1u8; 20]);
294    /// ```
295    //
296    // `#[inline(always)]` because this is a very hot path and `Bump::alloc` is a very small function.
297    // We always want it to be inlined.
298    #[expect(clippy::inline_always)]
299    #[inline(always)]
300    pub fn alloc<T>(&self, val: T) -> &mut T {
301        const { assert!(!std::mem::needs_drop::<T>(), "Cannot allocate Drop type in arena") };
302
303        #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
304        self.stats.record_allocation();
305
306        self.bump.alloc(val)
307    }
308
309    /// Copy a string slice into this [`Allocator`] and return a reference to it.
310    ///
311    /// # Panics
312    /// Panics if reserving space for the string fails.
313    ///
314    /// # Examples
315    /// ```
316    /// use oxc_allocator::Allocator;
317    /// let allocator = Allocator::default();
318    /// let hello = allocator.alloc_str("hello world");
319    /// assert_eq!(hello, "hello world");
320    /// ```
321    //
322    // `#[inline(always)]` because this is a hot path and `Bump::alloc_str` is a very small function.
323    // We always want it to be inlined.
324    #[expect(clippy::inline_always)]
325    #[inline(always)]
326    pub fn alloc_str<'alloc>(&'alloc self, src: &str) -> &'alloc str {
327        #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
328        self.stats.record_allocation();
329
330        self.bump.alloc_str(src)
331    }
332
333    /// `Copy` a slice into this `Bump` and return an exclusive reference to the copy.
334    ///
335    /// # Panics
336    /// Panics if reserving space for the slice fails.
337    ///
338    /// # Examples
339    /// ```
340    /// use oxc_allocator::Allocator;
341    /// let allocator = Allocator::default();
342    /// let x = allocator.alloc_slice_copy(&[1, 2, 3]);
343    /// assert_eq!(x, &[1, 2, 3]);
344    /// ```
345    // `#[inline(always)]` because this is a hot path and `Bump::alloc_slice_copy` is a very small function.
346    // We always want it to be inlined.
347    #[expect(clippy::inline_always)]
348    #[inline(always)]
349    pub fn alloc_slice_copy<T: Copy>(&self, src: &[T]) -> &mut [T] {
350        #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
351        self.stats.record_allocation();
352
353        self.bump.alloc_slice_copy(src)
354    }
355
356    /// Allocate space for an object with the given [`Layout`].
357    ///
358    /// The returned pointer points at uninitialized memory, and should be initialized with
359    /// [`std::ptr::write`].
360    ///
361    /// # Panics
362    ///
363    /// Panics if reserving space matching `layout` fails.
364    pub fn alloc_layout(&self, layout: Layout) -> NonNull<u8> {
365        #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
366        self.stats.record_allocation();
367
368        self.bump.alloc_layout(layout)
369    }
370
371    /// Create new `&str` from a fixed-size array of `&str`s concatenated together,
372    /// allocated in the given `allocator`.
373    ///
374    /// # Panics
375    ///
376    /// Panics if the sum of length of all strings exceeds `isize::MAX`.
377    ///
378    /// # Example
379    /// ```
380    /// use oxc_allocator::Allocator;
381    ///
382    /// let allocator = Allocator::new();
383    /// let s = allocator.alloc_concat_strs_array(["hello", " ", "world", "!"]);
384    /// assert_eq!(s, "hello world!");
385    /// ```
386    // `#[inline(always)]` because want compiler to be able to remove checked addition where some of
387    // `strings` are statically known.
388    #[expect(clippy::inline_always)]
389    #[inline(always)]
390    pub fn alloc_concat_strs_array<'a, const N: usize>(&'a self, strings: [&str; N]) -> &'a str {
391        // Calculate total length of all the strings concatenated.
392        //
393        // We have to use `checked_add` here to guard against additions wrapping around
394        // if some of the input `&str`s are very long, or there's many of them.
395        //
396        // However, `&str`s have max length of `isize::MAX`.
397        // https://users.rust-lang.org/t/does-str-reliably-have-length-isize-max/126777
398        // Use `assert_unchecked!` to communicate this invariant to compiler, which allows it to
399        // optimize out the overflow checks where some of `strings` are static, so their size is known.
400        //
401        // e.g. `allocator.from_strs_array_in(["__vite_ssr_import_", str, "__"])`, for example,
402        // requires no checks at all, because the static parts have total length of 20 bytes,
403        // and `str` has max length of `isize::MAX`. `isize::MAX as usize + 20` cannot overflow `usize`.
404        // Compiler can see that, and removes the overflow check.
405        // https://godbolt.org/z/MGh44Yz5d
406        #[expect(clippy::checked_conversions)]
407        let total_len = strings.iter().fold(0usize, |total_len, s| {
408            let len = s.len();
409            // SAFETY: `&str`s have maximum length of `isize::MAX`
410            unsafe { assert_unchecked!(len <= (isize::MAX as usize)) };
411            total_len.checked_add(len).unwrap()
412        });
413        assert!(
414            isize::try_from(total_len).is_ok(),
415            "attempted to create a string longer than `isize::MAX` bytes"
416        );
417
418        #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
419        self.stats.record_allocation();
420
421        // Create actual `&str` in a separate function, to ensure that `alloc_concat_strs_array`
422        // is inlined, so that compiler has knowledge to remove the overflow checks above.
423        // When some of `strings` are static, this function is usually only a few instructions.
424        // Compiler can choose whether or not to inline `alloc_concat_strs_array_with_total_len_in`.
425        // SAFETY: `total_len` has been calculated correctly above.
426        // `total_len` is `<= isize::MAX`.
427        unsafe { self.alloc_concat_strs_array_with_total_len_in(strings, total_len) }
428    }
429
430    /// Create a new `&str` from a fixed-size array of `&str`s concatenated together,
431    /// allocated in the given `allocator`, with provided `total_len`.
432    ///
433    /// # SAFETY
434    /// * `total_len` must be the total length of all `strings` concatenated.
435    /// * `total_len` must be `<= isize::MAX`.
436    unsafe fn alloc_concat_strs_array_with_total_len_in<'a, const N: usize>(
437        &'a self,
438        strings: [&str; N],
439        total_len: usize,
440    ) -> &'a str {
441        if total_len == 0 {
442            return "";
443        }
444
445        // Allocate `total_len` bytes.
446        // SAFETY: Caller guarantees `total_len <= isize::MAX`.
447        let layout = unsafe { Layout::from_size_align_unchecked(total_len, 1) };
448        let start_ptr = self.bump().alloc_layout(layout);
449
450        let mut end_ptr = start_ptr;
451        for str in strings {
452            let src_ptr = str.as_ptr();
453            let len = str.len();
454
455            // SAFETY:
456            // `src` is obtained from a `&str` with length `len`, so is valid for reading `len` bytes.
457            // `end_ptr` is within bounds of the allocation. So is `end_ptr + len`.
458            // `u8` has no alignment requirements, so `src_ptr` and `end_ptr` are sufficiently aligned.
459            // No overlapping, because we're copying from an existing `&str` to a newly allocated buffer.
460            unsafe { ptr::copy_nonoverlapping(src_ptr, end_ptr.as_ptr(), len) };
461
462            // SAFETY: We allocated sufficient capacity for all the strings concatenated.
463            // So `end_ptr.add(len)` cannot go out of bounds.
464            end_ptr = unsafe { end_ptr.add(len) };
465        }
466
467        debug_assert_eq!(end_ptr.as_ptr() as usize - start_ptr.as_ptr() as usize, total_len);
468
469        // SAFETY: We have allocated and filled `total_len` bytes starting at `start_ptr`.
470        // Concatenating multiple `&str`s results in a valid UTF-8 string.
471        unsafe {
472            let slice = slice::from_raw_parts(start_ptr.as_ptr(), total_len);
473            str::from_utf8_unchecked(slice)
474        }
475    }
476
477    /// Reset this allocator.
478    ///
479    /// Performs mass deallocation on everything allocated in this arena by resetting the pointer
480    /// into the underlying chunk of memory to the start of the chunk.
481    /// Does not run any `Drop` implementations on deallocated objects.
482    ///
483    /// If this arena has allocated multiple chunks to bump allocate into, then the excess chunks
484    /// are returned to the global allocator.
485    ///
486    /// # Examples
487    /// ```
488    /// use oxc_allocator::Allocator;
489    ///
490    /// let mut allocator = Allocator::default();
491    ///
492    /// // Allocate a bunch of things.
493    /// {
494    ///     for i in 0..100 {
495    ///         allocator.alloc(i);
496    ///     }
497    /// }
498    ///
499    /// // Reset the arena.
500    /// allocator.reset();
501    ///
502    /// // Allocate some new things in the space previously occupied by the
503    /// // original things.
504    /// for j in 200..400 {
505    ///     allocator.alloc(j);
506    /// }
507    /// ```
508    //
509    // `#[inline(always)]` because it just delegates to `bumpalo`
510    #[expect(clippy::inline_always)]
511    #[inline(always)]
512    pub fn reset(&mut self) {
513        #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
514        self.stats.reset();
515
516        self.bump.reset();
517    }
518
519    /// Calculate the total capacity of this [`Allocator`] including all chunks, in bytes.
520    ///
521    /// Note: This is the total amount of memory the [`Allocator`] owns NOT the total size of data
522    /// that's been allocated in it. If you want the latter, use [`used_bytes`] instead.
523    ///
524    /// # Examples
525    /// ```
526    /// use oxc_allocator::Allocator;
527    ///
528    /// let capacity = 64 * 1024; // 64 KiB
529    /// let mut allocator = Allocator::with_capacity(capacity);
530    /// allocator.alloc(123u64); // 8 bytes
531    ///
532    /// // Result is the capacity (64 KiB), not the size of allocated data (8 bytes).
533    /// // `Allocator::with_capacity` may allocate a bit more than requested.
534    /// assert!(allocator.capacity() >= capacity);
535    /// ```
536    ///
537    /// [`used_bytes`]: Allocator::used_bytes
538    //
539    // `#[inline(always)]` because it just delegates to `bumpalo`
540    #[expect(clippy::inline_always)]
541    #[inline(always)]
542    pub fn capacity(&self) -> usize {
543        self.bump.allocated_bytes()
544    }
545
546    /// Calculate the total size of data used in this [`Allocator`], in bytes.
547    ///
548    /// This is the total amount of memory that has been *used* in the [`Allocator`], NOT the amount of
549    /// memory the [`Allocator`] owns. If you want the latter, use [`capacity`] instead.
550    ///
551    /// The result includes:
552    ///
553    /// 1. Padding bytes between objects which have been allocated to preserve alignment of types
554    ///    where they have different alignments or have larger-than-typical alignment.
555    /// 2. Excess capacity in [`Vec`]s, [`StringBuilder`]s and [`HashMap`]s.
556    /// 3. Objects which were allocated but later dropped. [`Allocator`] does not re-use allocations,
557    ///    so anything which is allocated into arena continues to take up "dead space", even after it's
558    ///    no longer referenced anywhere.
559    /// 4. "Dead space" left over where a [`Vec`], [`StringBuilder`] or [`HashMap`] has grown and had to
560    ///    make a new allocation to accommodate its new larger size. Its old allocation continues to
561    ///    take up "dead" space in the allocator, unless it was the most recent allocation.
562    ///
563    /// In practice, this almost always means that the result returned from this function will be an
564    /// over-estimate vs the amount of "live" data in the arena.
565    ///
566    /// However, if you are using the result of this method to create a new `Allocator` to clone
567    /// an AST into, it is theoretically possible (though very unlikely) that it may be a slight
568    /// under-estimate of the capacity required in new allocator to clone the AST into, depending
569    /// on the order that `&str`s were allocated into arena in parser vs the order they get allocated
570    /// during cloning. The order allocations are made in affects the amount of padding bytes required.
571    ///
572    /// # Examples
573    /// ```
574    /// use oxc_allocator::{Allocator, Vec};
575    ///
576    /// let capacity = 64 * 1024; // 64 KiB
577    /// let mut allocator = Allocator::with_capacity(capacity);
578    ///
579    /// allocator.alloc(1u8); // 1 byte with alignment 1
580    /// allocator.alloc(2u8); // 1 byte with alignment 1
581    /// allocator.alloc(3u64); // 8 bytes with alignment 8
582    ///
583    /// // Only 10 bytes were allocated, but 16 bytes were used, in order to align `3u64` on 8
584    /// assert_eq!(allocator.used_bytes(), 16);
585    ///
586    /// allocator.reset();
587    ///
588    /// let mut vec = Vec::<u64>::with_capacity_in(2, &allocator);
589    ///
590    /// // Allocate something else, so `vec`'s allocation is not the most recent
591    /// allocator.alloc(123u64);
592    ///
593    /// // `vec` has to grow beyond it's initial capacity
594    /// vec.extend([1, 2, 3, 4]);
595    ///
596    /// // `vec` takes up 32 bytes, and `123u64` takes up 8 bytes = 40 total.
597    /// // But there's an additional 16 bytes consumed for `vec`'s original capacity of 2,
598    /// // which is still using up space
599    /// assert_eq!(allocator.used_bytes(), 56);
600    /// ```
601    ///
602    /// [`capacity`]: Allocator::capacity
603    /// [`Vec`]: crate::Vec
604    /// [`StringBuilder`]: crate::StringBuilder
605    /// [`HashMap`]: crate::HashMap
606    pub fn used_bytes(&self) -> usize {
607        let mut bytes = 0;
608        // SAFETY: No allocations are made while `chunks_iter` is alive. No data is read from the chunks.
609        let chunks_iter = unsafe { self.bump.iter_allocated_chunks_raw() };
610        for (_, size) in chunks_iter {
611            bytes += size;
612        }
613        bytes
614    }
615
616    /// Get inner [`bumpalo::Bump`].
617    ///
618    /// This method is not public. We don't want to expose `Bump` to user.
619    /// The fact that we're using `bumpalo` is an internal implementation detail.
620    //
621    // `#[inline(always)]` because it's a no-op
622    #[expect(clippy::inline_always)]
623    #[inline(always)]
624    pub(crate) fn bump(&self) -> &Bump {
625        &self.bump
626    }
627
628    /// Create [`Allocator`] from a [`bumpalo::Bump`].
629    ///
630    /// This method is not public. Only used by [`Allocator::from_raw_parts`].
631    //
632    // `#[inline(always)]` because it's a no-op
633    #[cfg(feature = "from_raw_parts")]
634    #[expect(clippy::inline_always)]
635    #[inline(always)]
636    pub(crate) fn from_bump(bump: Bump) -> Self {
637        Self {
638            bump,
639            #[cfg(all(feature = "track_allocations", not(feature = "disable_track_allocations")))]
640            stats: AllocationStats::default(),
641        }
642    }
643}
644
645#[cfg(test)]
646mod test {
647    use super::Allocator;
648
649    #[test]
650    fn test_api() {
651        let mut allocator = Allocator::default();
652        {
653            let array = allocator.alloc([123; 10]);
654            assert_eq!(array, &[123; 10]);
655            let str = allocator.alloc_str("hello");
656            assert_eq!(str, "hello");
657        }
658        allocator.reset();
659    }
660
661    #[test]
662    fn string_from_array_len_1() {
663        let allocator = Allocator::default();
664        let s = allocator.alloc_concat_strs_array(["hello"]);
665        assert_eq!(s, "hello");
666    }
667
668    #[test]
669    fn string_from_array_len_2() {
670        let allocator = Allocator::default();
671        let s = allocator.alloc_concat_strs_array(["hello", "world!"]);
672        assert_eq!(s, "helloworld!");
673    }
674
675    #[test]
676    fn string_from_array_len_3() {
677        let hello = "hello";
678        let world = std::string::String::from("world");
679        let allocator = Allocator::default();
680        let s = allocator.alloc_concat_strs_array([hello, &world, "!"]);
681        assert_eq!(s, "helloworld!");
682    }
683
684    #[test]
685    fn string_from_empty_array() {
686        let allocator = Allocator::default();
687        let s = allocator.alloc_concat_strs_array([]);
688        assert_eq!(s, "");
689    }
690
691    #[test]
692    fn string_from_array_of_empty_strs() {
693        let allocator = Allocator::default();
694        let s = allocator.alloc_concat_strs_array(["", "", ""]);
695        assert_eq!(s, "");
696    }
697
698    #[test]
699    fn string_from_array_containing_some_empty_strs() {
700        let allocator = Allocator::default();
701        let s = allocator.alloc_concat_strs_array(["", "hello", ""]);
702        assert_eq!(s, "hello");
703    }
704}