Repository: luau-lang/lute
Pull request #286: Implement `Runtime::runOnce()` and use it in the internal scheduler loop
Author: login: ffrostfall
Created: 2025-05-18T19:30:42Z | Merged: 2025-05-21T06:09:19Z

This PR implements a C-sided API `Runtime::runOnce()` to run a singular iteration of the scheduler loop. This returns an `std::variant` between `StepErr` and `StepSuccess`, returning the `lua_State* L` which errored if the variant is a `StepErr`. This is then utilized in `Runtime::runToCompletion`, which simplifies some logic regarding exiting the runtime loop.

Fixes #272

Linked issue #272: Expose a Runtime::runOneIteration API
Exposing a Runtime::runOneIteration API that runs one iteration of the scheduler and returns data + code (with code at top of stack) about the status would be really useful for me in terms of embedding lute.

Diff:
diff --git a/runtime/include/lute/runtime.h b/runtime/include/lute/runtime.h
index e3bae6651..bb2cb05a1 100644
--- a/runtime/include/lute/runtime.h
+++ b/runtime/include/lute/runtime.h
@@ -1,5 +1,7 @@
 #pragma once
 
+#include "Luau/Variant.h"
+#include "lua.h"
 #include "lute/ref.h"
 
 #include <atomic>
@@ -19,12 +21,29 @@ struct ThreadToContinue
     std::function<void()> cont;
 };
 
+struct StepErr
+{
+    lua_State* L;
+};
+
+struct StepSuccess
+{
+    lua_State* L;
+};
+
+struct StepEmpty
+{
+};
+
+using RuntimeStep = Luau::Variant<StepSuccess, StepErr, StepEmpty>;
+
 struct Runtime
 {
     Runtime();
     ~Runtime();
 
     bool runToCompletion();
+    RuntimeStep runOnce();
 
     // For child runtimes, run a thread waiting for work
     void runContinuously();
@@ -32,6 +51,7 @@ struct Runtime
     // Reports an error for a specified lua state.
     void reportError(lua_State* L);
 
+    bool hasWork();
     bool hasContinuations();
     bool hasThreads();
 
diff --git a/runtime/src/runtime.cpp b/runtime/src/runtime.cpp
index 0a8fd89eb..0406bd0ee 100644
--- a/runtime/src/runtime.cpp
+++ b/runtime/src/runtime.cpp
@@ -35,69 +35,93 @@ Runtime::~Runtime()
         runLoopThread.join();
 }
 
-bool Runtime::runToCompletion()
+bool Runtime::hasWork()
 {
-    // While there is some C++ or Luau code left to run (waiting for something to happen?)
-    while (!runningThreads.empty() || hasContinuations() || activeTokens.load() != 0)
+    return hasContinuations() || hasThreads() || activeTokens.load() != 0;
+}
+
+RuntimeStep Runtime::runOnce()
+{
+    uv_run(uv_default_loop(), UV_RUN_DEFAULT);
+
+    // Complete all C++ continuations
+    std::vector<std::function<void()>> copy;
+
     {
-        uv_run(uv_default_loop(), UV_RUN_DEFAULT);
+        std::unique_lock lock(continuationMutex);
+        copy = std::move(continuations);
+        continuations.clear();
+    }
 
-        // Complete all C++ continuations
-        std::vector<std::function<void()>> copy;
+    for (auto&& continuation : copy)
+        continuation();
 
-        {
-            std::unique_lock lock(continuationMutex);
-            copy = std::move(continuations);
-            continuations.clear();
-        }
+    if (runningThreads.empty())
+        return StepEmpty{};
 
-        for (auto&& continuation : copy)
-            continuation();
+    auto next = std::move(runningThreads.front());
+    runningThreads.erase(runningThreads.begin());
 
-        if (runningThreads.empty())
-            continue;
+    next.ref->push(GL);
+    lua_State* L = lua_tothread(GL, -1);
 
-        auto next = std::move(runningThreads.front());
-        runningThreads.erase(runningThreads.begin());
+    if (L == nullptr)
+    {
+        fprintf(stderr, "Cannot resume a non-thread reference");
+        return StepErr{L};
+    }
 
-        next.ref->push(GL);
-        lua_State* L = lua_tothread(GL, -1);
+    // We still have 'next' on stack to hold on to thread we are about to run
+    lua_pop(GL, 1);
 
-        if (L == nullptr)
-        {
-            fprintf(stderr, "Cannot resume a non-thread reference");
-            return false;
-        }
+    int status = LUA_OK;
 
-        // We still have 'next' on stack to hold on to thread we are about to run
-        lua_pop(GL, 1);
+    if (!next.success)
+        status = lua_resumeerror(L, nullptr);
+    else
+        status = lua_resume(L, nullptr, next.argumentCount);
 
-        int status = LUA_OK;
+    if (status == LUA_YIELD)
+    {
+        return StepSuccess{L};
+    }
 
-        if (!next.success)
-            status = lua_resumeerror(L, nullptr);
-        else
-            status = lua_resume(L, nullptr, next.argumentCount);
+    if (status != LUA_OK)
+    {
+        return StepErr{L};
+    };
 
-        if (status == LUA_YIELD)
-        {
-            continue;
-        }
+    if (next.cont)
+        next.cont();
+
+    return StepSuccess{L};
+}
 
