luau 0.732.0

Safe lifetime-bound Rust embedding API for the Luau runtime
use luau::{Error, Function, Lua, ThreadStatus};

#[test]
fn thread_resume() -> Result<(), Error> {
    let lua = Lua::new()?;
    let thread = lua
        .load(
            r#"
            local sum = ...
            for _ = 1, 4 do
                sum += coroutine.yield(sum)
            end
            return sum
            "#,
        )
        .into_thread()?;

    assert_eq!(thread.status(), ThreadStatus::Resumable);
    assert_eq!(thread.resume::<i64>(0)?, 0);
    assert_eq!(thread.status(), ThreadStatus::Resumable);
    assert_eq!(thread.resume::<i64>(1)?, 1);
    assert_eq!(thread.resume::<i64>(2)?, 3);
    assert_eq!(thread.resume::<i64>(3)?, 6);
    assert_eq!(thread.resume::<i64>(4)?, 10);
    assert_eq!(thread.status(), ThreadStatus::Finished);
    assert!(matches!(
        thread.resume::<()>(()),
        Err(Error::CoroutineUnresumable)
    ));

    Ok(())
}

#[test]
fn sandboxed_chunk_thread_binds_an_isolated_environment() -> Result<(), Error> {
    let lua = Lua::new()?;
    lua.globals()?.set("shared_global", 1)?;
    lua.sandbox(true)?;
    lua.globals()?.set("shared_global", 2)?;

    let chunk = lua
        .load("file_global = 42; return file_global, shared_global")
        .into_sandboxed()?;

    {
        let function = chunk.function();
        lua.gc_collect()?;
        assert_eq!(function.call::<(i32, i32)>(())?, (42, 1));
    }

    let thread = chunk.into_thread();
    assert_eq!(thread.resume::<(i32, i32)>(())?, (42, 1));
    assert_eq!(lua.globals()?.get::<Option<i32>>("file_global")?, None);

    Ok(())
}

#[test]
fn thread_sandbox_inherits_current_globals_without_stale_imports() -> Result<(), Error> {
    let lua = Lua::new()?;
    lua.globals()?.set("x", 1)?;
    lua.sandbox(true)?;

    let load_parent_setter = lua.create_function(luau::callback!(|lua| {
        lua.load("return function(value) x = value end")
            .eval::<Function<'_>>()
    }))?;
    let set_parent: Function<'_> = load_parent_setter.call(())?;
    lua.current_thread().sandbox()?;

    let get_x = lua.load("return x").into_function()?;
    assert_eq!(get_x.call::<i32>(())?, 1);

    set_parent.call::<()>(2)?;
    assert_eq!(get_x.call::<i32>(())?, 2);
    Ok(())
}

#[test]
fn thread_resume_error_is_raised_at_the_suspended_yield() -> Result<(), Error> {
    let lua = Lua::new()?;
    let thread: luau::Thread<'_> = lua
        .load(
            r#"
                return coroutine.create(function()
                    local ok, error = pcall(coroutine.yield, 123)
                    assert(not ok)
                    assert(error == "injected error")
                    return "recovered"
                end)
            "#,
        )
        .eval()?;

    assert_eq!(thread.resume::<i32>(())?, 123);
    assert_eq!(
        thread.resume_error::<String>("injected error")?,
        "recovered"
    );

    Ok(())
}

#[test]
fn thread_resume_inherits_the_active_callback_depth() {
    std::thread::Builder::new()
        .stack_size(16 * 1024 * 1024)
        .spawn(|| {
            let lua = Lua::new().expect("Lua should initialize");
            let resume = lua
                .create_function(|lua, mut arguments| {
                    let depth: i32 = arguments.next()?;
                    if depth > 0 {
                        let thread = lua.load("return rust_resume(...)").into_thread()?;
                        thread.resume::<()>(depth - 1)?;
                    }
                    arguments.finish(())
                })
                .expect("the callback should be created");
            lua.globals()
                .and_then(|globals| globals.set("rust_resume", resume))
                .expect("the callback should be registered");

            let thread = lua
                .load("return rust_resume(...)")
                .into_thread()
                .expect("the coroutine should be created");
            let error = thread
                .resume::<()>(256)
                .expect_err("nested native resumes must observe Luau's call-depth limit");
            assert!(error.to_string().contains("C stack overflow"), "{error}");
        })
        .expect("the test thread should start")
        .join()
        .expect("the test thread should finish");
}

#[test]
fn thread_runtime_errors_include_the_failed_coroutine_traceback() -> Result<(), Error> {
    let lua = Lua::new()?;
    let thread = lua
        .load(
            r#"
                local function fail()
                    error("expected failure")
                end
                fail()
            "#,
        )
        .set_name("thread_traceback")
        .into_thread()?;

    let error = thread
        .resume::<()>(())
        .expect_err("the coroutine should fail");
    let message = error.to_string();
    assert!(message.contains("expected failure"));
    assert!(message.contains("stack traceback:"));
    assert!(message.contains("thread_traceback"));

    Ok(())
}