Skip to main content

bump_scope/
bumping.rs

1#![forbid(unsafe_code)]
2#![expect(clippy::needless_pass_by_value, clippy::cast_possible_wrap)]
3//! This file intentionally doesn't import anything other than `core`
4//! to make it easy to fuzz and debug.
5
6use core::{alloc::Layout, num::NonZeroUsize, ops::Range};
7
8#[cold]
9#[inline(always)]
10pub(crate) fn cold() {}
11
12#[inline(always)]
13pub(crate) fn unlikely(condition: bool) -> bool {
14    if condition {
15        cold();
16    } else {
17        // ...
18    }
19
20    condition
21}
22
23pub(crate) const MIN_CHUNK_ALIGN: usize = 16;
24
25macro_rules! debug_assert_aligned {
26    ($addr:expr, $align:expr) => {
27        let addr = $addr;
28        let align = $align;
29
30        debug_assert!(align.is_power_of_two());
31        let is_aligned = addr & (align - 1) == 0;
32
33        debug_assert!(
34            is_aligned,
35            "expected `{}` ({}) to be aligned to `{}` ({})",
36            stringify!($addr),
37            addr,
38            stringify!($align),
39            align,
40        );
41    };
42}
43
44macro_rules! debug_assert_ge {
45    ($lhs:expr, $rhs:expr) => {
46        let lhs = $lhs;
47        let rhs = $rhs;
48
49        debug_assert!(
50            lhs >= rhs,
51            "expected `{}` ({}) to be greater or equal to `{}` ({})",
52            stringify!($lhs),
53            lhs,
54            stringify!($rhs),
55            rhs,
56        )
57    };
58}
59
60macro_rules! debug_assert_le {
61    ($lhs:expr, $rhs:expr) => {
62        let lhs = $lhs;
63        let rhs = $rhs;
64
65        debug_assert!(
66            lhs <= rhs,
67            "expected `{}` ({}) to be less than or equal to `{}` ({})",
68            stringify!($lhs),
69            lhs,
70            stringify!($rhs),
71            rhs,
72        )
73    };
74}
75
76/// Arguments for [`bump_up`], [`bump_down`], [`bump_prepare_up`] and [`bump_prepare_down`].
77#[derive(Debug)]
78pub(crate) struct BumpProps {
79    /// The start of the remaining free allocation region.
80    ///
81    /// This must not be zero.
82    pub(crate) start: usize,
83
84    /// The end of the remaining free allocation region.
85    ///
86    /// This must not be zero.
87    pub(crate) end: usize,
88
89    /// The minimum alignment of the bump allocator.
90    ///
91    /// This must be less or equal to [`MIN_CHUNK_ALIGN`].
92    pub(crate) min_align: usize,
93
94    /// The allocation layout.
95    pub(crate) layout: Layout,
96
97    /// Whether the allocation layout's alignment is known at compile time.
98    ///
99    /// This is an optimization hint. `false` is always valid.
100    pub(crate) align_is_const: bool,
101
102    /// Whether the allocation layout's size is known at compile time.
103    ///
104    /// This is an optimization hint. `false` is always valid.
105    pub(crate) size_is_const: bool,
106
107    /// Whether the allocation layout's size is a multiple of its alignment and that is known at compile time.
108    ///
109    /// This is an optimization hint. `false` is always valid.
110    ///
111    /// This must only be true if `layout.size() % layout.align()` is also true.
112    pub(crate) size_is_multiple_of_align: bool,
113}
114
115impl BumpProps {
116    #[inline(always)]
117    fn debug_assert_valid(&self, up: bool) {
118        debug_assert_ne!(self.start, 0);
119        debug_assert_ne!(self.end, 0);
120
121        debug_assert!(self.min_align.is_power_of_two());
122        debug_assert!(self.min_align <= MIN_CHUNK_ALIGN);
123
124        if self.size_is_multiple_of_align {
125            debug_assert_eq!(self.layout.size() % self.layout.align(), 0);
126        }
127
128        let is_dummy_chunk = self.start > self.end;
129
130        if is_dummy_chunk {
131            debug_assert_eq!(self.start, self.end + 16);
132            debug_assert_aligned!(self.start, MIN_CHUNK_ALIGN);
133            debug_assert_aligned!(self.end, MIN_CHUNK_ALIGN);
134        } else {
135            debug_assert_le!(self.start, self.end);
136
137            debug_assert!({
138                const MAX: i128 = isize::MAX as i128;
139                let size = self.end as i128 - self.start as i128;
140                (0..=MAX).contains(&size)
141            });
142
143            if up {
144                debug_assert_aligned!(self.start, self.min_align);
145                debug_assert_aligned!(self.end, MIN_CHUNK_ALIGN);
146            } else {
147                debug_assert_aligned!(self.start, MIN_CHUNK_ALIGN);
148                debug_assert_aligned!(self.end, self.min_align);
149            }
150        }
151    }
152}
153
154/// A successful upwards bump allocation. Returned from [`bump_up`].
155#[derive(Debug)]
156pub(crate) struct BumpUp {
157    /// The new position of the bump allocator (the next [`BumpProps::start`]).
158    pub(crate) new_pos: usize,
159
160    /// The address of the allocation's pointer.
161    pub(crate) ptr: usize,
162}
163
164/// Does upwards bump allocation.
165///
166/// - `end` must be a multiple of [`MIN_CHUNK_ALIGN`]
167#[inline(always)]
168pub(crate) fn bump_up(props: BumpProps) -> Option<BumpUp> {
169    props.debug_assert_valid(true);
170
171    let BumpProps {
172        mut start,
173        end,
174        layout,
175        min_align,
176        align_is_const,
177        size_is_const,
178        size_is_multiple_of_align,
179    } = props;
180
181    // Used for assertion at the end of the function.
182    let original_start = start;
183
184    let mut new_pos;
185
186    if align_is_const && layout.align() <= MIN_CHUNK_ALIGN {
187        // Constant, small alignment fast path!
188
189        if layout.align() <= min_align {
190            // Alignment is already sufficient.
191        } else {
192            // REGULAR_CHUNK: Aligning an address that is `<= range.end` with an alignment `<= MIN_CHUNK_ALIGN`
193            // can't exceed `range.end` nor overflow as `range.end` is always aligned to `MIN_CHUNK_ALIGN`.
194            //
195            // DUMMY_CHUNK: `start` is always aligned to `MIN_CHUNK_ALIGN` so this does nothing.
196            start = up_align_unchecked(start, layout.align());
197        }
198
199        if size_is_const && layout.size() < MIN_CHUNK_ALIGN {
200            // When `size < MIN_CHUNK_ALIGN` adding it to `start` can't overflow,
201            // as the highest value for `start` would be `end` which is aligned to `MIN_CHUNK_ALIGN`,
202            // making it `< (usize::MAX - (MIN_CHUNK_ALIGN - 1))`.
203            new_pos = start + layout.size();
204
205            if unlikely(end < new_pos) {
206                return None;
207            }
208        } else {
209            // REGULAR_CHUNK: `start` and `end` must be part of the same allocated object.
210            // Allocated objects can't have a size greater than `isize::MAX`, so this doesn't overflow.
211            //
212            // DUMMY_CHUNK: `end - start` will always return `-16`
213            let remaining = end.wrapping_sub(start) as isize;
214
215            if unlikely(layout.size() as isize > remaining) {
216                return None;
217            }
218
219            // Doesn't exceed `end` because of the check above.
220            new_pos = start + layout.size();
221        }
222    } else {
223        // Alignment is `> MIN_CHUNK_ALIGN` or not const.
224
225        // `start` and `align` are both nonzero.
226        // `aligned_down` is the aligned pointer minus `layout.align()`.
227        let aligned_down = (start - 1) & !(layout.align() - 1);
228
229        // `align + size` cannot overflow as per `Layout`'s rules.
230        //
231        // This could also be a `checked_add`, but we use `saturating_add` to save us a branch.
232        // The `if` below will return None if the addition saturated and returned `usize::MAX`.
233        new_pos = aligned_down.saturating_add(layout.align() + layout.size());
234
235        // Note that `new_pos` being `usize::MAX` is an invalid value for `new_pos` and we MUST return None.
236        // Due to `end` being always aligned to `MIN_CHUNK_ALIGN`, it can't be `usize::MAX`.
237        // Thus when `new_pos` is `usize::MAX` this will always return None.
238        if unlikely(new_pos > end) {
239            return None;
240        }
241
242        // Doesn't exceed `end` because `aligned_down + align + size` didn't.
243        start = aligned_down + layout.align();
244    }
245
246    if (align_is_const && size_is_multiple_of_align && layout.align() >= min_align)
247        || (size_is_const && (layout.size() % min_align == 0))
248    {
249        // We are already aligned to `min_align`.
250    } else {
251        // Up aligning an address `<= range.end` with an alignment `<= MIN_CHUNK_ALIGN` (which `min_align` is)
252        // can't exceed `range.end` and thus also can't overflow.
253        new_pos = up_align_unchecked(new_pos, min_align);
254    }
255
256    debug_assert_aligned!(start, layout.align());
257    debug_assert_aligned!(start, min_align);
258    debug_assert_aligned!(new_pos, min_align);
259    debug_assert_ne!(new_pos, 0);
260    debug_assert_ne!(start, 0);
261    debug_assert_le!(start, end);
262    debug_assert_ge!(start, original_start);
263    debug_assert_ge!(new_pos - start, layout.size());
264    debug_assert_le!(new_pos, end);
265    debug_assert_ge!(new_pos, start);
266
267    Some(BumpUp { new_pos, ptr: start })
268}
269
270/// Does downwards bump allocation.
271#[inline(always)]
272pub(crate) fn bump_down(props: BumpProps) -> Option<usize> {
273    props.debug_assert_valid(false);
274
275    let BumpProps {
276        start,
277        mut end,
278        layout,
279        min_align,
280        align_is_const,
281        size_is_const,
282        size_is_multiple_of_align,
283    } = props;
284
285    // Used for assertions only.
286    let original_end = end;
287
288    // This variables is meant to be computed at compile time.
289    let needs_aligning = {
290        // The bump pointer must end up aligned to the layout's alignment.
291        //
292        // Manual alignment for the layout's alignment can be elided
293        // if the layout's size is a multiple of its alignment
294        // and its alignment is less or equal to the minimum alignment.
295        let can_elide_aligning_for_layout = size_is_multiple_of_align && align_is_const && layout.align() <= min_align;
296
297        // The bump pointer must end up aligned to the minimum alignment again.
298        //
299        // Manual alignment for the minimum alignment can be elided if:
300        // - the layout's size is a multiple of its alignment
301        //   and its alignment is greater or equal to the minimum alignment
302        // - the layout's size is a multiple of the minimum alignment
303        //
304        // In either case the bump pointer will end up inherently aligned when the pointer
305        // is bumped downwards by the layout's size.
306        let can_elide_aligning_for_min_align = {
307            let due_to_layout_align = size_is_multiple_of_align && align_is_const && layout.align() >= min_align;
308            let due_to_layout_size = size_is_const && (layout.size() % min_align == 0);
309            due_to_layout_align || due_to_layout_size
310        };
311
312        let can_elide_aligning = can_elide_aligning_for_layout && can_elide_aligning_for_min_align;
313
314        !can_elide_aligning
315    };
316
317    if size_is_const && layout.size() <= MIN_CHUNK_ALIGN {
318        // When `size <= MIN_CHUNK_ALIGN` subtracting it from `end` can't overflow, as the lowest value for `end` would be `start` which is aligned to `MIN_CHUNK_ALIGN`,
319        // thus its address can't be smaller than it.
320        end -= layout.size();
321
322        if needs_aligning {
323            // At this point layout's align is const, because we assume `size_is_const` implies `align_is_const`.
324            // That means `max` is evaluated at compile time, so we don't bother having different cases for either alignment.
325            end = down_align(end, layout.align().max(min_align));
326        }
327
328        if unlikely(end < start) {
329            return None;
330        }
331    } else if align_is_const && layout.align() <= MIN_CHUNK_ALIGN {
332        // Constant, small alignment fast path!
333
334        // REGULAR_CHUNK: `start` and `end` must be part of the same allocated object.
335        // Allocated objects can't have a size greater than `isize::MAX`, so this doesn't overflow.
336        //
337        // DUMMY_CHUNK: `end - start` will always return `-16`
338        let remaining = end.wrapping_sub(start) as isize;
339
340        if unlikely(layout.size() as isize > remaining) {
341            return None;
342        }
343
344        // Doesn't overflow because of the check above.
345        end -= layout.size();
346
347        if needs_aligning {
348            // Down aligning an address `>= range.start` with an alignment `<= MIN_CHUNK_ALIGN` (which `layout.align()` is)
349            // can't exceed `range.start`, and thus also can't overflow.
350            end = down_align(end, layout.align().max(min_align));
351        }
352    } else {
353        // Alignment is `> MIN_CHUNK_ALIGN` or not const.
354
355        // This could also be a `checked_sub`, but we use `saturating_sub` to save us a branch.
356        // The `if` below will return None if the subtraction saturated and returned `0`.
357        end = end.saturating_sub(layout.size());
358        end = down_align(end, layout.align().max(min_align));
359
360        // Note that `end` being `0` is an invalid value for `end` and we MUST return None.
361        // Due to `start` being `NonNull`, it can't be `0`.
362        // Thus when `end` is `0` this will always return None.
363        if unlikely(end < start) {
364            return None;
365        }
366    }
367
368    debug_assert_aligned!(end, layout.align());
369    debug_assert_aligned!(end, min_align);
370    debug_assert_ne!(end, 0);
371    debug_assert_ge!(end, start);
372    debug_assert_ge!(original_end - end, layout.size());
373
374    Some(end)
375}
376
377/// Prepares a slice allocation by returning the start and end address for a maximally sized region
378/// where both start and end are aligned to `layout.align()`.
379///
380/// - `end` must be a multiple of [`MIN_CHUNK_ALIGN`]
381#[inline(always)]
382pub(crate) fn bump_prepare_up(props: BumpProps) -> Option<Range<usize>> {
383    props.debug_assert_valid(true);
384
385    let BumpProps {
386        mut start,
387        end,
388        layout,
389        min_align,
390        align_is_const,
391        size_is_const: _,
392        size_is_multiple_of_align: _,
393    } = props;
394
395    if align_is_const && layout.align() <= min_align {
396        // Alignment is already sufficient.
397    } else {
398        // `start` needs to be aligned.
399        if align_is_const && layout.align() <= MIN_CHUNK_ALIGN {
400            // Aligning an address that is `<= range.end` with an alignment
401            // that is `<= MIN_CHUNK_ALIGN` can't exceed `range.end` and
402            // thus can't overflow.
403            start = up_align_unchecked(start, layout.align());
404        } else {
405            start = if let Some(some) = up_align(start, layout.align()) {
406                some.get()
407            } else {
408                cold();
409                return None;
410            };
411
412            if unlikely(start > end) {
413                return None;
414            }
415        }
416    }
417
418    let remaining = end.wrapping_sub(start) as isize;
419
420    if unlikely(layout.size() as isize > remaining) {
421        return None;
422    }
423
424    // Layout fits, we just trim off the excess to make end aligned.
425    //
426    // Note that `end` does *not* need to be aligned to `min_align` because the `prepare` operation doesn't
427    // move the bump pointer which is the thing that does need to be aligned.
428    //
429    // Aligning to `min_align` will happen once `use_prepared_slice_allocation` is called.
430    let end = down_align(end, layout.align());
431
432    debug_assert_aligned!(start, layout.align());
433    debug_assert_aligned!(end, layout.align());
434    debug_assert_ne!(start, 0);
435    debug_assert_ne!(end, 0);
436
437    Some(start..end)
438}
439
440/// Prepares a slice allocation by returning the start and end address for a maximally sized region
441/// where both start and end are aligned to `layout.align()`.
442#[inline(always)]
443pub(crate) fn bump_prepare_down(props: BumpProps) -> Option<Range<usize>> {
444    props.debug_assert_valid(false);
445
446    let BumpProps {
447        start,
448        mut end,
449        layout,
450        min_align,
451        align_is_const,
452        size_is_const: _,
453        size_is_multiple_of_align: _,
454    } = props;
455
456    if align_is_const && layout.align() <= min_align {
457        // Alignment is already sufficient.
458    } else {
459        end = down_align(end, layout.align());
460
461        if align_is_const && layout.align() <= MIN_CHUNK_ALIGN {
462            // End is valid.
463        } else {
464            // End could be less than start at this point.
465            if unlikely(end < start) {
466                return None;
467            }
468        }
469    }
470
471    // REGULAR_CHUNK: `start` and `end` must be part of the same allocated object.
472    // Allocated objects can't have a size greater than `isize::MAX`, so this doesn't overflow.
473    //
474    // DUMMY_CHUNK: `end - start` will always return `-16`
475    let remaining = end.wrapping_sub(start) as isize;
476
477    if unlikely(layout.size() as isize > remaining) {
478        return None;
479    }
480
481    // Layout fits, we just trim off the excess to make start aligned.
482    //
483    // Note that `start` doesn't need to be aligned to `min_align` because the `prepare` operation doesn't
484    // move the bump pointer which is the thing that needs to be aligned.
485    //
486    // Aligning to `min_align` will happen once `use_prepared_slice_allocation` is called.
487    let start = up_align_unchecked(start, layout.align());
488
489    debug_assert_aligned!(start, layout.align());
490    debug_assert_aligned!(end, layout.align());
491    debug_assert_ne!(start, 0);
492    debug_assert_ne!(end, 0);
493
494    Some(start..end)
495}
496
497#[inline(always)]
498const fn down_align(addr: usize, align: usize) -> usize {
499    debug_assert!(align.is_power_of_two());
500    let mask = align - 1;
501    addr & !mask
502}
503
504/// Doesn't check for overflow.
505#[inline(always)]
506const fn up_align_unchecked(addr: usize, align: usize) -> usize {
507    debug_assert!(align.is_power_of_two());
508    let mask = align - 1;
509    (addr + mask) & !mask
510}
511
512#[inline(always)]
513const fn up_align(addr: usize, align: usize) -> Option<NonZeroUsize> {
514    debug_assert!(align.is_power_of_two());
515    let mask = align - 1;
516    let Some(addr_plus_mask) = addr.checked_add(mask) else {
517        return None;
518    };
519    NonZeroUsize::new(addr_plus_mask & !mask)
520}