luau 0.732.0

Safe lifetime-bound Rust embedding API for the Luau runtime
use std::collections::{BTreeMap, HashSet};
use std::ffi::CString;

use bstr::BString;
use luau::{Error, Lua, MultiValue, Value};

#[test]
fn callback_integer_conversion_preserves_argument_error_context() -> Result<(), Error> {
    let lua = Lua::new()?;
    let function = lua.create_function(luau::callback!(|_lua, value: i32| { Ok(value) }))?;

    assert_eq!(function.call::<i32>("42")?, 42);

    let error = function
        .call::<i32>(i64::MAX)
        .expect_err("i64::MAX must not fit into i32");
    let Error::CallbackError { cause, .. } = error else {
        panic!("expected callback error");
    };
    let Error::BadArgument { pos, cause, .. } = cause.as_ref() else {
        panic!("expected bad argument error");
    };
    assert_eq!(*pos, 1);
    assert!(matches!(
        cause.as_ref(),
        Error::FromLuaConversionError { from, to, message }
            if from == "integer" && to == "i32" && message.as_deref() == Some("out of range")
    ));

    Ok(())
}

#[test]
fn integer_extraction_rejects_unrepresentable_values() -> Result<(), Error> {
    let lua = Lua::new()?;

    assert_eq!(lua.load("1.5").eval::<i64>()?, 1);
    assert!(lua.load("-1").eval::<u64>().is_err());
    assert!(lua.load("math.huge").eval::<i64>().is_err());
    assert!(lua.unpack::<u64>(lua.pack(1_u128 << 64)?).is_err());
    assert_eq!(lua.unpack::<i128>(lua.pack(1_i128 << 64)?)?, 1_i128 << 64);

    Ok(())
}

#[test]
fn rust_integers_use_luau_representations_without_precision_loss() -> Result<(), Error> {
    let lua = Lua::new()?;

    assert!(matches!(lua.pack(42_i32)?, Value::Number(42.0)));
    assert!(matches!(lua.pack(42_i64)?, Value::Integer(42)));
    assert!(matches!(lua.pack(42_isize)?, Value::Number(42.0)));
    assert!(matches!(lua.pack(42_usize)?, Value::Number(42.0)));
    assert!(matches!(
        lua.pack(i64::MAX as u64)?,
        Value::Integer(i64::MAX)
    ));

    let exact_number = 1_u64 << 63;
    assert!(matches!(
        lua.pack(exact_number)?,
        Value::Number(value) if value == exact_number as f64
    ));
    assert_eq!(lua.pack(exact_number)?.as_u64(), Some(exact_number));
    assert!(matches!(
        lua.pack(exact_number + 1),
        Err(Error::IntoLuaConversionError { from, to, message })
            if from == "u64" && to == "number" && message.as_deref() == Some("out of range")
    ));

    assert_eq!(lua.pack(42_usize)?.as_usize(), Some(42));
    assert_eq!(Value::Number(-1.0).as_u32(), None);
    assert_eq!(Value::Number(f64::INFINITY).as_i32(), None);

    #[cfg(target_pointer_width = "64")]
    {
        let exact_pointer_number = 1_usize << 53;
        assert!(lua.pack(exact_pointer_number).is_ok());
        assert!(matches!(
            lua.pack(exact_pointer_number + 1),
            Err(Error::IntoLuaConversionError { from, to, message })
                if from == "usize"
                    && to == "number"
                    && message.as_deref() == Some("out of range")
        ));
    }

    Ok(())
}

#[test]
fn string_conversion_matches_between_values_and_stack_arguments() -> Result<(), Error> {
    let lua = Lua::new()?;

    assert_eq!(lua.unpack::<String>(Value::Number(-123.55))?, "-123.55");
    let string_argument =
        lua.create_function(luau::callback!(|_lua, value: String| { Ok(value) }))?;
    assert_eq!(string_argument.call::<String>(-43.22)?, "-43.22");

    assert!(lua.unpack::<String>(Value::Integer(123)).is_err());
    assert!(string_argument.call::<String>(123_i64).is_err());

    let bytes = b"\xffbuffer bytes";
    let buffer = lua.create_buffer(bytes)?;
    assert_eq!(
        lua.unpack::<BString>(Value::Buffer(buffer.try_clone()?))?,
        bytes.as_slice()
    );
    let buffer_argument =
        lua.create_function(luau::callback!(|_lua, value: BString| { Ok(value.len()) }))?;
    assert_eq!(buffer_argument.call::<usize>(&buffer)?, bytes.len());

    Ok(())
}

