Allow to check whether modules finished evaluating

This commit is contained in:
phosit 2025-01-15 21:01:23 +01:00 committed by phosit
parent 32ef4fd0fa
commit ce01bdddf6
6 changed files with 202 additions and 16 deletions

View file

@ -0,0 +1,3 @@
await undefined;
// `import "blabbermouth.js";` would be hoisted before the await resulting in it not being delayed.
log("blah blah blah");

View file

@ -0,0 +1,2 @@
await undefined;
await undefined;

View file

@ -0,0 +1 @@
await new Promise(() => {});

View file

@ -22,6 +22,7 @@
#include "ps/CStr.h"
#include "ps/Filesystem.h"
#include "scriptinterface/Object.h"
#include "scriptinterface/Promises.h"
#include "scriptinterface/ScriptConversions.h"
#include "scriptinterface/ScriptInterface.h"
@ -63,7 +64,7 @@ namespace
return std::get<1>(*std::get<0>(insertResult)).m_ModuleObject;
}
void Evaluate(const ScriptRequest& rq, JS::HandleObject mod)
[[nodiscard]] JSObject* Evaluate(const ScriptRequest& rq, JS::HandleObject mod)
{
if (!JS::ModuleLink(rq.cx, mod))
{
@ -72,12 +73,33 @@ void Evaluate(const ScriptRequest& rq, JS::HandleObject mod)
}
JS::RootedValue val{rq.cx};
if (!JS::ModuleEvaluate(rq.cx, mod, &val))
if (!JS::ModuleEvaluate(rq.cx, mod, &val) || !val.isObject())
{
ScriptException::CatchPending(rq);
throw std::invalid_argument{"Unable to evaluate module."};
}
return &val.toObject();
}
bool Call(JSContext* cx, const unsigned argc, JS::Value* vp)
{
JS::CallArgs args{JS::CallArgsFromVp(argc, vp)};
const ScriptRequest rq{cx};
const auto statusPtr{JS::GetMaybePtrFromReservedSlot<ModuleLoader::Future::Status>(
&args.callee(), 0)};
if (!statusPtr)
return true;
(*statusPtr) = ModuleLoader::Future::Fulfilled{};
return true;
}
constexpr JSClassOps callbackClassOps{nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
/*call =*/Call, nullptr, nullptr};
constexpr JSClass callbackClass{"Callback", JSCLASS_HAS_RESERVED_SLOTS(1), &callbackClassOps};
} // anonymous namespace
ModuleLoader::CompiledModule::CompiledModule(const ScriptRequest& rq, const VfsPath& filePath):
@ -103,10 +125,56 @@ ModuleLoader::CompiledModule::CompiledModule(const ScriptRequest& rq, const VfsP
}
}
void ModuleLoader::LoadModule(const ScriptRequest& rq, const VfsPath& modulePath)
ModuleLoader::Future::Future(const ScriptRequest& rq, ModuleLoader& loader, const VfsPath& modulePath):
m_Status{Evaluating{{rq.cx, JS_NewObject(rq.cx, &callbackClass)}}}
{
JS::RootedObject mod{rq.cx, CompileModule(rq, m_Registry, modulePath)};
Evaluate(rq, mod);
JS::RootedObject mod{rq.cx, CompileModule(rq, loader.m_Registry, modulePath)};
JS::RootedObject promise{rq.cx, Evaluate(rq, mod)};
SetReservedSlot(JS::PrivateValue(static_cast<void*>(&m_Status)));
if (!JS::AddPromiseReactions(rq.cx, promise, std::get<Evaluating>(m_Status).fulfill, nullptr))
throw std::runtime_error{"Failed adding promise reaction."};
}
ModuleLoader::Future::Future(Future&& other) noexcept:
m_Status{std::exchange(other.m_Status, Invalid{})}
{
SetReservedSlot(JS::PrivateValue(static_cast<void*>(&m_Status)));
}
ModuleLoader::Future& ModuleLoader::Future::operator=(Future&& other) noexcept
{
SetReservedSlot(JS::UndefinedValue());
m_Status = std::exchange(other.m_Status, Invalid{});
SetReservedSlot(JS::PrivateValue(static_cast<void*>(&m_Status)));
return *this;
}
ModuleLoader::Future::~Future()
{
SetReservedSlot(JS::UndefinedValue());
}
[[nodiscard]] bool ModuleLoader::Future::IsDone() const noexcept
{
return std::holds_alternative<Fulfilled>(m_Status);
}
void ModuleLoader::Future::SetReservedSlot(JS::Value privateValue) noexcept
{
Evaluating* evaluatingStatus{std::get_if<Evaluating>(&m_Status)};
if (!evaluatingStatus)
return;
if (evaluatingStatus->fulfill)
JS::SetReservedSlot(evaluatingStatus->fulfill, 0, privateValue);
}
[[nodiscard]] ModuleLoader::Future ModuleLoader::LoadModule(const ScriptRequest& rq,
const VfsPath& modulePath)
{
return Future{rq, *this, modulePath};
}
[[nodiscard]] JSObject* ModuleLoader::ResolveHook(JSContext* cx, JS::HandleValue,

View file

@ -22,6 +22,7 @@
#include "scriptinterface/ScriptTypes.h"
#include <unordered_map>
#include <variant>
class ScriptContext;
class ScriptRequest;
@ -42,13 +43,43 @@ public:
using RegistryType = std::unordered_map<VfsPath, CompiledModule>;
class Future
{
public:
struct Evaluating
{
JS::PersistentRootedObject fulfill;
};
struct Fulfilled {};
struct Invalid {};
using Status = std::variant<Evaluating, Fulfilled, Invalid>;
explicit Future(const ScriptRequest& rq, ModuleLoader& loader, const VfsPath& modulePath);
Future() = default;
Future(const Future&) = delete;
Future& operator=(const Future&) = delete;
Future(Future&& other) noexcept;
Future& operator=(Future&& other) noexcept;
~Future();
[[nodiscard]] bool IsDone() const noexcept;
private:
// It's save to not require a `JS::HandleValue` here.
void SetReservedSlot(JS::Value privateValue) noexcept;
Status m_Status{Invalid{}};
};
/**
* Load the specified module and all module it imports recursively.
*
* @param rq @c globalThis is taken from this @c ScriptRequest.
* @param modulePath The path to the file which should be loaded as a module.
* @return A future that is fulfilled when the evaluation of the module
* completes.
*/
void LoadModule(const ScriptRequest& rq, const VfsPath& modulePath);
[[nodiscard]] Future LoadModule(const ScriptRequest& rq, const VfsPath& modulePath);
private:
// Functions used by the `ScriptContext`.

View file

@ -20,6 +20,7 @@
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "scriptinterface/ModuleLoader.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptInterface.h"
class TestScriptModule : public CxxTest::TestSuite
@ -43,7 +44,7 @@ public:
const ScriptRequest rq{script};
TestLogger logger;
script.GetModuleLoader().LoadModule(rq, "include/entry.js");
std::ignore = script.GetModuleLoader().LoadModule(rq, "include/entry.js");
// This test does not rely on export to the engine. So use the logger to check if it succeeded.
TS_ASSERT_STR_CONTAINS(logger.GetOutput(),"Test succeeded");
@ -54,12 +55,12 @@ public:
{
ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script};
script.GetModuleLoader().LoadModule(rq, "empty.js");
std::ignore = script.GetModuleLoader().LoadModule(rq, "empty.js");
}
{
ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script};
script.GetModuleLoader().LoadModule(rq, "empty.js");
std::ignore = script.GetModuleLoader().LoadModule(rq, "empty.js");
}
}
@ -70,9 +71,9 @@ public:
{
ScriptInterface scriptInner{"Test", "Test", g_ScriptContext};
const ScriptRequest rqInner{scriptInner};
scriptInner.GetModuleLoader().LoadModule(rqInner, "empty.js");
std::ignore = scriptInner.GetModuleLoader().LoadModule(rqInner, "empty.js");
}
scriptOuter.GetModuleLoader().LoadModule(rqOuter, "empty.js");
std::ignore = scriptOuter.GetModuleLoader().LoadModule(rqOuter, "empty.js");
}
void test_ImportInFunction()
@ -81,8 +82,8 @@ public:
const ScriptRequest rq{script};
TestLogger logger;
TS_ASSERT_THROWS(script.GetModuleLoader().LoadModule(rq, "import_inside_function.js"),
const std::invalid_argument&);
TS_ASSERT_THROWS(std::ignore = script.GetModuleLoader().LoadModule(rq,
"import_inside_function.js"), const std::invalid_argument&);
const std::string log{logger.GetOutput()};
TS_ASSERT_STR_CONTAINS(log, "import_inside_function.js line 3");
TS_ASSERT_STR_CONTAINS(log, "import declarations may only appear at top level of a module");
@ -94,7 +95,7 @@ public:
const ScriptRequest rq{script};
const TestLogger _;
TS_ASSERT_THROWS(script.GetModuleLoader().LoadModule(rq, "nonexistent.js"),
TS_ASSERT_THROWS(std::ignore = script.GetModuleLoader().LoadModule(rq, "nonexistent.js"),
const std::runtime_error&);
}
@ -105,13 +106,93 @@ public:
{
TestLogger logger;
script.GetModuleLoader().LoadModule(rq, "blabbermouth.js");
std::ignore = script.GetModuleLoader().LoadModule(rq, "blabbermouth.js");
TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "blah blah blah");
}
{
TestLogger logger;
script.GetModuleLoader().LoadModule(rq, "include/../blabbermouth.js");
std::ignore = script.GetModuleLoader().LoadModule(rq, "include/../blabbermouth.js");
TS_ASSERT_STR_NOT_CONTAINS(logger.GetOutput(), "blah blah blah");
}
}
void test_TopLevelAwaitFinite()
{
ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script};
auto future = script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js");
TS_ASSERT(!future.IsDone());
g_ScriptContext->RunJobs();
TS_ASSERT(future.IsDone());
}
void test_TopLevelAwaitInfinite()
{
ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script};
auto future = script.GetModuleLoader().LoadModule(rq, "top_level_await_infinite.js");
g_ScriptContext->RunJobs();
TS_ASSERT(!future.IsDone());
}
void test_MoveFulfilledFuture()
{
ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script};
Script::ModuleLoader::Future future0{
script.GetModuleLoader().LoadModule(rq, "empty.js")};
g_ScriptContext->RunJobs();
TS_ASSERT(future0.IsDone());
Script::ModuleLoader::Future future1{std::move(future0)};
Script::ModuleLoader::Future future2;
future2 = std::move(future1);
TS_ASSERT(!future0.IsDone());
TS_ASSERT(!future1.IsDone());
TS_ASSERT(future2.IsDone());
}
void test_MoveEvaluatingFuture()
{
ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script};
Script::ModuleLoader::Future future0{
script.GetModuleLoader().LoadModule(rq, "top_level_await_finite.js")};
Script::ModuleLoader::Future future1{std::move(future0)};
Script::ModuleLoader::Future future2;
future2 = std::move(future1);
TS_ASSERT(!future0.IsDone());
TS_ASSERT(!future1.IsDone());
TS_ASSERT(!future2.IsDone());
g_ScriptContext->RunJobs();
TS_ASSERT(!future0.IsDone());
TS_ASSERT(!future1.IsDone());
TS_ASSERT(future2.IsDone());
}
void test_EvaluateReplacedFuture()
{
ScriptInterface script{"Test", "Test", g_ScriptContext};
const ScriptRequest rq{script};
TestLogger logger;
auto future{script.GetModuleLoader().LoadModule(rq, "delayed_blabbermouth.js")};
TS_ASSERT_STR_NOT_CONTAINS(logger.GetOutput(), "blah blah blah");
TS_ASSERT(!future.IsDone());
future = script.GetModuleLoader().LoadModule(rq, "empty.js");
TS_ASSERT(!future.IsDone());
g_ScriptContext->RunJobs();
TS_ASSERT(future.IsDone());
TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "blah blah blah");
}
};