mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
Nearly every long-lived object becomes a cycle candidate once, soon after it is built: the constructing frame's release leaves it held only by the structure it was stored into. A nursery pass then walks it (about 75 ns per object), it survives, and it is re-buffered for the mature level, whose next pass walks it again. In tsc-ts single-threaded, nursery passes walked 6.0M objects on Excalidraw and 36.5M on Sentry and freed 7%; 99% of what they walked was promoted. Most of that walking goes to types whose objects never die in a young cycle: SymbolLinks, NodeLinks and Symbol were 2.0M of Excalidraw's candidates and the collector freed none of them, while young garbage comes from a few types (inference contexts and mappers, closures, some arrays). Types with a collector yield under 0.5% are 42-55% of what nursery passes walk. So the collector now keeps per-thread statistics per type and level. Each ScrCycType gains a `kind` index: emitted descriptors number themselves from 32 (FIRST_EMITTED_CYC_KIND / SCR_CYC_KIND_EMITTED), the runtime's own families have fixed kinds below that, 0 keeps no statistics. For the nursery and mature levels, a pass counts the candidates of each kind it walks and the objects of each kind it frees. After every 8192 walked candidates a kind bypasses that level for the next window if fewer than 1 in 128 of them were freed. A release that buffers an object at a level its type bypasses promotes it past the level first, and a nursery pass that re-buffers a survivor into a bypassed mature level moves it to the old level. One candidate in 16 stays put, so the yield keeps being measured and a type whose objects start dying young again is walked again after one window. Only the generation an object is judged in changes. Any assignment is sound (a restricted pass counts edges from older objects as external), a bypassing candidate stays buffered, and full passes walk everything they did before, so the old level's bounds hold unchanged. Cyclic garbage made only of objects of a bypassing type waits for the next pass at an older level. Programs that never complete a window (8192 walked candidates of one type) are collected exactly as before. The descriptor layout is part of the emitted-code/runtime ABI, so the program-object marker moves to scr_runtime_abi_v12. Pass log (instrumented build, single-threaded, one run each, with the preceding null-slot change): all collector passes Excalidraw 606 -> 313 ms (nursery 435 -> 153), TypeORM 629 -> 383 ms, tsc-ts self-check 377 -> 215 ms, Sentry 7.61 -> 5.75 s (nursery 2.92 -> 1.04 s, mature 1.17 -> 1.02 s; the full pass is unchanged at 3.6 s). tsc-ts (Linux sandbox, interleaved, medians; 10 rounds, Sentry 5 rounds pinned to 8 CPUs; both changes against int/best-v6): single-threaded Excalidraw -8.0%, TypeORM -4.2%, self-check -4.3%, Sentry -5.3%; --checkers 8 -12.2%, -0.8%, -2.3%, -1.6%. Peak RSS -0.8% to +0.6%. Diagnostics identical. Tests: test_cycle gains check_nursery_bypass, check_mature_bypass and check_rebuffer_past_mature (a test-only entry point, scr_cyc_pass_at under SCR_CYC_TESTING, runs passes at exact levels); each fails under the corresponding mutation (no bypass, a sticky decision, no sampling, no freed or walked counts, a bypassing candidate left unbuffered, survivors re-buffered into a bypassed level). Corpus 4690 builds a long-lived declaration graph large enough for both levels to bypass and lets objects of the same classes die in cycles; under --sanitize the exit RC audit proves they are reclaimed.
438 lines
17 KiB
C
438 lines
17 KiB
C
/* Error objects: the runtime-provided root of the `extends Error` class
|
|
* hierarchy (layout/vtable contract in scr_runtime.h). Everything here
|
|
* mirrors what the compiler emits for its own hierarchy classes — retain
|
|
* bumps rc (and marks live under the collector), the public release
|
|
* dispatches through the vtable so a base-typed release tears down the
|
|
* derived object, and the direct release in the builtin vtables does the
|
|
* ScrError teardown. The traced/untraced split is a runtime MODE (set by
|
|
* main() from the compiler's cycle fixpoint) because this one compilation
|
|
* unit serves every program.
|
|
*/
|
|
#include "scr_runtime.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
static void scr_error_oom(void) {
|
|
scr_trap("scriptc: out of memory\n");
|
|
}
|
|
|
|
static SCR_TL bool scr_error_traced = false;
|
|
static SCR_TL void (*scr_error_cause_drop)(void *obj) = NULL;
|
|
|
|
void scr_error_install_cause_drop(void (*fn)(void *obj)) {
|
|
scr_error_cause_drop = fn;
|
|
}
|
|
|
|
bool scr_error_has_cause(ScrError *e) { return e->error_cause != NULL; }
|
|
|
|
void scr_error_set_traced(void) { scr_error_traced = true; }
|
|
|
|
static const char *const scr_error_names[SCR_ERR_COUNT] = {
|
|
"Error", "TypeError", "RangeError", "SyntaxError",
|
|
/* DOMException's DEFAULT name is "Error" too (WebIDL: the name
|
|
* parameter defaults to "Error"); the class identity lives in the
|
|
* vtable, never the name. */
|
|
"Error", "ReferenceError", "EvalError", "URIError"};
|
|
|
|
void scr_error_trace(void *e, ScrTraceVisit visit, void *ctx) {
|
|
/* name/message are strings — never collector-headered; nothing to visit. */
|
|
(void)e;
|
|
(void)visit;
|
|
(void)ctx;
|
|
}
|
|
|
|
static void scr_error_gcfree(void *obj) {
|
|
ScrError *e = (ScrError *)obj;
|
|
scr_str_release(e->name);
|
|
scr_str_release(e->message);
|
|
scr_str_release(e->code); /* NULL-safe: absent on most errors */
|
|
scr_str_release(e->stack_frames);
|
|
scr_str_release(e->stack);
|
|
scr_str_release(e->system_call);
|
|
scr_str_release(e->system_path);
|
|
scr_str_release(e->system_dest);
|
|
if (e->error_cause && scr_error_cause_drop) scr_error_cause_drop(obj);
|
|
scr_obj_free_note();
|
|
scr_cyc_free(e);
|
|
}
|
|
|
|
/* DOMException teardown: the ScrError fields plus the owned cause dyn
|
|
* value. The cause is a ScrDyn — scr_json.c territory — and this file
|
|
* must stay linkable WITHOUT the checked-dynamic tree (the runtime C-unit tests link
|
|
* subsets), so the drop goes through a hook scr_json.c installs before
|
|
* any cause can exist (scr_domex_new is the only producer). */
|
|
static SCR_TL void (*scr_domex_cause_drop)(void *obj) = NULL;
|
|
|
|
void scr_domex_install_cause_drop(void (*fn)(void *obj)) {
|
|
scr_domex_cause_drop = fn;
|
|
}
|
|
|
|
static void scr_domex_gcfree(void *obj) {
|
|
ScrDomException *d = (ScrDomException *)obj;
|
|
if (d->cause != NULL && scr_domex_cause_drop != NULL) scr_domex_cause_drop(obj);
|
|
scr_error_gcfree(obj);
|
|
}
|
|
|
|
/* The DIRECT release stored in the builtin vtables (the compiler emits the
|
|
* analogous sc_reld_* for user subclasses). */
|
|
static void scr_error_destroy(void *obj) {
|
|
ScrError *e = (ScrError *)obj;
|
|
scr_str_release(e->name);
|
|
scr_str_release(e->message);
|
|
scr_str_release(e->code);
|
|
scr_str_release(e->stack_frames);
|
|
scr_str_release(e->stack);
|
|
scr_str_release(e->system_call);
|
|
scr_str_release(e->system_path);
|
|
scr_str_release(e->system_dest);
|
|
if (e->error_cause && scr_error_cause_drop) scr_error_cause_drop(obj);
|
|
scr_obj_free_note();
|
|
if (scr_error_traced) scr_cyc_free(e);
|
|
else free(e);
|
|
}
|
|
|
|
static void scr_error_reld(void *obj) {
|
|
ScrError *e = (ScrError *)obj;
|
|
if (--e->rc == 0) {
|
|
if (scr_error_traced) scr_cyc_dead(e);
|
|
scr_rc_destroy(obj, scr_error_destroy);
|
|
} else if (scr_error_traced) {
|
|
scr_cyc_released(e); /* possible cycle root; may collect */
|
|
}
|
|
}
|
|
|
|
static void scr_domex_destroy(void *obj) {
|
|
ScrDomException *d = (ScrDomException *)obj;
|
|
if (d->cause != NULL && scr_domex_cause_drop != NULL) scr_domex_cause_drop(obj);
|
|
scr_error_destroy(obj);
|
|
}
|
|
|
|
/* Both cause chains participate in the shared destruction depth budget. */
|
|
static void scr_domex_reld(void *obj) {
|
|
ScrDomException *d = (ScrDomException *)obj;
|
|
if (--d->rc == 0) {
|
|
if (scr_error_traced) scr_cyc_dead(d);
|
|
scr_rc_destroy(obj, scr_domex_destroy);
|
|
} else if (scr_error_traced) {
|
|
scr_cyc_released(d);
|
|
}
|
|
}
|
|
|
|
/* Defaults cover only the instants before main() stamps the program's real
|
|
* preorder intervals; release is permanent. */
|
|
SCR_TL ScrVt scr_error_vts[SCR_ERR_COUNT] = {
|
|
{0, 7, &scr_error_reld}, /* Error */
|
|
{1, 1, &scr_error_reld}, /* TypeError */
|
|
{2, 2, &scr_error_reld}, /* RangeError */
|
|
{3, 3, &scr_error_reld}, /* SyntaxError */
|
|
{4, 4, &scr_domex_reld}, /* DOMException */
|
|
{5, 5, &scr_error_reld}, /* ReferenceError */
|
|
{6, 6, &scr_error_reld}, /* EvalError */
|
|
{7, 7, &scr_error_reld}, /* URIError */
|
|
};
|
|
|
|
ScrError *scr_error_retain(ScrError *e) {
|
|
if (e && e->rc != SIZE_MAX) {
|
|
e->rc++;
|
|
}
|
|
return e;
|
|
}
|
|
|
|
void scr_error_release(ScrError *e) {
|
|
if (!e || e->rc == SIZE_MAX) return;
|
|
e->vt->release(e); /* the DYNAMIC class's teardown */
|
|
}
|
|
|
|
void *scr_error_retain_v(void *e) { return scr_error_retain((ScrError *)e); }
|
|
void scr_error_release_v(void *e) { scr_error_release((ScrError *)e); }
|
|
|
|
void scr_error_init(void *obj, int kind, ScrStr *message) {
|
|
ScrError *e = (ScrError *)obj;
|
|
const char *n = scr_error_names[kind];
|
|
e->name = scr_str_new(n, strlen(n));
|
|
e->message = message ? scr_str_retain(message) : scr_str_new("", 0);
|
|
e->message_present = message != NULL;
|
|
e->stack_frames = scr_stack_capture();
|
|
}
|
|
|
|
/* The Error hierarchy's trace entry point when the program's cycle fixpoint
|
|
* marked it cycle-capable (NULL otherwise) — the same argument the emitter's
|
|
* throw sites pass. For runtime-side code that stores an Error payload
|
|
* outside a throw (the Promise reject closure in scr_async.c). */
|
|
ScrTraceFn scr_error_trace_arg(void) {
|
|
return scr_error_traced ? &scr_error_trace : NULL;
|
|
}
|
|
|
|
/* Mode-aware allocation shared by every runtime-side error creation.
|
|
* DOMException allocates its wider layout (and registers the teardown
|
|
* that drops the cause); calloc/cyc_alloc zeroing covers the extra
|
|
* slots' defaults (code 0, no cause). */
|
|
static ScrError *scr_error_alloc(int kind) {
|
|
size_t size = kind == SCR_ERR_DOMEX ? sizeof(ScrDomException) : sizeof(ScrError);
|
|
static const ScrCycType error_type = { &scr_error_trace, &scr_error_gcfree, SCR_CYC_KIND_ERROR };
|
|
static const ScrCycType domex_type = { &scr_error_trace, &scr_domex_gcfree, SCR_CYC_KIND_DOMEXCEPTION };
|
|
ScrError *e;
|
|
if (scr_error_traced) {
|
|
e = scr_cyc_alloc(size, kind == SCR_ERR_DOMEX ? &domex_type : &error_type);
|
|
} else {
|
|
e = calloc(1, size);
|
|
if (!e) scr_error_oom();
|
|
}
|
|
e->rc = 1;
|
|
e->vt = &scr_error_vts[kind];
|
|
scr_obj_alloc_note();
|
|
return e;
|
|
}
|
|
|
|
ScrError *scr_error_new(int kind, ScrStr *message) {
|
|
ScrError *e = scr_error_alloc(kind);
|
|
scr_error_init(e, kind, message);
|
|
return e;
|
|
}
|
|
|
|
static SCR_TL ScrStr *(*scr_error_message_reader)(ScrError *);
|
|
static SCR_TL ScrStr *(*scr_error_name_reader)(ScrError *);
|
|
|
|
void scr_error_install_message_reader(ScrStr *(*reader)(ScrError *)) {
|
|
scr_error_message_reader = reader;
|
|
}
|
|
|
|
void scr_error_install_name_reader(ScrStr *(*reader)(ScrError *)) {
|
|
scr_error_name_reader = reader;
|
|
}
|
|
|
|
ScrStr *scr_error_default_name(const ScrError *e) {
|
|
for (int i = 1; i < SCR_ERR_COUNT; i++) {
|
|
if (i == SCR_ERR_DOMEX) continue;
|
|
if (scr_error_vts[i].pre <= e->vt->pre && e->vt->pre <= scr_error_vts[i].post)
|
|
return scr_str_new(scr_error_names[i], strlen(scr_error_names[i]));
|
|
}
|
|
return scr_str_new("Error", 5);
|
|
}
|
|
|
|
static ScrStr *scr_error_format(ScrError *e, ScrStr *name, ScrStr *message) {
|
|
/* ECMA-262 Error.prototype.toString over the resolved properties — except Node's
|
|
* OWN error classes: AssertionError's toString and the NodeError
|
|
* family's construction-time name both render "name [code]: message".
|
|
* Every ERR_*-coded error this runtime mints IS one of those classes
|
|
* (fs/exec errno codes like ENOENT keep the plain form, like Node), so
|
|
* the code-prefix test is exact for runtime-thrown errors; a user
|
|
* assigning an ERR_* code onto a plain Error would bracket here where
|
|
* Node would not (documented — user code owns that spelling). */
|
|
bool assertion = name && e->code && e->code->len >= 4 &&
|
|
memcmp(e->code->data, "ERR_", 4) == 0;
|
|
if (!name || name->len == 0) {
|
|
return message ? scr_str_retain(message) : scr_str_new("", 0);
|
|
}
|
|
if (!assertion && (!message || message->len == 0)) return scr_str_retain(name);
|
|
size_t msglen = message ? message->len : 0;
|
|
size_t codelen = assertion ? e->code->len + 3 : 0; /* " [" + code + "]" */
|
|
size_t len = name->len + codelen + 2 + msglen;
|
|
char *buf = malloc(len);
|
|
if (!buf) scr_error_oom();
|
|
size_t n = 0;
|
|
memcpy(buf + n, name->data, name->len);
|
|
n += name->len;
|
|
if (assertion) {
|
|
buf[n++] = ' ';
|
|
buf[n++] = '[';
|
|
memcpy(buf + n, e->code->data, e->code->len);
|
|
n += e->code->len;
|
|
buf[n++] = ']';
|
|
}
|
|
buf[n++] = ':';
|
|
buf[n++] = ' ';
|
|
if (message) memcpy(buf + n, message->data, message->len);
|
|
n += msglen;
|
|
ScrStr *out = scr_str_new(buf, n);
|
|
free(buf);
|
|
return out;
|
|
}
|
|
|
|
ScrStr *scr_error_to_string(ScrError *e) {
|
|
// Diagnostic formatting can run with the original error still pending.
|
|
// Accessors execute with a clear cell, and cannot replace that error.
|
|
ScrCaught *pending = scr_exc_pending() ? scr_exc_take() : NULL;
|
|
ScrStr *name = !e->name_present && scr_error_name_reader
|
|
? scr_error_name_reader(scr_error_retain(e)) : scr_str_retain(e->name);
|
|
ScrStr *message = scr_exc_pending() ? NULL : !e->message_present && scr_error_message_reader
|
|
? scr_error_message_reader(scr_error_retain(e)) : scr_str_retain(e->message);
|
|
if (pending) {
|
|
if (scr_exc_pending()) {
|
|
scr_exc_clear();
|
|
scr_str_release(message);
|
|
message = scr_str_retain(e->message);
|
|
scr_str_release(name);
|
|
name = scr_str_retain(e->name);
|
|
}
|
|
scr_rethrow(pending);
|
|
scr_caught_release(pending);
|
|
}
|
|
if (!name || !message) { scr_str_release(name); scr_str_release(message); return NULL; }
|
|
ScrStr *result = scr_error_format(e, name, message);
|
|
scr_str_release(name);
|
|
scr_str_release(message);
|
|
return result;
|
|
}
|
|
|
|
ScrStr *scr_error_stack(ScrError *e) {
|
|
if (!e->stack) {
|
|
ScrStr *heading = scr_error_to_string(e);
|
|
if (!heading) return NULL;
|
|
e->stack = e->stack_frames ? scr_str_concat(heading, e->stack_frames) : scr_str_retain(heading);
|
|
scr_str_release(heading);
|
|
}
|
|
return scr_str_retain(e->stack);
|
|
}
|
|
|
|
bool scr_error_is(const void *obj) {
|
|
const ScrVt *vt = ((const ScrError *)obj)->vt;
|
|
return scr_error_vts[SCR_ERR_ERROR].pre <= vt->pre &&
|
|
vt->pre <= scr_error_vts[SCR_ERR_ERROR].post;
|
|
}
|
|
|
|
void scr_throw_error(int kind, ScrStr *message) {
|
|
ScrError *e = scr_error_new(kind, message);
|
|
scr_str_release(message); /* scr_error_new retained a copy */
|
|
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v,
|
|
scr_error_traced ? &scr_error_trace : NULL);
|
|
}
|
|
|
|
void scr_throw_error_msg(int kind, const char *message, size_t len) {
|
|
scr_throw_error(kind, scr_str_new(message, len));
|
|
}
|
|
|
|
/* ── the `code` slot (NodeJS.ErrnoException's .code) ─────────────────── */
|
|
|
|
void scr_error_set_code(ScrError *e, const char *code) {
|
|
scr_str_release(e->code); /* NULL-safe; replace is Node's assignment */
|
|
e->code = scr_str_new(code, strlen(code));
|
|
}
|
|
|
|
ScrStr *scr_error_code(ScrError *e) {
|
|
return e->code ? scr_str_retain(e->code) : NULL;
|
|
}
|
|
|
|
/* scr_throw_error_msg with the code stamped on the payload — the fs and
|
|
* exec throwers' one-call spelling. */
|
|
void scr_throw_error_msg_code(int kind, const char *message, size_t len,
|
|
const char *code) {
|
|
ScrStr *msg = scr_str_new(message, len);
|
|
ScrError *e = scr_error_new(kind, msg);
|
|
scr_str_release(msg); /* scr_error_new retained its copy */
|
|
scr_error_set_code(e, code);
|
|
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v,
|
|
scr_error_traced ? &scr_error_trace : NULL);
|
|
}
|
|
|
|
/* A compiler-resolved Node-parity throw (the always-throwing lowered
|
|
* arms: ERR_INVALID_THIS receivers, ERR_MISSING_ARGS ladders, the
|
|
* symbol-to-string TypeError): builds the builtin error of `kind` with
|
|
* `code` stamped when non-empty. Borrows both strings; always throws
|
|
* (catchably — call sites are compiler-emitted pending checks). */
|
|
void scr_throw_node_coded(double kind, const ScrStr *code, const ScrStr *msg) {
|
|
ScrError *e = scr_error_new((int)kind, (ScrStr *)msg); /* borrowed in */
|
|
if (code->len > 0) {
|
|
scr_str_release(e->code); /* NULL-safe */
|
|
e->code = scr_str_retain((ScrStr *)code);
|
|
}
|
|
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v,
|
|
scr_error_traced ? &scr_error_trace : NULL);
|
|
}
|
|
|
|
/* A read of a `declare`d const nothing defines (the bundler-define
|
|
* pattern — __VERSION__): Node running the source throws ReferenceError
|
|
* "<name> is not defined" at the access. Borrows `name`; always throws
|
|
* (catchably — callers are compiler-emitted pending checks). */
|
|
void scr_undef_global_read(ScrStr *name) {
|
|
static const char suffix[] = " is not defined";
|
|
size_t len = name->len + sizeof suffix - 1;
|
|
char *msg = malloc(len + 1);
|
|
if (!msg) {
|
|
scr_trap("scriptc: out of memory\n");
|
|
}
|
|
memcpy(msg, name->data, name->len);
|
|
memcpy(msg + name->len, suffix, sizeof suffix);
|
|
ScrStr *m = scr_str_new(msg, len);
|
|
free(msg);
|
|
scr_throw_error_named(scr_str_new("ReferenceError", 14), m);
|
|
}
|
|
|
|
/* ── DOMException ─────────────────────────────────────────────────────
|
|
* The web-standard error shape (a Node global since v17): the ScrError
|
|
* prefix plus the WebIDL slots (legacy numeric code, the options form's
|
|
* cause). Construction resolves WebIDL's optionality over borrowed dyn
|
|
* arguments — one place, shared by the compiled `new DOMException(...)`
|
|
* and the runtime's own throw sites (atob/btoa). */
|
|
|
|
static const struct { const char *name; double code; } scr_domex_codes[] = {
|
|
{"IndexSizeError", 1}, {"DOMStringSizeError", 2},
|
|
{"HierarchyRequestError", 3}, {"WrongDocumentError", 4},
|
|
{"InvalidCharacterError", 5}, {"NoDataAllowedError", 6},
|
|
{"NoModificationAllowedError", 7}, {"NotFoundError", 8},
|
|
{"NotSupportedError", 9}, {"InUseAttributeError", 10},
|
|
{"InvalidStateError", 11}, {"SyntaxError", 12},
|
|
{"InvalidModificationError", 13}, {"NamespaceError", 14},
|
|
{"InvalidAccessError", 15}, {"ValidationError", 16},
|
|
{"TypeMismatchError", 17}, {"SecurityError", 18},
|
|
{"NetworkError", 19}, {"AbortError", 20},
|
|
{"URLMismatchError", 21}, {"QuotaExceededError", 22},
|
|
{"TimeoutError", 23}, {"InvalidNodeTypeError", 24},
|
|
{"DataCloneError", 25},
|
|
};
|
|
|
|
double scr_domex_code_of(const ScrStr *name) {
|
|
for (size_t i = 0; i < sizeof scr_domex_codes / sizeof scr_domex_codes[0]; i++) {
|
|
const char *n = scr_domex_codes[i].name;
|
|
size_t len = strlen(n);
|
|
if (name->len == len && memcmp(name->data, n, len) == 0) {
|
|
return scr_domex_codes[i].code;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* The blank allocation scr_json.c's constructors fill (scr_error_alloc
|
|
* stays private — the mode-aware sizing/teardown selection lives here). */
|
|
ScrError *scr_domex_alloc(void) { return scr_error_alloc(SCR_ERR_DOMEX); }
|
|
|
|
double scr_domex_code(ScrError *e) { return ((ScrDomException *)e)->dom_code; }
|
|
|
|
bool scr_domex_has_cause(ScrError *e) {
|
|
return ((ScrDomException *)e)->has_cause;
|
|
}
|
|
|
|
void scr_throw_domex(const char *name, const char *message) {
|
|
scr_throw_domex_str(name, scr_str_new(message, strlen(message)));
|
|
}
|
|
|
|
void scr_throw_domex_str(const char *name, ScrStr *message) {
|
|
ScrDomException *d = (ScrDomException *)scr_error_alloc(SCR_ERR_DOMEX);
|
|
d->name = scr_str_new(name, strlen(name));
|
|
d->message = message; /* ownership moves in */
|
|
d->dom_code = scr_domex_code_of(d->name);
|
|
scr_throw_obj(d, &scr_error_retain_v, &scr_error_release_v,
|
|
scr_error_traced ? &scr_error_trace : NULL);
|
|
}
|
|
|
|
|
|
void scr_throw_error_named(ScrStr *name, ScrStr *message) {
|
|
int kind = SCR_ERR_ERROR;
|
|
for (int i = 1; i < SCR_ERR_COUNT; i++) {
|
|
if (i == SCR_ERR_DOMEX) continue;
|
|
const char *n = scr_error_names[i];
|
|
if (name->len == strlen(n) && memcmp(name->data, n, name->len) == 0) {
|
|
kind = i;
|
|
break;
|
|
}
|
|
}
|
|
ScrError *e = scr_error_alloc(kind);
|
|
e->name = name; /* ownership moves in (engine names may be custom) */
|
|
e->message = message; /* ownership moves in */
|
|
e->name_present = kind == SCR_ERR_ERROR && !(name->len == 5 && memcmp(name->data, "Error", 5) == 0);
|
|
e->message_present = true;
|
|
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v,
|
|
scr_error_traced ? &scr_error_trace : NULL);
|
|
}
|