-        if (status != LUA_OK)
+bool Runtime::runToCompletion()
+{
+    while (hasWork())
+    {
+        auto step = runOnce();
+
+        if (auto err = Luau::get_if<StepErr>(&step))
         {
-            reportError(L);
+            if (err->L == nullptr)
+            {
+                fprintf(stderr, "lua_State* L is nullptr");
+                return false;
+            }
+
+            reportError(err->L);
 
-            // If we have no work to do, then exit the process.
-            if (!hasThreads() && !hasContinuations())
+            // ensure we exit the process with error code properly
+            if (!hasWork())
                 return false;
-            else
-                continue;
         }
-
-        if (next.cont)
-            next.cont();
-    }
+        else
+        {
+            continue;
+        }
+    };
 
     return true;
 }


Reviews:
user: login: aatxe
body: Looks good, but use `Luau::Variant`, not `std::variant`.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-21T05:55:58Z
user: login: aatxe
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-21T06:04:22Z

Review comments:
diff_hunk: @@ -19,19 +21,37 @@ struct ThreadToContinue
     std::function<void()> cont;
 };
 
+struct StepErr
+{
+    lua_State* L;
+};
+
+struct StepSuccess
+{
+    lua_State* L;
+};
+
+struct StepEmpty
+{
+};
+
+typedef std::variant<StepSuccess, StepErr, StepEmpty> RuntimeStep;
path: runtime/include/lute/runtime.h
user: login: aatxe
body: ```suggestion
using RuntimeStep = std::variant<StepSuccess, StepErr, StepEmpty>;
```
created_at: 2025-05-21T05:53:52Z
author_association: MEMBER
diff_hunk: @@ -19,19 +21,37 @@ struct ThreadToContinue
     std::function<void()> cont;
 };
 
+struct StepErr
+{
+    lua_State* L;
+};
+
+struct StepSuccess
+{
+    lua_State* L;
+};
+
+struct StepEmpty
+{
+};
+
+typedef std::variant<StepSuccess, StepErr, StepEmpty> RuntimeStep;
path: runtime/include/lute/runtime.h
user: login: aatxe
body: Prefer `using` to `typedef`, categorically.
created_at: 2025-05-21T05:54:03Z
author_association: MEMBER
diff_hunk: @@ -19,19 +21,37 @@ struct ThreadToContinue
     std::function<void()> cont;
 };
 
+struct StepErr
+{
+    lua_State* L;
+};
+
+struct StepSuccess
+{
+    lua_State* L;
+};
+
+struct StepEmpty
+{
+};
+
+typedef std::variant<StepSuccess, StepErr, StepEmpty> RuntimeStep;
path: runtime/include/lute/runtime.h
user: login: aatxe
body: Use `Luau::Variant`, not `std::variant`.
created_at: 2025-05-21T05:55:31Z
author_association: MEMBER
diff_hunk: @@ -35,73 +35,98 @@ Runtime::~Runtime()
         runLoopThread.join();
 }
 