#[test]
fn rust_collections_use_luau_table_sequence_and_key_semantics() -> Result<(), Error> {
    let lua = Lua::new()?;

    let sequence = lua.create_sequence_from([10, 20, 30])?;
    assert_eq!(sequence.raw_get::<i32>(1)?, 10);
    assert_eq!(
        lua.unpack::<Vec<i32>>(Value::Table(sequence.try_clone()?))?,
        [10, 20, 30]
    );
    assert_eq!(
        lua.unpack::<[i32; 3]>(Value::Table(sequence.try_clone()?))?,
        [10, 20, 30]
    );
    assert!(
        lua.unpack::<[i32; 2]>(Value::Table(sequence.try_clone()?))
            .is_err()
    );

    let map = BTreeMap::from([("left".to_owned(), 1), ("right".to_owned(), 2)]);
    let packed_map = lua.pack(map.clone())?;
    assert_eq!(lua.unpack::<BTreeMap<String, i32>>(packed_map)?, map);

    let set = HashSet::from(["alpha".to_owned(), "beta".to_owned()]);
    let packed_set = lua.pack(set.clone())?;
    assert_eq!(lua.unpack::<HashSet<String>>(packed_set)?, set);

    Ok(())
}

#[test]
fn character_and_c_string_conversions_enforce_their_value_domains() -> Result<(), Error> {
    let lua = Lua::new()?;

    assert_eq!(lua.convert::<char>("A")?, 'A');
    assert_eq!(lua.convert::<char>(65_i64)?, 'A');
    assert_eq!(lua.convert::<char>(128_175_i64)?, '💯');
    assert!(lua.convert::<char>(1.0).is_err());
    assert!(lua.convert::<char>("more than one").is_err());
    assert!(lua.convert::<char>(5_456_324_i64).is_err());

    let c_string = CString::new("embedded value").expect("test string should not contain a nul");
    assert_eq!(
        lua.unpack::<CString>(lua.pack(c_string.clone())?)?,
        c_string
    );
    let interior_nul = lua.create_string(b"not\0a C string")?;
    assert!(lua.unpack::<CString>(Value::String(interior_nul)).is_err());

    Ok(())
}

#[test]
fn borrowed_table_and_multi_value_conversions_preserve_identity_and_roots() -> Result<(), Error> {
    let lua = Lua::new()?;
    let (table_pointer, function_pointer, packed_table, packed_values) = {
        let table = lua.create_table()?;
        let function: luau::Function<'_> = lua.load("return function() return 42 end").call(())?;
        let values = MultiValue::from_vec(vec![
            Value::Table(table.try_clone()?),
            Value::Function(function.try_clone()?),
        ]);

        (
            table.to_pointer(),
            function.to_pointer(),
            lua.pack(&table)?,
            lua.pack_multi(&values)?,
        )
    };

    lua.gc_collect()?;

    let Value::Table(packed_table) = packed_table else {
        panic!("the borrowed table should remain a table");
    };
    assert_eq!(packed_table.to_pointer(), table_pointer);

    let identity: luau::Function<'_> = lua.load("return function(...) return ... end").call(())?;
    let returned_table: luau::Table<'_> = identity.call(&packed_table)?;
    let returned_values: MultiValue<'_> = identity.call(&packed_values)?;

    assert_eq!(returned_table.to_pointer(), table_pointer);
    assert_eq!(returned_values.len(), 2);
    let mut returned_values = returned_values.into_iter();
    let Some(Value::Table(returned_table)) = returned_values.next() else {
        panic!("the first borrowed multi-value should remain a table");
    };
    let Some(Value::Function(returned_function)) = returned_values.next() else {
        panic!("the second borrowed multi-value should remain a function");
    };
    assert_eq!(returned_table.to_pointer(), table_pointer);
    assert_eq!(returned_function.to_pointer(), function_pointer);

    Ok(())
}