12#include <unordered_map>
13#include <unordered_set>
19std::string normalizedPath(std::string
value,
bool& escaped) {
20 std::replace(
value.begin(),
value.end(),
'\\',
'/');
21 std::vector<std::string>
parts;
26 if (part.empty() || part ==
".") {
27 }
else if (part ==
"..") {
34 parts.push_back(part);
36 if (
end == std::string::npos)
break;
39 std::ostringstream out;
40 for (
size_t i = 0; i <
parts.size(); ++i) {
47std::string hashSource(std::string_view
source) {
48 uint64_t
hash = 1469598103934665603ull;
49 for (
const unsigned char c :
source) {
51 hash *= 1099511628211ull;
53 std::ostringstream out;
54 out << std::hex <<
hash;
58class GameFileProvider final :
public IScriptModuleProvider {
61 std::string&
error)
override {
68 const std::string
path(canonicalUri.substr(6));
69 std::vector<uint8_t>
bytes;
70 if (
auto* filesystem = eve::cap::query<eve::service::IFileSystem>();
71 filesystem && filesystem->readFile(
path,
bytes)) {
72 source.utf8Source.assign(
reinterpret_cast<const char*
>(
bytes.data()),
bytes.size());
74 std::ifstream
input(
path, std::ios::binary);
76 error =
"script module not found: " + std::string(canonicalUri);
79 source.utf8Source.assign(std::istreambuf_iterator<char>(
input), {});
81 source.canonicalUri = std::string(canonicalUri);
94 std::shared_ptr<IScriptModuleProvider>
provider;
107 sq_setmodulehandlers(
vm,
nullptr,
nullptr,
nullptr);
108 for (
auto& [_, module] :
modules) {
109 if (!sq_isnull(module.closure)) sq_release(
vm, &module.closure);
110 if (!sq_isnull(module.exports)) sq_release(
vm, &module.exports);
115 SQUserPointer user) {
116 auto& self = *
static_cast<Impl*
>(user);
118 const std::string dependency = self.
ensureCompiled(importer, specifier);
119 auto&
edges = self.dependencies[importer];
120 if (std::find(
edges.begin(),
edges.end(), dependency) ==
edges.end())
edges.push_back(dependency);
122 }
catch (
const std::exception&
error) {
123 self.recordCompilationFailure(
error.what());
129 HSQOBJECT* exports) {
130 auto& self = *
static_cast<Impl*
>(user);
132 const std::string canonical = self.
resolve({importer, specifier});
133 const auto found = self.modules.find(canonical);
135 throw std::runtime_error(
"module was not instantiated before import: " + canonical);
136 *exports =
found->second.exports;
138 }
catch (
const std::exception&
error) {
139 self.recordCompilationFailure(
error.what());
140 return sq_throwerror(
vm,
error.what());
149 std::string attempts;
151 std::string canonical;
156 throw std::runtime_error(
error.empty() ?
"module resolution failed" :
error);
157 if (!attempts.empty()) attempts +=
", ";
158 attempts += std::to_string(entry.id);
160 throw std::runtime_error(
"no script module provider handled '" +
request.specifier +
"' imported by '" +
161 request.importerUri +
"' (providers: " + attempts +
")");
171 throw std::runtime_error(
error.empty() ?
"module load failed" :
error);
172 if (
source.canonicalUri != canonical)
173 throw std::runtime_error(
"module provider returned inconsistent identity: " +
source.canonicalUri +
177 throw std::runtime_error(
"no script module provider can load: " + canonical);
180 std::string
ensureCompiled(std::string_view importer, std::string_view specifier) {
181 const std::string canonical =
resolve({std::string(importer), std::string(specifier)});
184 throw std::runtime_error(
"cyclic script import: " + canonical);
188 Module&
module = modules[canonical];
189 sq_resetobject(&module.closure);
190 sq_resetobject(&module.exports);
191 module.source = load(canonical);
192 module.state = Module::State::Compiling;
193 const SQInteger
top = sq_gettop(
vm);
194 if (SQ_FAILED(sq_compilebuffer(
vm, module.source.utf8Source.c_str(),
195 static_cast<SQInteger
>(module.source.utf8Source.size()), canonical.c_str(),
202 throw std::runtime_error(std::move(failure));
204 throw std::runtime_error(
"failed to compile script module: " + canonical);
206 sq_getstackobj(
vm, -1, &module.closure);
207 sq_addref(
vm, &module.closure);
209 module.dependencies = dependencies[canonical];
210 module.state = Module::State::Compiled;
216 if (
found ==
modules.end())
throw std::runtime_error(
"unknown script module: " + canonical);
223 throw std::runtime_error(
"cyclic script module instantiation: " + canonical);
224 module.state = Module::State::Instantiating;
225 for (
const std::string& dependency :
module.dependencies) instantiate(dependency);
227 const SQInteger
top = sq_gettop(
vm);
229 sq_pushroottable(
vm);
230 sq_setdelegate(
vm, -2);
231 sq_pushstring(
vm,
"__exports", -1);
235 sq_newslot(
vm, -3, SQFalse);
238 sq_setclosureroot(
vm, -2);
240 if (SQ_FAILED(sq_call(
vm, 1, SQFalse, SQTrue))) {
242 module.state = Module::State::Compiled;
244 sq_resetobject(&module.
exports);
245 throw std::runtime_error(
"failed to instantiate script module: " + canonical);
248 module.state = Module::State::Ready;
254 sq_resetobject(&module.
closure);
255 sq_resetobject(&module.
exports);
261 std::unordered_set<std::string> affected{canonical};
266 if (affected.count(importer) ||
modules.find(importer) ==
modules.end())
continue;
267 if (std::any_of(
edges.begin(),
edges.end(), [&](
const auto&
edge) { return affected.count(edge); })) {
268 affected.insert(importer);
274 std::vector<std::string>
order(affected.begin(), affected.end());
277 std::unordered_set<std::string> previousKeys;
278 for (
const auto& [
uri, _] :
modules) previousKeys.insert(
uri);
280 std::unordered_map<std::string, Module>
previous;
281 for (
const std::string&
uri :
order) {
294 if (affected.count(it->first) || !previousKeys.count(it->first)) {
313 std::unordered_map<std::string, Module>
modules;
314 std::unordered_map<std::string, std::vector<std::string>>
dependencies;
323 if (!provider)
return 0;
324 const ProviderId id = impl_->nextProviderId++;
325 impl_->providers.push_back({
id,
priority, std::move(provider)});
326 std::stable_sort(impl_->providers.begin(), impl_->providers.end(),
327 [](
const auto&
a,
const auto&
b) { return a.priority > b.priority; });
332 const auto oldSize = impl_->providers.size();
333 impl_->providers.erase(std::remove_if(impl_->providers.begin(), impl_->providers.end(),
334 [
id](
const auto& entry) { return entry.id == id; }),
335 impl_->providers.end());
336 return oldSize != impl_->providers.size();
342 impl_->pendingCompilationFailure.reset();
343 impl_->dependencies.erase(std::string(importerUri));
347 std::optional<std::string> failure = std::move(impl_->pendingCompilationFailure);
348 impl_->pendingCompilationFailure.reset();
353 enum class Visit { Visiting, Ready };
354 std::unordered_map<std::string, Visit> visits;
355 std::vector<std::string> stack;
356 const auto verify = [&](
const auto& self,
const std::string&
uri) ->
void {
357 if (
const auto seen = visits.find(
uri); seen != visits.end()) {
358 if (seen->second == Visit::Ready)
return;
360 const auto begin = std::find(stack.begin(), stack.end(),
uri);
361 for (
auto it =
begin; it != stack.end(); ++it) {
362 if (!cycle.empty()) cycle +=
" -> ";
365 if (!cycle.empty()) cycle +=
" -> ";
366 throw std::runtime_error(
"cyclic script import: " + cycle +
uri);
369 visits.emplace(
uri, Visit::Visiting);
370 stack.push_back(
uri);
371 const auto edges = impl_->dependencies.find(
uri);
372 if (
edges != impl_->dependencies.end()) {
373 for (
const std::string& dependency :
edges->second) {
374 if (impl_->modules.find(dependency) == impl_->modules.end())
375 throw std::runtime_error(
"script dependency was not compiled: " + dependency);
376 self(self, dependency);
380 visits[
uri] = Visit::Ready;
383 const std::string
root(importerUri);
384 const auto edges = impl_->dependencies.find(
root);
385 if (
edges == impl_->dependencies.end())
return;
386 for (
const std::string& dependency :
edges->second) verify(verify, dependency);
390 const auto found = impl_->dependencies.find(std::string(importerUri));
391 if (
found == impl_->dependencies.end())
return;
392 for (
const std::string& dependency :
found->second) impl_->instantiate(dependency);
396 std::unordered_set<std::string> invalid{std::string(canonicalUri)};
400 for (
const auto& [importer, deps] : impl_->dependencies) {
401 if (!invalid.count(importer) &&
402 std::any_of(deps.begin(), deps.end(), [&](
const auto& dep) { return invalid.count(dep); })) {
403 invalid.insert(importer);
408 for (
const std::string&
uri : invalid) {
409 const auto found = impl_->modules.find(
uri);
410 if (
found == impl_->modules.end())
continue;
411 if (!sq_isnull(
found->second.closure)) sq_release(impl_->vm, &
found->second.closure);
412 if (!sq_isnull(
found->second.exports)) sq_release(impl_->vm, &
found->second.exports);
413 impl_->modules.erase(
found);
414 impl_->dependencies.erase(
uri);
419 if (impl_->reloadAffected(std::string(canonicalUri)).empty())
420 throw std::runtime_error(
"cannot reload unknown script module: " + std::string(canonicalUri));
424 return impl_->reloadAffected(std::string(canonicalUri));
428 const auto found = impl_->dependencies.find(std::string(importerUri));
429 return found == impl_->dependencies.end() ? std::vector<std::string>{} :
found->second;
433 std::vector<std::string> result;
434 for (
const auto& [importer,
dependencies] : impl_->dependencies) {
436 result.push_back(importer);
438 std::sort(result.begin(), result.end());
443 std::string scheme =
"game:";
445 const size_t schemeEnd =
request.specifier.find(
":/");
446 if (schemeEnd != std::string::npos) {
447 scheme =
request.specifier.substr(0, schemeEnd + 1);
449 }
else if (
request.specifier.rfind(
"./", 0) == 0 ||
request.specifier.rfind(
"../", 0) == 0) {
450 std::string importer =
request.importerUri;
451 const size_t importerScheme = importer.find(
":/");
452 if (importerScheme != std::string::npos) {
453 scheme = importer.substr(0, importerScheme + 1);
454 importer = importer.substr(importerScheme + 2);
456 const size_t slash = importer.find_last_of(
"/\\");
457 path = (slash == std::string::npos ? std::string() : importer.substr(0, slash + 1)) +
request.specifier;
461 bool escaped =
false;
462 path = normalizedPath(
path, escaped);
463 if (escaped ||
path.empty()) {
464 error = escaped ?
"script import escapes its module root: " +
request.specifier :
"empty script module path";
std::vector< eve::artifact::PartView > parts
struct SQVM * HSQUIRRELVM
std::array< std::uint8_t, 32 > hash
const GltfImportRequest & request
wgpu::PopErrorScopeStatus status
graphics::Canvas * previous
std::function< eve::Result< void >()> release
const UnitySourceAsset & source
void instantiateDependencies(std::string_view importerUri)
Instantiates the importer's dependencies in topological order.
ProviderId registerProvider(std::shared_ptr< IScriptModuleProvider > provider, int priority=0)
Registers a provider. Higher priorities are tried first.
std::vector< std::string > dependencies(std::string_view importerUri) const
Returns canonical direct dependencies for an importer.
void beginCompilation(std::string_view importerUri)
Clears prior diagnostics and dependency edges before compiling an importer.
std::optional< std::string > takeCompilationFailure()
Consumes the native failure captured while resolving the current compilation.
std::vector< std::string > reverseDependencies(std::string_view canonicalUri) const
Returns canonical importers that directly depend on a module.
std::vector< std::string > reloadAffected(std::string_view canonicalUri)
Transactionally reloads a module and every cached module that imports it.
void invalidate(std::string_view canonicalUri)
Invalidates a module and all reverse dependencies.
void reload(std::string_view canonicalUri)
Transactionally reloads one module, retaining the prior generation on failure.
~ScriptModuleResolver()
Script module resolver.
void registerDefaultProviders()
Installs the default game:/ provider backed by IFileSystem and disk fallback.
ScriptModuleResolver(SQVM *vm)
Script module resolver.
static bool canonicalize(const ScriptModuleRequest &request, std::string &output, std::string &error)
Normalizes a logical module URI and rejects root traversal.
bool unregisterProvider(ProviderId id)
Removes a provider; existing compiled module records remain valid.
void prepareDependencies(std::string_view importerUri)
Compiles all imports discovered while compiling an importer.
ScriptModuleStatus
Result of asking a provider to resolve or load a script module.
Logical module request emitted by an EveScript import declaration.
ScriptModuleSource source
std::vector< std::string > dependencies
std::shared_ptr< IScriptModuleProvider > provider
void instantiate(const std::string &canonical)
static SQRESULT dependencyCallback(HSQUIRRELVM, const SQChar *importer, const SQChar *specifier, SQUserPointer user)
std::string ensureCompiled(std::string_view importer, std::string_view specifier)
std::unordered_map< std::string, Module > modules
std::optional< std::string > pendingCompilationFailure
std::unordered_map< std::string, std::vector< std::string > > dependencies
static SQRESULT importCallback(HSQUIRRELVM vm, const SQChar *importer, const SQChar *specifier, SQUserPointer user, HSQOBJECT *exports)
ScriptModuleSource load(const std::string &canonical)
void release(Module &module)
ProviderId nextProviderId
std::vector< std::string > reloadAffected(const std::string &canonical)
void instantiateModule(const std::string &canonical, Module &module)
std::string resolve(const ScriptModuleRequest &request)
std::vector< ProviderEntry > providers
void recordCompilationFailure(std::string message)
UTF-8 source and stable identity returned by a module provider.