/* 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 #include 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", "AggregateError", "AssertionError", "SuppressedError"}; 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); scr_str_release(e->system_message); scr_str_release(e->system_code); 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); } /* AggregateError teardown: the ScrError fields plus the owned errors * list, dropped through the hook scr_json.c installs before any list can * exist (the same split as the DOMException cause). */ static SCR_TL void (*scr_aggregate_errors_drop)(void *obj) = NULL; void scr_aggregate_install_errors_drop(void (*fn)(void *obj)) { scr_aggregate_errors_drop = fn; } static void scr_aggregate_gcfree(void *obj) { ScrAggregateError *a = (ScrAggregateError *)obj; if (a->errors != NULL && scr_aggregate_errors_drop != NULL) scr_aggregate_errors_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); scr_str_release(e->system_message); scr_str_release(e->system_code); 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); } } static void scr_aggregate_destroy(void *obj) { ScrAggregateError *a = (ScrAggregateError *)obj; if (a->errors != NULL && scr_aggregate_errors_drop != NULL) scr_aggregate_errors_drop(obj); scr_error_destroy(obj); } /* SuppressedError teardown: the ScrError fields plus the disposal * snapshots scr_exc_suppress recorded for its `error` and `suppressed`. */ static void scr_suppressed_gcfree(void *obj) { scr_exc_suppressed_forget(obj); scr_error_gcfree(obj); } static void scr_suppressed_destroy(void *obj) { scr_exc_suppressed_forget(obj); scr_error_destroy(obj); } static void scr_suppressed_reld(void *obj) { ScrError *e = (ScrError *)obj; if (--e->rc == 0) { if (scr_error_traced) scr_cyc_dead(e); scr_rc_destroy(obj, scr_suppressed_destroy); } else if (scr_error_traced) { scr_cyc_released(e); } } static void scr_aggregate_reld(void *obj) { ScrAggregateError *a = (ScrAggregateError *)obj; if (--a->base.rc == 0) { if (scr_error_traced) scr_cyc_dead(a); scr_rc_destroy(obj, scr_aggregate_destroy); } else if (scr_error_traced) { scr_cyc_released(a); } } /* 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, 8, &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 */ {8, 8, &scr_aggregate_reld}, /* AggregateError */ {9, 9, &scr_error_reld}, /* AssertionError */ {10, 10, &scr_suppressed_reld}, /* SuppressedError */ }; 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) : kind == SCR_ERR_AGGREGATE ? sizeof(ScrAggregateError) : sizeof(ScrError); static ScrCycType error_type = { &scr_error_trace, &scr_error_gcfree, SCR_CYC_KIND_ERROR }; static ScrCycType domex_type = { &scr_error_trace, &scr_domex_gcfree, SCR_CYC_KIND_DOMEXCEPTION }; static ScrCycType aggregate_type = { &scr_error_trace, &scr_aggregate_gcfree, SCR_CYC_KIND_ERROR }; static ScrCycType suppressed_type = { &scr_error_trace, &scr_suppressed_gcfree, SCR_CYC_KIND_ERROR }; ScrError *e; if (scr_error_traced) { e = scr_cyc_alloc(size, kind == SCR_ERR_DOMEX ? &domex_type : kind == SCR_ERR_AGGREGATE ? &aggregate_type : kind == SCR_ERR_SUPPRESSED ? &suppressed_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 = !e->native_plain && 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); } void scr_error_set_stack(ScrError *e, ScrStr *stack) { ScrStr *replacement = scr_str_retain(stack); scr_str_release(e->stack); e->stack = replacement; } 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)); } /* new AbortError(undefined, { cause: reason }) as Node's AbortController * paths build it: message "The operation was aborted", code ABORT_ERR, * then name AbortError as own enumerable properties. `reason` is * borrowed; NULL stands for undefined. +1. */ ScrError *scr_error_abort(struct ScrDyn *reason) { ScrStr *message = scr_str_new("The operation was aborted", 25); ScrError *e = scr_error_new(SCR_ERR_ERROR, message); scr_str_release(message); scr_str_release(e->name); e->name = scr_str_new("AbortError", 10); scr_error_set_code(e, "ABORT_ERR"); e->name_present = true; e->name_enumerable = true; scr_error_define_cause(e, reason ? reason : scr_dyn_undefined()); return e; } /* The error object behind an event-error message, while a fire site whose * payload is an Error (not only a message) runs its listeners; the error * listener adapters answer it in place of an Error built from the * message. Borrowed for the duration of the fire. */ SCR_TL ScrError *scr_event_error_payload = NULL; /* The code Node attaches to the runtime's event-error messages whose text * carries no errno name: connResetException's "socket hang up" and * "aborted", the TLS client's disconnect before the handshake completes, * the HTTP parser's header overflow, and the TLS verification failures (the openssl reason Node reports as the code). NULL for any * other message. */ const char *scr_error_message_code(const ScrStr *msg) { static const struct { const char *prefix; bool whole; const char *code; } known[] = { { "socket hang up", true, "ECONNRESET" }, { "aborted", true, "ECONNRESET" }, { "Client network socket disconnected before secure TLS connection was established", true, "ECONNRESET" }, { "Parse Error: Header overflow", true, "HPE_HEADER_OVERFLOW" }, { "certificate has expired", true, "CERT_HAS_EXPIRED" }, { "self-signed certificate in certificate chain", true, "SELF_SIGNED_CERT_IN_CHAIN" }, { "self-signed certificate", false, "DEPTH_ZERO_SELF_SIGNED_CERT" }, { "unable to verify the first certificate", false, "UNABLE_TO_VERIFY_LEAF_SIGNATURE" }, { "Hostname/IP does not match certificate's altnames", false, "ERR_TLS_CERT_ALTNAME_INVALID" }, { "error:0A000460:SSL routines:ssl3_read_bytes:tlsv1 alert no application protocol", true, "ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL" }, { "error:0A00042E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version", true, "ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION" }, { "error:0A0000BF:SSL routines::no protocols available", true, "ERR_SSL_NO_PROTOCOLS_AVAILABLE" }, }; for (size_t i = 0; i < sizeof known / sizeof known[0]; i++) { size_t n = strlen(known[i].prefix); if (msg->len < n || memcmp(msg->data, known[i].prefix, n) != 0) continue; if (known[i].whole ? msg->len == n : (msg->len == n || msg->data[n] == ';' || msg->data[n] == ':')) return known[i].code; } return NULL; } 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 * " 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); } ScrError *scr_aggregate_alloc(void) { return scr_error_alloc(SCR_ERR_AGGREGATE); } 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); }