luau 0.732.0

Safe lifetime-bound Rust embedding API for the Luau runtime
use core::fmt;

use std::borrow::Cow;
use std::cmp;
use std::hash::{Hash, Hasher};
use std::ptr::NonNull;

use luau_common::{BStr, ByteSlice};
use luau_vm::VmErrorResult;
use luau_vm::thread::{StackGuard, Thread as VmThread};

use crate::error::Error;
use crate::lua::{Lua, LuaRef};
use crate::thread::Thread;
use crate::value::ValueRef;

mod conversion;

/// A handle to an immutable Luau byte string.
pub struct LuaString<'lua> {
    reference: ValueRef<'lua>,
    bytes: NonNull<BStr>,
}

impl Lua {
    /// Creates and roots a Luau string from arbitrary bytes.
    pub fn create_string(&self, bytes: impl AsRef<[u8]>) -> Result<LuaString<'_>, Error> {
        self.lua_ref().create_string(bytes)
    }
}

impl<'lua> LuaRef<'lua> {
    /// Creates and roots a Luau string from arbitrary bytes.
    pub fn create_string(&self, bytes: impl AsRef<[u8]>) -> Result<LuaString<'lua>, Error> {
        LuaString::from_bytes(&self.current_thread(), bytes)
    }
}

impl<'lua> LuaString<'lua> {
    pub(crate) fn from_bytes(
        thread: &Thread<'lua>,
        bytes: impl AsRef<[u8]>,
    ) -> Result<Self, Error> {
        unsafe {
            let vm_thread = thread.as_vm();
            let _stack = StackGuard::new(vm_thread);
            vm_thread
                .push_string(bytes.as_ref())
                .map_err(|exit| Error::from_thread_exit(vm_thread, exit))?;
            Self::from_stack(thread, -1).map_err(|exit| Error::from_thread_exit(vm_thread, exit))
        }
    }

    pub(crate) unsafe fn from_stack(thread: &Thread<'lua>, index: i32) -> VmErrorResult<Self> {
        let stack_thread = thread.as_vm();
        debug_assert_ne!(unsafe { stack_thread.is_string(index) }, 0);
        let bytes = unsafe {
            stack_thread
                .to_string(index)?
                .expect("string stack slot should contain a string")
        };
        Ok(Self {
            reference: ValueRef::from_stack(thread, index)?,
            bytes: NonNull::from(bytes),
        })
    }

    pub(crate) fn coerce_from_stack(thread: &Thread<'lua>, index: i32) -> Result<Self, Error> {
        unsafe {
            let vm_thread = thread.as_vm();
            if vm_thread
                .to_string(index)
                .map_err(|exit| Error::from_thread_exit(vm_thread, exit))?
                .is_none()
            {
                let type_name = vm_thread.type_name(vm_thread.type_of(index));
                let type_name = type_name.as_bytes().to_str_lossy();
                return Err(Error::from_lua_conversion(
                    type_name.as_ref(),
                    "string",
                    Some("expected string or number"),
                ));
            }

            Self::from_stack(thread, index).map_err(|exit| Error::from_thread_exit(vm_thread, exit))
        }
    }

    /// Creates another rooted handle to this string.
    pub fn try_clone(&self) -> Result<Self, Error> {
        Ok(Self {
            reference: self.reference.try_clone()?,
            bytes: self.bytes,
        })
    }

    /// Returns the string's bytes.
    pub fn as_bytes(&self) -> &BStr {
        unsafe {
            // SAFETY: `bytes` is captured from a VM string at construction time, and
            // `reference` roots that immutable string for at least the borrow of `self`.
            self.bytes.as_ref()
        }
    }

    /// Returns the string as UTF-8.
    pub fn to_str(&self) -> Result<&str, Error> {
        self.as_bytes().to_str().map_err(|error| {
            let message = error.to_string();
            Error::from_lua_conversion("string", "&str", Some(message.as_str()))
        })
    }

    /// Returns a lossy UTF-8 representation of the string.
    pub fn to_str_lossy(&self) -> Cow<'_, str> {
        self.as_bytes().to_str_lossy()
    }

    /// Returns a pointer that uniquely identifies this Luau string.
    pub const fn to_pointer(&self) -> *const () {
        self.reference.pointer()
    }

    pub(crate) fn push_to(&self, target: impl AsRef<VmThread>) -> Result<(), Error> {
        self.reference.push_to(target)
    }
}

impl fmt::Display for LuaString<'_> {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(self.as_bytes(), formatter)
    }
}

impl fmt::Debug for LuaString<'_> {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.to_str() {
            Ok(value) => fmt::Debug::fmt(value, formatter),
            Err(_) => {
                formatter.write_str("b")?;
                fmt::Debug::fmt(self.as_bytes(), formatter)
            }
        }
    }
}

impl PartialEq for LuaString<'_> {
    fn eq(&self, other: &Self) -> bool {
        self.as_bytes() == other.as_bytes()
    }
}

impl<T> PartialEq<T> for LuaString<'_>
where
    T: AsRef<[u8]> + ?Sized,
{
    fn eq(&self, other: &T) -> bool {
        self.as_bytes() == other.as_ref()
    }
}

impl Eq for LuaString<'_> {}

impl<T> PartialOrd<T> for LuaString<'_>
where
    T: AsRef<[u8]> + ?Sized,
{
    fn partial_cmp(&self, other: &T) -> Option<cmp::Ordering> {
        self.as_bytes().partial_cmp(other.as_ref())
    }
}

impl PartialOrd for LuaString<'_> {
    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for LuaString<'_> {
    fn cmp(&self, other: &Self) -> cmp::Ordering {
        self.as_bytes().cmp(other.as_bytes())
    }
}

impl Hash for LuaString<'_> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.as_bytes().hash(state);
    }
}