-bool Runtime::runToCompletion()
+bool Runtime::hasWork()
 {
-    // While there is some C++ or Luau code left to run (waiting for something to happen?)
-    while (!runningThreads.empty() || hasContinuations() || activeTokens.load() != 0)
+    return hasContinuations() || hasThreads() || activeTokens.load() != 0;
+}
+
+RuntimeStep Runtime::runOnce()
+{
+    uv_run(uv_default_loop(), UV_RUN_DEFAULT);
+
+    // Complete all C++ continuations
+    std::vector<std::function<void()>> copy;
+
     {
-        uv_run(uv_default_loop(), UV_RUN_DEFAULT);
+        std::unique_lock lock(continuationMutex);
+        copy = std::move(continuations);
+        continuations.clear();
+    }
 
-        // Complete all C++ continuations
-        std::vector<std::function<void()>> copy;
+    for (auto&& continuation : copy)
+        continuation();
 
-        {
-            std::unique_lock lock(continuationMutex);
-            copy = std::move(continuations);
-            continuations.clear();
-        }
+    if (runningThreads.empty())
+        return StepEmpty{};
 
-        for (auto&& continuation : copy)
-            continuation();
+    auto next = std::move(runningThreads.front());
+    runningThreads.erase(runningThreads.begin());
 
-        if (runningThreads.empty())
-            continue;
+    next.ref->push(GL);
+    lua_State* L = lua_tothread(GL, -1);
 
-        auto next = std::move(runningThreads.front());
-        runningThreads.erase(runningThreads.begin());
+    if (L == nullptr)
+    {
+        fprintf(stderr, "Cannot resume a non-thread reference");
+        return StepErr{L};
+    }
 
-        next.ref->push(GL);
-        lua_State* L = lua_tothread(GL, -1);
+    // We still have 'next' on stack to hold on to thread we are about to run
+    lua_pop(GL, 1);
 
-        if (L == nullptr)
-        {
-            fprintf(stderr, "Cannot resume a non-thread reference");
-            return false;
-        }
+    int status = LUA_OK;
 
-        // We still have 'next' on stack to hold on to thread we are about to run
-        lua_pop(GL, 1);
+    if (!next.success)
+        status = lua_resumeerror(L, nullptr);
+    else
+        status = lua_resume(L, nullptr, next.argumentCount);
 
-        int status = LUA_OK;
+    if (status == LUA_YIELD)
+    {
+        return StepSuccess{L};
+    }
 
-        if (!next.success)
-            status = lua_resumeerror(L, nullptr);
-        else
-            status = lua_resume(L, nullptr, next.argumentCount);
+    if (status != LUA_OK)
+    {
+        return StepErr{L};
+    };
 
-        if (status == LUA_YIELD)
-        {
-            continue;
-        }
+    if (next.cont)
+        next.cont();
+
+    return StepSuccess{L};
+}
+
+bool Runtime::runToCompletion()
+{
+    while (hasWork())
+    {
+        auto step = runOnce();
 
-        if (status != LUA_OK)
+        if (auto err = Luau::get_if<StepErr>(&step))
         {
-            reportError(L);
+            if (err->L == nullptr)
+            {
+                fprintf(stderr, "lua_State* L is nullptr");
+                return false;
+            }
+
+            reportError(err->L);
 
-            // If we have no work to do, then exit the process.
-            if (!hasThreads() && !hasContinuations())
+            // ensure we exit the process with error code properly
+            if (!hasWork())
                 return false;
-            else
-                continue;
         }
-
-        if (next.cont)
-            next.cont();
-    }
+        else
+        {
+            continue;
+        }
+    };
 
     return true;
 }
 
+
path: runtime/src/runtime.cpp
user: login: aatxe
body: ```suggestion
```
created_at: 2025-05-21T06:04:10Z
author_association: MEMBER

PR comments:
user: login: ffrostfall
created_at: 2025-05-18T19:31:51Z
body: I'm uncertain about the namespacing & organization related to `StepSuccess` and `StepErr`, and the usage of `holds_alternative`, but I think this is the right way to do it.

also side note, sorry for the horrific diff :/
author_association: CONTRIBUTOR
user: login: cheesycod
created_at: 2025-05-18T21:21:19Z
body: > I'm uncertain about the namespacing & organization related to `StepSuccess` and `StepErr`, and the usage of `holds_alternative`, but I think this is the right way to do it.
> 
> also side note, sorry for the horrific diff :/

It'd be nice if StepSuccess included lua_State of the thread and maybe even a reason enum/the status(?)
author_association: CONTRIBUTOR
user: login: cheesycod
created_at: 2025-05-19T00:48:29Z
body: I approve this PR personally
author_association: CONTRIBUTOR