diff --git a/src/engine/common/ScriptModule.cpp b/src/engine/common/ScriptModule.cpp index f4557217..5c6c45f6 100644 --- a/src/engine/common/ScriptModule.cpp +++ b/src/engine/common/ScriptModule.cpp @@ -204,7 +204,10 @@ struct ScriptModuleResolver::Impl { void instantiate(const std::string& canonical) { auto found = modules.find(canonical); if (found == modules.end()) throw std::runtime_error("unknown script module: " + canonical); - Module& module = found->second; + instantiateModule(canonical, found->second); + } + + void instantiateModule(const std::string& canonical, Module& module) { if (module.state == Module::State::Ready) return; if (module.state == Module::State::Instantiating) throw std::runtime_error("cyclic script module instantiation: " + canonical); @@ -235,6 +238,51 @@ struct ScriptModuleResolver::Impl { module.state = Module::State::Ready; } + void release(Module& module) { + if (!sq_isnull(module.closure)) sq_release(vm, &module.closure); + if (!sq_isnull(module.exports)) sq_release(vm, &module.exports); + sq_resetobject(&module.closure); + sq_resetobject(&module.exports); + } + + void reload(const std::string& canonical) { + const auto found = modules.find(canonical); + if (found == modules.end()) throw std::runtime_error("cannot reload unknown script module: " + canonical); + Module& previous = found->second; + const Module::State previousState = previous.state; + const std::vector previousEdges = dependencies[canonical]; + Module candidate; + sq_resetobject(&candidate.closure); + sq_resetobject(&candidate.exports); + try { + candidate.source = load(canonical); + candidate.state = Module::State::Compiling; + dependencies.erase(canonical); + previous.state = Module::State::Compiling; + const SQInteger top = sq_gettop(vm); + if (SQ_FAILED(sq_compilebuffer(vm, candidate.source.utf8Source.c_str(), + static_cast(candidate.source.utf8Source.size()), + canonical.c_str(), SQTrue))) { + sq_settop(vm, top); + throw std::runtime_error("failed to compile script module generation: " + canonical); + } + sq_getstackobj(vm, -1, &candidate.closure); + sq_addref(vm, &candidate.closure); + sq_settop(vm, top); + candidate.dependencies = dependencies[canonical]; + candidate.state = Module::State::Compiled; + previous.state = previousState; + instantiateModule(canonical, candidate); + } catch (...) { + previous.state = previousState; + dependencies[canonical] = previousEdges; + release(candidate); + throw; + } + release(previous); + previous = std::move(candidate); + } + SQVM* vm = nullptr; ProviderId nextProviderId = 1; std::vector providers; @@ -304,6 +352,8 @@ void ScriptModuleResolver::invalidate(std::string_view canonicalUri) { } } +void ScriptModuleResolver::reload(std::string_view canonicalUri) { impl_->reload(std::string(canonicalUri)); } + const std::string& ScriptModuleResolver::lastError() const noexcept { return impl_->lastError; } bool ScriptModuleResolver::canonicalize(const ScriptModuleRequest& request, std::string& output, std::string& error) { diff --git a/src/engine/common/ScriptModule.h b/src/engine/common/ScriptModule.h index 1137fccc..eee1e3fc 100644 --- a/src/engine/common/ScriptModule.h +++ b/src/engine/common/ScriptModule.h @@ -71,6 +71,8 @@ public: void instantiateDependencies(std::string_view importerUri); /** @brief Invalidates a module and all reverse dependencies. */ void invalidate(std::string_view canonicalUri); + /** @brief Transactionally reloads one module, retaining the prior generation on failure. */ + void reload(std::string_view canonicalUri); /** @brief Last resolver diagnostic produced by a Squirrel callback. */ const std::string& lastError() const noexcept; diff --git a/test/script_module.cpp b/test/script_module.cpp index 09cded2e..b68f80a2 100644 --- a/test/script_module.cpp +++ b/test/script_module.cpp @@ -81,3 +81,30 @@ TEST_CASE("scriptModule.rejectsCyclesBeforeExecution") { } CHECK(rejected); } + +TEST_CASE("scriptModule.reloadReplacesGenerationAtomically") { + Runtime runtime(512, ssq::Libs::ALL); + auto provider = std::make_shared(); + provider->sources["mem:/value.nut"] = "export function value() { return 1 }\n"; + runtime.scriptModules().registerProvider(provider, 100); + runtime.runSource("import { value } from \"mem:/value.nut\"\nfirst <- value()\n", "game:/first.nut"); + CHECK_EQ(runtime.vm().get("first"), int64_t(1)); + + provider->sources["mem:/value.nut"] = "export function value( {\n"; + bool failed = false; + try { + runtime.scriptModules().reload("mem:/value.nut"); + } catch (const std::exception&) { + failed = true; + } + CHECK(failed); + runtime.runSource("import { value } from \"mem:/value.nut\"\nold_generation <- value()\n", + "game:/old-generation.nut"); + CHECK_EQ(runtime.vm().get("old_generation"), int64_t(1)); + + provider->sources["mem:/value.nut"] = "export function value() { return 2 }\n"; + runtime.scriptModules().reload("mem:/value.nut"); + runtime.runSource("import { value } from \"mem:/value.nut\"\nnew_generation <- value()\n", + "game:/new-generation.nut"); + CHECK_EQ(runtime.vm().get("new_generation"), int64_t(2)); +}