13thread_local std::vector<Runtime*> runtime_stack;
27 switch (sq_gettype(
vm, index)) {
28 case OT_NULL:
return "null";
30 SQBool
value = SQFalse;
31 return SQ_SUCCEEDED(sq_getbool(
vm, index, &
value)) &&
value ?
"true" :
"false";
35 if (SQ_SUCCEEDED(sq_getinteger(
vm, index, &
value)))
return std::to_string(
value);
40 if (SQ_SUCCEEDED(sq_getfloat(
vm, index, &
value))) {
41 std::ostringstream out;
48 const SQChar*
value =
nullptr;
57SQUserPointer objectIdentity(
const HSQOBJECT&
object) {
58 return reinterpret_cast<SQUserPointer
>(
object._unVal.pRefCounted);
61std::unique_ptr<ssq::VM> createVm(
size_t stackSize, ssq::Libs::Flag libraries) {
65 EV_PARAM_CHECK(stackSize > 0,
"Runtime stack size must be positive");
66 return std::make_unique<ssq::VM>(stackSize, libraries);
87 const std::string& message)
88 : std::runtime_error([&] {
89 std::ostringstream out;
90 out <<
"Script " << stageName(
stage);
91 if (!
source.empty()) out <<
" failed in '" <<
source <<
"'";
93 out <<
": " << message;
96 stage_(stage), source_(std::move(source)), script_id_(scriptId) {}
100 : std::runtime_error([&] {
101 std::ostringstream out;
102 out <<
"Script " << stageName(
stage);
103 if (!
source.empty()) out <<
" failed in '" <<
source <<
"'";
109 source_(std::move(source)),
110 script_id_(scriptId),
112 column_(context.column),
113 function_(context.function),
115 reported_(context.reported) {
116 if (!stack_trace_.empty() && stack_trace_.back() ==
'\n') stack_trace_.pop_back();
120 : vm_(runtime.handle()), top_(vm_ ? sq_gettop(vm_) : 0) {}
123 if (vm_) sq_settop(vm_, top_);
127 : vm_(std::exchange(other.vm_,
nullptr)), top_(other.top_) {}
130 if (
this == &other)
return *
this;
131 if (vm_) sq_settop(vm_, top_);
132 vm_ = std::exchange(other.vm_,
nullptr);
140 if (!runtime_)
return;
141 auto it = std::find(runtime_stack.rbegin(), runtime_stack.rend(), runtime_);
142 if (it != runtime_stack.rend()) runtime_stack.erase(std::next(it).base());
146 : runtime_(std::exchange(other.runtime_,
nullptr)) {}
149 : vm_(createVm(stackSize, libraries)) {
150 installErrorHandler();
156 if (initialized_)
return;
158 auto stack =
guard();
162 }
catch (
const std::exception&
error) {
168void Runtime::installErrorHandler() {
171 sq_newclosure(
vm, &scriptErrorHook, 0);
172 sq_seterrorhandler(
vm);
176 if (shutting_down_ || stopped_)
return;
177 shutting_down_ =
true;
182 auto it = std::find(runtime_stack.begin(), runtime_stack.end(),
this);
183 if (it == runtime_stack.end())
break;
184 runtime_stack.erase(it);
187 class_owners_.clear();
188 initialized_ =
false;
191 shutting_down_ =
false;
198ssq::Table
Runtime::root()
const {
return ssq::Table(
static_cast<const ssq::Object&
>(*vm_)); }
200 const bool validName =
name !=
nullptr &&
name[0] !=
'\0';
201 EV_PARAM_CHECK(validName,
"table name must not be null or empty");
202 return ssq::Table(vm_->find(
name));
206 return runtime_stack.empty() ? nullptr : runtime_stack.back();
214 auto stack =
guard();
215 const ScriptId id = next_script_id_++;
216 auto record = std::make_unique<ScriptRecord>();
217 record->info.id =
id;
218 record->info.source = sourceName;
219 record->source_text = std::move(source);
222 std::make_unique<ssq::Script>(vm_->compileSource(record->source_text.c_str(), sourceName.c_str()));
223 auto [it, inserted] = scripts_.emplace(
id, std::move(record));
225 notifyLifecycle(it->second->info);
227 }
catch (
const std::exception&
error) {
234 const bool validPath = !path.empty();
238 auto stack =
guard();
239 const ScriptId id = next_script_id_++;
240 auto record = std::make_unique<ScriptRecord>();
241 record->info.id =
id;
242 record->info.source = path;
243 record->source_text = path;
244 record->from_file =
true;
246 record->compiled = std::make_unique<ssq::Script>(vm_->compileFile(path.c_str()));
247 auto [it, inserted] = scripts_.emplace(
id, std::move(record));
249 notifyLifecycle(it->second->info);
251 }
catch (
const std::exception&
error) {
258 auto found = scripts_.find(
id);
259 if (found == scripts_.end())
266 auto stack =
guard();
267 const auto before = rootClasses();
269 record.info.error.clear();
270 notifyLifecycle(record.info);
278 sq_pushobject(
vm, record.compiled->getRaw());
279 sq_pushroottable(
vm);
280 const SQRESULT result = sq_call(
vm, 1, SQFalse, SQTrue);
281 if (SQ_FAILED(result)) {
285 discoverClasses(record, before);
290 notifyLifecycle(record.info);
293 discoverClasses(record, before);
295 notifyLifecycle(record.info);
299 }
catch (
const std::exception&
error) {
301 discoverClasses(record, before);
305 record.info.error =
error.what();
306 notifyLifecycle(record.info);
324 const bool validName = !
name.empty();
325 EV_PARAM_CHECK(validName,
"reflected class name must not be empty");
327 auto stack =
guard();
329 ssq::Class
cls = vm_->findClass(
name.c_str());
331 return classes_.at(
name);
332 }
catch (
const std::exception&
error) {
338 auto found = scripts_.find(
id);
339 if (found == scripts_.end())
341 const bool fromFile = found->second->from_file;
342 const std::string input = found->second->source_text;
343 const std::string source = found->second->info.
source;
349 auto found = scripts_.find(
id);
350 if (found == scripts_.end())
return false;
354 auto stack =
guard();
356 notifyLifecycle(record.
info);
359 auto owner = class_owners_.find(pair.first);
360 if (owner == class_owners_.end() || owner->second !=
id)
continue;
361 ssq::Object currentObject = vm_->find(pair.first.c_str());
362 if (currentObject.getType() == ssq::Type::CLASS &&
363 objectIdentity(currentObject.getRaw()) == objectIdentity(pair.second.getRaw())) {
364 sq_pushroottable(
handle());
365 sq_pushstring(
handle(), pair.first.c_str(),
static_cast<SQInteger
>(pair.first.size()));
366 sq_rawdeleteslot(
handle(), -2, SQFalse);
369 classes_.erase(pair.first);
370 class_owners_.erase(owner);
375 notifyLifecycle(record.
info);
377 }
catch (
const std::exception&
error) {
380 notifyLifecycle(record.
info);
386 std::vector<ScriptId> ids;
387 ids.reserve(scripts_.size());
388 for (
const auto& pair : scripts_) ids.push_back(pair.first);
389 std::reverse(ids.begin(), ids.end());
402 auto found = scripts_.find(
id);
403 return found == scripts_.end() ? nullptr : &found->second->info;
407 std::vector<ScriptInfo> result;
408 result.reserve(scripts_.size());
409 for (
const auto& pair : scripts_) result.push_back(pair.second->info);
417 auto found = classes_.find(
name);
418 return found == classes_.end() ? nullptr : &found->second;
422 std::vector<ReflectedClass> result;
423 result.reserve(classes_.size());
424 for (
const auto& pair : classes_) result.push_back(pair.second);
426 return a.name < b.name;
432 const bool validName = !
name.empty();
434 return vm_->findClass(
name.c_str());
437void Runtime::notifyLifecycle(
const ScriptInfo& info)
noexcept {
438 if (!lifecycle_handler_)
return;
440 lifecycle_handler_(info);
445script::ScriptErrorContext Runtime::compileErrorContext(
const std::string& what,
446 const std::string& sourceText) {
447 script::ScriptErrorContext ctx;
452 if (lineText.empty())
return ctx;
453 std::string hint = std::to_string(ctx.line) +
" | " + lineText;
454 if (ctx.column > 0) {
456 const size_t caret =
static_cast<size_t>(ctx.column > 1 ? ctx.column - 1 : 0);
457 hint.append(caret,
' ');
460 ctx.hint = std::move(hint);
464[[noreturn]]
void Runtime::fail(
ScriptStage stage,
const std::string& source, ScriptId
id,
465 script::ScriptErrorContext context) {
466 ScriptException wrapped(stage, source,
id, context);
467 if (error_handler_) {
469 error_handler_(wrapped);
476[[noreturn]]
void Runtime::fail(
ScriptStage stage,
const std::string& source, ScriptId
id,
477 const std::exception&
error) {
479 if (ctx.empty()) ctx.message =
error.what();
480 fail(stage, source,
id, std::move(ctx));
483std::unordered_map<std::string, SQUserPointer> Runtime::rootClasses()
const {
484 std::unordered_map<std::string, SQUserPointer> result;
485 StackGuard stack(*
const_cast<Runtime*
>(
this));
487 sq_pushroottable(squirrel);
488 sq_pushnull(squirrel);
489 while (SQ_SUCCEEDED(sq_next(squirrel, -2))) {
490 if (sq_gettype(squirrel, -2) == OT_STRING && sq_gettype(squirrel, -1) == OT_CLASS) {
491 const SQChar*
name =
nullptr;
493 if (SQ_SUCCEEDED(sq_getstring(squirrel, -2, &
name)) &&
name &&
494 SQ_SUCCEEDED(sq_getstackobj(squirrel, -1, &
object)))
495 result[
name] = objectIdentity(
object);
502void Runtime::discoverClasses(
503 ScriptRecord& record,
const std::unordered_map<std::string, SQUserPointer>& before) {
504 const auto after = rootClasses();
505 for (
const auto& pair : after) {
506 auto old = before.find(pair.first);
507 if (old != before.end() && old->second == pair.second)
continue;
508 ssq::Class
cls = vm_->findClass(pair.first.c_str());
509 record.class_objects.insert_or_assign(pair.first,
cls);
510 if (std::find(record.info.classes.begin(), record.info.classes.end(), pair.first) ==
511 record.info.classes.end())
512 record.info.classes.push_back(pair.first);
513 classes_[pair.first] = inspectClass(pair.first,
cls, record.info.source);
514 class_owners_[pair.first] = record.info.id;
516 std::sort(record.info.classes.begin(), record.info.classes.end());
519 for (
const std::string&
name : record.info.classes) {
520 auto info = classes_.find(
name);
521 auto cls = record.class_objects.find(
name);
522 if (info == classes_.end() ||
cls == record.class_objects.end())
continue;
523 StackGuard stack(*
this);
524 sq_pushobject(
handle(),
cls->second.getRaw());
525 if (SQ_SUCCEEDED(sq_getbase(
handle(), -1)) && sq_gettype(
handle(), -1) == OT_CLASS) {
526 HSQOBJECT baseObject;
527 if (SQ_SUCCEEDED(sq_getstackobj(
handle(), -1, &baseObject))) {
528 const SQUserPointer identity = objectIdentity(baseObject);
529 for (
const auto& candidate : after) {
530 if (candidate.second == identity) {
531 info->second.base = candidate.first;
540ReflectedClass Runtime::inspectClass(
const std::string&
name,
const ssq::Class&
cls,
541 const std::string& source)
const {
544 info.source = source;
545 StackGuard stack(*
const_cast<Runtime*
>(
this));
547 sq_pushobject(squirrel,
cls.getRaw());
548 sq_pushnull(squirrel);
549 while (SQ_SUCCEEDED(sq_next(squirrel, -2))) {
550 if (sq_gettype(squirrel, -2) == OT_STRING) {
551 const SQChar* memberName =
nullptr;
552 if (SQ_SUCCEEDED(sq_getstring(squirrel, -2, &memberName)) && memberName) {
553 ReflectedMember member;
554 member.name = memberName;
555 member.type =
static_cast<ssq::Type
>(sq_gettype(squirrel, -1));
556 member.method = member.type == ssq::Type::CLOSURE ||
557 member.type == ssq::Type::NATIVECLOSURE;
561 const SQInteger memberTop = sq_gettop(squirrel);
562 sq_push(squirrel, -2);
563 if (SQ_SUCCEEDED(sq_getattributes(squirrel, -5)) &&
564 sq_gettype(squirrel, -1) == OT_TABLE) {
565 sq_pushnull(squirrel);
566 while (SQ_SUCCEEDED(sq_next(squirrel, -2))) {
567 if (sq_gettype(squirrel, -2) == OT_STRING) {
568 const SQChar* attributeName =
nullptr;
569 if (SQ_SUCCEEDED(sq_getstring(squirrel, -2, &attributeName)) &&
571 ReflectedAttribute attribute;
572 attribute.name = attributeName;
574 static_cast<ssq::Type
>(sq_gettype(squirrel, -1));
575 attribute.value = valueString(squirrel, -1);
576 member.attributes.push_back(std::move(attribute));
582 sq_settop(squirrel, memberTop);
583 std::sort(member.attributes.begin(), member.attributes.end(),
584 [](
const ReflectedAttribute&
a,
const ReflectedAttribute&
b) {
585 return a.name < b.name;
587 info.members.push_back(std::move(member));
592 std::sort(info.members.begin(), info.members.end(),
593 [](
const ReflectedMember&
a,
const ReflectedMember&
b) { return a.name < b.name; });
EVEngine assertion entry point, backed by zeroerr.
#define EV_PARAM_CHECK(cond,...)
Validate a function parameter / public API precondition.
struct SQVM * HSQUIRRELVM
static void detach(Runtime *runtime)
Clears the active runtime if it matches; called during shutdown.
static void expose(Runtime &runtime)
Exposes every registered module into the given runtime's root table.
Pushes this Runtime on the current thread's runtime stack.
Restores the native Squirrel stack when a binding operation leaves scope.
StackGuard & operator=(StackGuard &&other) noexcept
StackGuard(Runtime &runtime) noexcept
void shutdown() noexcept
Tears down the VM and detaches the runtime; idempotent and noexcept.
ssq::Class findClass(const std::string &name) const
Finds a script class by name (throws if it does not exist).
ScriptId runFile(const std::string &path)
Convenience: compileFile() then execute().
void unloadAll() noexcept
Unloads every script.
static Runtime * current() noexcept
The runtime currently at the top of this thread's runtime stack, or nullptr.
ssq::Table table(const char *name) const
Looks up a named global table.
bool contains(ScriptId id) const noexcept
True if a script with the given id is tracked.
ScriptId compileSource(std::string source, std::string sourceName="buffer")
Compiles script source without running it.
bool unload(ScriptId id)
Unloads a script and removes its declared classes; false if unknown/unloaded.
ScriptId reload(ScriptId id)
Recompiles and re-runs a script from its original source.
Runtime(size_t stackSize=2048, ssq::Libs::Flag libraries=ssq::Libs::ALL)
Creates a script runtime with its own Squirrel VM.
std::vector< ReflectedClass > reflectedClasses() const
All reflected classes, sorted by name.
const ReflectedClass * reflectedClass(const std::string &name) const noexcept
Reflected class by name, or nullptr.
static size_t stackDepth() noexcept
Depth of the thread-local runtime stack.
ScriptId runSource(std::string source, std::string sourceName="buffer")
Convenience: compileSource() then execute().
StackGuard guard() noexcept
RAII guard that restores the Squirrel stack top on scope exit.
bool initialized() const noexcept
True once initialize() has completed successfully.
ScriptId compileFile(const std::string &path)
Compiles a script file without running it.
void initialize()
Exposes registered engine modules into the script root table. Safe to call more than once.
const ReflectedClass & reflectClass(const std::string &name, const std::string &source={})
Reflects a class by name, storing its inspected members.
const ScriptInfo * script(ScriptId id) const noexcept
Script metadata for an id, or nullptr if unknown.
HSQUIRRELVM handle() const noexcept
Raw Squirrel VM handle; nullptr after shutdown.
ssq::Table root() const
Root script table of the VM.
Scope enter()
Pushes this runtime on the thread-local runtime stack (RAII pop).
ssq::VM & vm() noexcept
Underlying SimpleSquirrel VM (requires a live, initialized runtime).
std::vector< ScriptInfo > scripts() const
Metadata for all tracked scripts, sorted by id.
const ScriptInfo & execute(ScriptId id)
Runs a previously compiled script; throws ScriptException on failure.
Exception raised at the public Runtime boundary.
ScriptStage stage() const noexcept
const std::string & source() const noexcept
ScriptException(ScriptStage stage, std::string source, uint64_t scriptId, const std::string &message)
uint64_t scriptId() const noexcept
void clearLastScriptError(HSQUIRRELVM vm)
Drops any recorded error for a VM.
bool parseCompileError(const std::string &text, std::string *source, int *line, int *column, std::string *message)
Parses the ssq compile message "Compile error at source:line:column msg".
std::string formatStackTrace(const std::vector< ScriptFrame > &frames)
Formats just the call-stack portion of a context.
std::string formatScriptError(const ScriptErrorContext &ctx)
Formats a context into a human-readable multi-line report.
std::string sourceLineText(const std::string &sourceText, int line)
Extracts one 1-based line from script source text.
ScriptErrorContext takeLastScriptError(HSQUIRRELVM vm)
Consumes and clears the last recorded error for a VM.
void setLastScriptError(HSQUIRRELVM vm, ScriptErrorContext ctx)
Records the last error for a VM (thread-local, synchronous consumers).
ScriptErrorContext captureScriptError(HSQUIRRELVM vm)
Captures the pending runtime error from inside the Squirrel error handler.
std::unique_ptr< ssq::Script > compiled
std::unordered_map< std::string, ssq::Class > class_objects
Structured snapshot of a script error: message, throw site and stack.
bool empty() const noexcept
True when no error payload was captured.
std::string message
Raw error value ("kaboom", "42", ...).