use std::collections::BTreeSet;
const MIN_ALLOC: usize = 32;
pub type BlockRef = u32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BlockState {
Free,
InUse,
}
#[derive(Debug, Clone, Copy)]
struct Block {
offset: usize,
size: usize,
state: BlockState,
}
pub struct LuaLimitedManagedAllocator {
pool: Vec<u8>,
blocks: Vec<Block>,
free_list: Vec<BlockRef>,
infallible_depth: u32,
debug_allocated_bytes: usize,
}
impl Default for LuaLimitedManagedAllocator {
fn default() -> Self {
Self::new(1024 * 1024)
}
}
impl LuaLimitedManagedAllocator {
pub fn new(pool_size: usize) -> Self {
let mut allocator = Self {
pool: vec![0u8; pool_size],
blocks: Vec::new(),
free_list: Vec::new(),
infallible_depth: 0,
debug_allocated_bytes: 0,
};
if pool_size > 0 {
let blocks = &mut allocator.blocks;
blocks.push(Block {
offset: 0,
size: pool_size,
state: BlockState::Free,
});
allocator.free_list.push(0);
}
allocator
}
pub fn mark_free(&mut self, block_ref: BlockRef) {
if let Some(block) = self.block_mut(block_ref) {
block.state = BlockState::Free;
self.add_to_free_list(block_ref);
}
}
pub fn mark_in_use(&mut self, block_ref: BlockRef) {
if let Some(block) = self.block_mut(block_ref) {
block.state = BlockState::InUse;
self.remove_from_free_list(block_ref);
}
}
pub fn get_next_free_block_ref(&self, block_ref: BlockRef) -> Option<BlockRef> {
let pos = self.free_list.iter().position(|&r| r == block_ref)?;
self.free_list.get(pos + 1).copied()
}
pub fn get_prev_free_block_ref(&self, block_ref: BlockRef) -> Option<BlockRef> {
let pos = self.free_list.iter().position(|&r| r == block_ref)?;
if pos == 0 {
None
} else {
Some(self.free_list[pos - 1])
}
}
pub fn get_next_adjacent_block_ref(&self, block_ref: BlockRef) -> Option<BlockRef> {
let (idx, _) = self
.blocks
.iter()
.enumerate()
.find(|(_, b)| block_of(block_ref) == b.offset)?;
self
.blocks
.get(idx + 1)
.map(|b| b_ref_idx(idx + 1, b.offset))
}
pub fn get_ref_val(&self, block_ref: BlockRef) -> Option<usize> {
Some(self.block(block_ref)?.offset)
}
pub fn enter_infallible_allocation_region(&mut self) {
self.infallible_depth += 1;
}
pub fn try_exit_infallible_allocation_region(&mut self) -> bool {
self.infallible_depth = self.infallible_depth.saturating_sub(1);
true
}
pub fn allocate_new(&mut self, size: usize, _align: usize) -> Option<*mut u8> {
let need = Self::round_to_min_alloc(size);
let mut chosen = None;
for &block_ref in &self.free_list {
let block = self.block(block_ref)?;
if block.size >= need {
chosen = Some(block_ref);
break;
}
}
let block_ref = chosen.or_else(|| {
self.try_coalesce_all_free_blocks();
self
.free_list
.iter()
.copied()
.find(|&r| self.block(r).is_some_and(|b| b.size >= need))
})?;
let block_size = self.block(block_ref)?.size;
if Self::should_split(block_size, need) {
self.split_free_block(block_ref, need);
}
let data_offset = self.block(block_ref)?.offset;
self.mark_in_use(block_ref);
self.debug_allocated_bytes += need;
Some(unsafe { self.pool.as_mut_ptr().add(data_offset) })
}
pub fn resize_allocation(&mut self, block_ref: BlockRef, new_size: usize) -> Option<usize> {
let need = Self::round_to_min_alloc(new_size);
let current = self.block(block_ref)?.size;
if need == current {
return Some(self.block(block_ref)?.offset);
}
if need < current {
self.split_free_block(block_ref, need);
let block = self.block(block_ref)?;
self.debug_allocated_bytes = self.debug_allocated_bytes.saturating_sub(block.size);
return Some(block.offset);
}
while self.block(block_ref).is_some_and(|b| b.size < need) {
let next = self.get_next_adjacent_block_ref(block_ref)?;
if !self
.block(next)
.is_some_and(|b| b.state == BlockState::Free)
{
return None;
}
self.coalesce_pair(block_ref, next);
if self.block(block_ref).is_some_and(|b| b.size < need)
&& self.get_next_adjacent_block_ref(block_ref).is_none()
{
return None;
}
}
self.block(block_ref).map(|b| b.offset)
}
pub fn contains_ref(&self, offset: usize) -> bool {
offset < self.pool.len()
}
pub fn is_valid_block_ref(&self, block_ref: BlockRef) -> bool {
self.block(block_ref).is_some()
}
pub fn debug_check(&self) -> bool {
self.check_correctness()
}
pub fn check_correctness(&self) -> bool {
let mut last_end = 0usize;
for block in &self.blocks {
if block.offset < last_end {
return false;
}
last_end = block.offset + block.size;
}
let free_in_list = self.free_list.iter().copied().collect::<BTreeSet<_>>();
free_in_list.len() == self.free_list.len()
}
pub fn try_coalesce_all_free_blocks(&mut self) -> bool {
let mut merged = false;
let mut idx = 0;
while idx + 1 < self.blocks.len() {
let a = self.blocks[idx];
let b = self.blocks[idx + 1];
let (ra, rb) = (b_ref_idx(idx, a.offset), b_ref_idx(idx + 1, b.offset));
if a.state == BlockState::Free && b.state == BlockState::Free && a.offset + a.size == b.offset
{
self.coalesce_pair(ra, rb);
merged = true;
} else {
idx += 1;
}
}
merged
}
pub fn add_to_free_list(&mut self, block_ref: BlockRef) {
let Some(offset) = self.get_ref_val(block_ref) else {
return;
};
let pos = self
.free_list
.iter()
.position(|&r| self.get_ref_val(r).is_some_and(|o| o > offset))
.unwrap_or(self.free_list.len());
self.free_list.insert(pos, block_ref);
}
pub fn remove_from_free_list(&mut self, block_ref: BlockRef) {
self.free_list.retain(|&r| r != block_ref);
}
pub fn move_to_head_of_free_list(&mut self, block_ref: BlockRef) {
self.remove_from_free_list(block_ref);
self.free_list.insert(0, block_ref);
}
pub fn try_coalesce_single_block(&mut self, block_ref: BlockRef) -> bool {
let Some(next) = self.get_next_adjacent_block_ref(block_ref) else {
return false;
};
if !self
.block(next)
.is_some_and(|b| b.state == BlockState::Free)
{
return false;
}
self.coalesce_pair(block_ref, next);
true
}
pub fn split_in_use_block(&mut self, block_ref: BlockRef, first_size: usize) -> Option<BlockRef> {
self.split_common(block_ref, first_size, BlockState::Free)
}
pub fn split_free_block(&mut self, block_ref: BlockRef, first_size: usize) -> Option<BlockRef> {
self.split_common(block_ref, first_size, BlockState::Free)
}
pub fn get_data_start_ref(&self, block_ref: BlockRef) -> Option<usize> {
Some(self.block(block_ref)?.offset + BLOCK_HEADER_SIZE)
}
pub fn get_free_list(&self) -> &[BlockRef] {
&self.free_list
}
pub fn get_block_ref(&self, ptr: *mut u8) -> Option<BlockRef> {
let offset = ptr as usize - self.pool.as_ptr() as usize;
self
.blocks
.iter()
.enumerate()
.find(|(_, b)| offset >= b.offset && offset < b.offset + b.size)
.map(|(idx, b)| b_ref_idx(idx, b.offset))
}
pub fn update_debug_allocated_bytes(&mut self, delta: i64) {
self.debug_allocated_bytes = (self.debug_allocated_bytes as i64 + delta).max(0) as usize;
}
fn split_common(
&mut self,
block_ref: BlockRef,
first_size: usize,
tail_state: BlockState,
) -> Option<BlockRef> {
let first_size = Self::round_to_min_alloc(first_size);
let block = self.block(block_ref)?;
if block.size <= first_size {
return None;
}
let (offset, size) = (block.offset, block.size);
let idx = self.block_idx(block_ref)?;
self.blocks[idx].size = first_size;
self.blocks.insert(
idx + 1,
Block {
offset: offset + first_size,
size: size - first_size,
state: tail_state,
},
);
let tail_ref = b_ref_idx(idx + 1, offset + first_size);
if tail_state == BlockState::Free {
self.add_to_free_list(tail_ref);
}
Some(tail_ref)
}
fn should_split(block_size: usize, need: usize) -> bool {
block_size >= need + MIN_ALLOC
}
fn round_to_min_alloc(size: usize) -> usize {
size.max(1).div_ceil(MIN_ALLOC) * MIN_ALLOC
}
fn coalesce_pair(&mut self, head: BlockRef, tail: BlockRef) {
let (Some(h_idx), Some(t_idx)) = (self.block_idx(head), self.block_idx(tail)) else {
return;
};
let merged_size = self.blocks[h_idx].size + self.blocks[t_idx].size;
self.blocks[h_idx].size = merged_size;
self.remove_from_free_list(tail);
self.blocks.remove(t_idx);
}
fn block(&self, block_ref: BlockRef) -> Option<Block> {
let idx = self.block_idx(block_ref)?;
self.blocks.get(idx).copied()
}
fn block_mut(&mut self, block_ref: BlockRef) -> Option<&mut Block> {
let idx = self.block_idx(block_ref)?;
self.blocks.get_mut(idx)
}
fn block_idx(&self, block_ref: BlockRef) -> Option<usize> {
let offset = block_of(block_ref);
self.blocks.iter().position(|b| b.offset == offset)
}
}
pub const BLOCK_HEADER_SIZE: usize = 16;
fn block_of(block_ref: BlockRef) -> usize {
block_ref as usize
}
fn b_ref_idx(_idx: usize, offset: usize) -> BlockRef {
offset as BlockRef
}
#[cfg(test)]
mod tests {
use super::LuaLimitedManagedAllocator;
#[test]
fn allocate_free_coalesce_cycle() {
let mut alloc = LuaLimitedManagedAllocator::new(1024);
let a = alloc.allocate_new(64, 1).unwrap();
let b = alloc.allocate_new(64, 1).unwrap();
assert_ne!(a, b);
assert!(alloc.debug_check());
assert_eq!(alloc.get_free_list().len(), 1);
}
#[test]
fn quota_exhaustion_returns_none() {
let mut alloc = LuaLimitedManagedAllocator::new(256);
assert!(alloc.allocate_new(256, 1).is_some());
assert!(alloc.allocate_new(32, 1).is_none());
assert!(alloc.check_correctness());
}
#[test]
fn split_marks_and_reuses() {
let mut alloc = LuaLimitedManagedAllocator::new(1024);
let a = alloc.allocate_new(64, 1).unwrap();
let a_ref = alloc.get_block_ref(a).unwrap();
let tail = alloc.split_in_use_block(a_ref, 32);
assert!(tail.is_some());
assert_eq!(alloc.get_free_list().len(), 2);
}
}