luau 0.732.0

Safe lifetime-bound Rust embedding API for the Luau runtime
use core::alloc::Layout;
use core::cell::Cell;
use core::ptr::NonNull;
use std::rc::Rc;

use luau_vm::state::LuaAllocator;

use super::{Lua, LuaRef};

#[derive(Default)]
pub(crate) struct MemoryState {
    used: Cell<usize>,
    limit: Cell<usize>,
}

impl MemoryState {
    fn can_grow_by(&self, amount: usize) -> bool {
        let Some(used) = self.used.get().checked_add(amount) else {
            return false;
        };
        let limit = self.limit.get();
        limit == 0 || used <= limit
    }

    fn record_growth(&self, amount: usize) {
        self.used.set(
            self.used
                .get()
                .checked_add(amount)
                .expect("successful allocator growth must fit in usize"),
        );
    }

    fn record_shrink(&self, amount: usize) {
        self.used.set(
            self.used
                .get()
                .checked_sub(amount)
                .expect("allocator accounting must remain balanced"),
        );
    }

    fn set_limit(&self, limit: usize) -> usize {
        self.limit.replace(limit)
    }
}

pub(crate) struct TrackingAllocator<A> {
    allocator: A,
    memory: Rc<MemoryState>,
}

impl<A> TrackingAllocator<A> {
    pub(crate) const fn new(allocator: A, memory: Rc<MemoryState>) -> Self {
        Self { allocator, memory }
    }
}

unsafe impl<A> LuaAllocator for TrackingAllocator<A>
where
    A: LuaAllocator,
{
    unsafe fn allocate(&self, layout: Layout) -> Option<NonNull<u8>> {
        if !self.memory.can_grow_by(layout.size()) {
            return None;
        }
        let allocation = unsafe { self.allocator.allocate(layout) }?;
        self.memory.record_growth(layout.size());
        Some(allocation)
    }

    unsafe fn reallocate(
        &self,
        pointer: NonNull<u8>,
        old_layout: Layout,
        new_layout: Layout,
    ) -> Option<NonNull<u8>> {
        if new_layout.size() > old_layout.size()
            && !self
                .memory
                .can_grow_by(new_layout.size() - old_layout.size())
        {
            return None;
        }

        let allocation = unsafe { self.allocator.reallocate(pointer, old_layout, new_layout) }?;
        match new_layout.size().cmp(&old_layout.size()) {
            core::cmp::Ordering::Greater => self
                .memory
                .record_growth(new_layout.size() - old_layout.size()),
            core::cmp::Ordering::Less => self
                .memory
                .record_shrink(old_layout.size() - new_layout.size()),
            core::cmp::Ordering::Equal => {}
        }
        Some(allocation)
    }

    unsafe fn deallocate(&self, pointer: NonNull<u8>, layout: Layout) {
        unsafe {
            self.allocator.deallocate(pointer, layout);
        }
        self.memory.record_shrink(layout.size());
    }
}

impl Lua {
    /// Returns the number of bytes currently allocated by this Luau state.
    pub fn used_memory(&self) -> usize {
        self.lua_ref().used_memory()
    }

    /// Sets the allocation limit in bytes and returns the previous limit.
    ///
    /// A limit of zero disables the limit. Allocations that would exceed a
    /// nonzero limit fail with [`crate::Error::MemoryError`].
    pub fn set_memory_limit(&self, limit: usize) -> usize {
        self.lua_ref().set_memory_limit(limit)
    }
}

impl LuaRef<'_> {
    /// Returns the number of bytes currently allocated by this Luau state.
    pub fn used_memory(&self) -> usize {
        self.runtime().memory().used.get()
    }

    /// Sets the allocation limit in bytes and returns the previous limit.
    ///
    /// A limit of zero disables the limit. Allocations that would exceed a
    /// nonzero limit fail with [`crate::Error::MemoryError`].
    pub fn set_memory_limit(&self, limit: usize) -> usize {
        self.runtime().memory().set_limit(limit)
    }
}