Files
scriptc/packages/runtime/src/scr_exception.c
T
2026-10-28 05:42:18 -07:00

769 lines
29 KiB
C

/* The in-flight exception cell (see scr_runtime.h). One cell per execution
* context — the main stack plus one per fiber (scr_async.c swaps the active
* cell on fiber switches) — never a stack of cells: JS has exactly one
* exception in flight per context, and a throw while one is pending (only
* reachable from a `finally` running on the exception path) REPLACES it,
* which is JS's semantics too.
*
* Unwinding itself is compiler-emitted (pending checks + RC releases +
* dummy returns); this file only stores, renders, and releases the value.
*/
#include "scr_runtime.h"
#ifdef SCR_NODE_ISLANDS
#include "scr_node_island.h"
#endif
#include <stdatomic.h>
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#ifndef SCR_LIB
/* The trap funnel's EXECUTABLE expansion: exactly the historical
* fputs/vfprintf-to-stderr + abort every trap site used to open-code — the
* default lane's bytes and behavior are unchanged by construction. Library
* builds (-DSCR_LIB) get the sink-routing definitions from scr_library.c
* instead; this pair compiles out there. Lives HERE (the failure-channel
* unit, present in every link including the runtime's own unit-test
* subsets) rather than in the console unit. */
_Noreturn void scr_trap(const char *msg) {
fputs(msg, stderr);
abort();
}
_Noreturn void scr_trap_fmt(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
abort();
}
#endif /* !SCR_LIB */
static SCR_TL ScrExcCell scr_main_exc; /* fiber zero (the main stack) */
/* The active cell is exported so executables can test its kind inline at
* every exception poll and call scr_exc_pending only on the slow path; the
* kind is the cell's first field. */
#if (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) || defined(SCR_WORKERS)
/* A thread-local pointer cannot be initialized with &scr_main_exc — the
* address of a thread-local is not a constant expression — so NULL stands
* for the main cell and every read resolves it. Worker executables also
* switch this pointer as fibers enter and leave their own exception cells,
* and scr_init points it at the context's main cell before program code
* runs, so generated inline polls never read NULL. */
SCR_TL ScrExcCell *scr_exc_active;
#define SCR_EXC_CUR() (scr_exc_active ? scr_exc_active : &scr_main_exc)
#else
ScrExcCell *scr_exc_active = &scr_main_exc;
/* What scr_exc_swap_cell(NULL) selects; the inline swap reads it. */
ScrExcCell *const scr_exc_main_cell = &scr_main_exc;
#define SCR_EXC_CUR() (scr_exc_active)
#endif
/* The running stack's overflow guard (scr_runtime.h). It lives beside the
* exception cell so fiber switching can maintain it whenever scr_async.c is
* linked; scr_context.c installs it per thread. The explicit initializer
* keeps it an ordinary definition rather than a common symbol, which a
* localized library archive would otherwise leave shared. */
SCR_TL uintptr_t scr_stack_guard = 0;
#if defined(SCR_WORKERS) && !defined(SCR_LIB) && defined(SCR_EXC_FLAG)
/* The thread's exact pending word, tested inline by every emitted pending
* check in ELF worker executables (one %fs/tpidr-relative load: the word is
* in the executable's static TLS block). Nonzero means the running code
* must unwind, so the emitted check needs no out-of-line answer:
*
* - SCR_EXC_FLAG_PENDING mirrors `active cell kind != NONE` exactly. Only
* the owner thread writes it, at every change of its active cell (throws,
* rethrows, take/clear, fiber switches, payload moves, the termination
* sentinel), because a stale bit would unwind a normal path.
* - SCR_EXC_FLAG_STOP means the context must stop. worker.terminate() sets
* it from the owner's thread (scr_exc_flag_request_stop, through the
* pointer the worker registers while its script runs), scr_context_stop
* sets it for process.exit() and fatal paths, and nothing clears it.
*
* A set STOP bit unwinds while the active cell may still be NONE: the
* TERMINATE sentinel is installed by whoever consumes the unwind before it
* inspects the cell. scr_exc_pending and scr_context_checkpoint do that,
* emitted try regions test the bit before entering a handler, and the
* runtime's unwind consumers (fiber completion, the uncaught reporters,
* exit listeners) checkpoint first. */
SCR_TL _Atomic uint32_t scr_exc_flag;
static inline void scr_exc_flag_pending(bool pending) {
uint32_t word = atomic_load_explicit(&scr_exc_flag, memory_order_relaxed);
if (((word & SCR_EXC_FLAG_PENDING) != 0) == pending) return;
if (pending) atomic_fetch_or_explicit(&scr_exc_flag, SCR_EXC_FLAG_PENDING, memory_order_relaxed);
else atomic_fetch_and_explicit(&scr_exc_flag, ~SCR_EXC_FLAG_PENDING, memory_order_relaxed);
}
void scr_exc_flag_request_stop(_Atomic uint32_t *flag) {
/* Release: a thread that observes the bit also observes the requester's
* earlier writes (the worker's cancel flag). */
atomic_fetch_or_explicit(flag, SCR_EXC_FLAG_STOP, memory_order_release);
}
/* The active cell, or the context's stop state, may have changed. */
void scr_exc_cell_changed(void) {
if (!scr_context_quiet() &&
!(atomic_load_explicit(&scr_exc_flag, memory_order_relaxed) & SCR_EXC_FLAG_STOP))
atomic_fetch_or_explicit(&scr_exc_flag, SCR_EXC_FLAG_STOP, memory_order_relaxed);
scr_exc_flag_pending(SCR_EXC_CUR()->kind != SCR_EXC_NONE);
}
void scr_exc_alert_thread_exit(void) {}
#define SCR_EXC_RAISED() scr_exc_flag_pending(true)
#elif defined(SCR_WORKERS) && !defined(SCR_LIB)
/* The process-wide exception alert, tested inline by every emitted pending
* check in worker executables: one ordinary load, where reading the active
* cell would be a thread-local access (a call on Darwin). It counts the
* script threads whose active cell may hold a pending exception or which
* must stop, plus one reference per outstanding worker.terminate() request
* (scr_worker.c). Zero proves that no thread has anything pending, so the
* check falls through; otherwise the out-of-line scr_exc_pending answers for
* the calling thread. A thread raises its share whenever its active cell may
* have become pending (throws, rethrows, the termination sentinel, fiber
* switches and moves into the active cell) and drops it once that cell is
* clear again (take, clear, the slow path), so an exception in flight in one
* thread costs the others slow-path calls only until it is caught. Only its
* owner changes a thread's share, so a thread's own raise is visible to all
* of its later checks. */
_Alignas(64) _Atomic uint32_t scr_exc_alert = 0;
static SCR_TL bool scr_exc_alerted;
void scr_exc_alert_add(int delta) {
atomic_fetch_add_explicit(&scr_exc_alert, (uint32_t)delta, memory_order_release);
}
static void scr_exc_alert_raise(void) {
if (scr_exc_alerted) return;
scr_exc_alerted = true;
atomic_fetch_add_explicit(&scr_exc_alert, 1, memory_order_relaxed);
}
static void scr_exc_alert_drop(void) {
if (!scr_exc_alerted) return;
scr_exc_alerted = false;
atomic_fetch_sub_explicit(&scr_exc_alert, 1, memory_order_relaxed);
}
/* The active cell, or the context's stop state, may have changed. */
void scr_exc_cell_changed(void) {
if (SCR_EXC_CUR()->kind != SCR_EXC_NONE || !scr_context_quiet()) scr_exc_alert_raise();
else scr_exc_alert_drop();
}
/* A worker thread's script execution has ended for good. */
void scr_exc_alert_thread_exit(void) { scr_exc_alert_drop(); }
#define SCR_EXC_RAISED() scr_exc_alert_raise()
#else
void scr_exc_cell_changed(void) {}
#define SCR_EXC_RAISED() ((void)0)
#endif
/* Fiber switching (scr_async.c) points the exception machinery at the
* incoming fiber's cell; returns the previous cell. */
ScrExcCell *(scr_exc_swap_cell)(ScrExcCell *cell) {
ScrExcCell *prev = SCR_EXC_CUR();
scr_exc_active = cell ? cell : &scr_main_exc;
scr_exc_cell_changed();
return prev;
}
/* The ACTIVE cell, for runtime code that moves a pending payload out of it
* (a new-Promise executor throw rejecting the promise). */
ScrExcCell *scr_exc_current_cell(void) { return SCR_EXC_CUR(); }
void scr_stack_enter(ScrStackFrame *frame, const char *text) {
frame->previous = SCR_EXC_CUR()->stack;
frame->text = text;
SCR_EXC_CUR()->stack = frame;
}
void scr_stack_leave(ScrStackFrame *frame) {
SCR_EXC_CUR()->stack = frame->previous;
}
static SCR_TL double scr_stack_limit = 10;
static SCR_TL bool scr_stack_limit_reset_registered;
static void scr_stack_limit_reset(void) {
scr_stack_limit = 10;
scr_stack_limit_reset_registered = false;
}
double scr_stack_limit_get(void) { return scr_stack_limit; }
void scr_stack_limit_set(double limit) {
if (!scr_stack_limit_reset_registered) {
scr_atexit(scr_stack_limit_reset);
scr_stack_limit_reset_registered = true;
}
scr_stack_limit = limit;
}
/* The frames from `first` down, at most Error.stackTraceLimit of them, one
* "\n at ..." line each; NULL when there are none. */
static ScrStr *scr_stack_capture_from(ScrStackFrame *first) {
if (!first) return NULL;
const double limit = floor(scr_stack_limit);
if (!(limit > 0)) return NULL;
size_t length = 0;
size_t count = 0;
for (ScrStackFrame *frame = first; frame && (double)count++ < limit; frame = frame->previous)
length += 1 + strlen(frame->text);
char *text = malloc(length);
if (!text) scr_trap("scriptc: out of memory\n");
size_t offset = 0;
count = 0;
for (ScrStackFrame *frame = first; frame && (double)count++ < limit; frame = frame->previous) {
text[offset++] = '\n';
size_t size = strlen(frame->text);
memcpy(text + offset, frame->text, size);
offset += size;
}
ScrStr *out = scr_str_new(text, length);
free(text);
return out;
}
ScrStr *scr_stack_capture(void) {
return scr_stack_capture_from(SCR_EXC_CUR()->stack);
}
/* The frames below the topmost frame whose text is `through` (a function's
* " at name (file)" line), as Error.captureStackTrace's constructorOpt
* leaves them; NULL when no such frame is on the stack. */
ScrStr *scr_stack_capture_after(const char *through) {
for (ScrStackFrame *frame = SCR_EXC_CUR()->stack; frame; frame = frame->previous)
if (frame->text == through || strcmp(frame->text, through) == 0)
return scr_stack_capture_from(frame->previous);
return NULL;
}
#define scr_exc_kind (SCR_EXC_CUR()->kind)
#define scr_exc_f64 (SCR_EXC_CUR()->f64)
#define scr_exc_bool (SCR_EXC_CUR()->b)
#define scr_exc_payload (SCR_EXC_CUR()->payload)
#define scr_exc_retain_fn (SCR_EXC_CUR()->retain_fn)
#define scr_exc_release_fn (SCR_EXC_CUR()->release_fn)
#define scr_exc_trace_fn (SCR_EXC_CUR()->trace_fn)
/* Parenthesized: scr_runtime.h maps runtime-internal calls to an inline
* test with a function-like macro; this keeps the exported symbol. */
bool(scr_exc_pending)(void) {
#ifdef SCR_WORKERS
if (scr_context_checkpoint()) return true;
#endif
bool pending = scr_exc_kind != SCR_EXC_NONE;
#if defined(SCR_WORKERS) && !defined(SCR_LIB) && defined(SCR_EXC_FLAG)
/* The pending bit is exact; settling it here is only a safety net. */
scr_exc_flag_pending(pending);
#elif defined(SCR_WORKERS) && !defined(SCR_LIB)
/* The slow path of the alert test: settle this thread's share. */
if (pending != scr_exc_alerted) scr_exc_cell_changed();
#endif
return pending;
}
/* Release the current payload (if any) and reset to NONE. Shared by clear,
* the uncaught printer, and every throw (replace semantics). */
static void scr_exc_reset(void) {
if (scr_exc_kind == SCR_EXC_STR) {
scr_str_release((ScrStr *)scr_exc_payload);
} else if (scr_exc_kind == SCR_EXC_REF || scr_exc_kind == SCR_EXC_OBJ ||
scr_exc_kind == SCR_EXC_PRIMITIVE_REF) {
scr_exc_release_fn(scr_exc_payload);
}
scr_exc_kind = SCR_EXC_NONE;
scr_exc_payload = NULL;
scr_exc_retain_fn = NULL;
scr_exc_release_fn = NULL;
scr_exc_trace_fn = NULL;
}
void scr_exc_clear(void) {
scr_exc_reset();
scr_exc_cell_changed();
}
/* ── catch bindings (ScrCaught — contract in scr_runtime.h) ──────────── */
void scr_caught_trace_v(void *object, ScrTraceVisit visit, void *ctx) {
ScrCaught *c = object;
if (c->payload && c->trace_fn) visit(c->payload, ctx);
}
static void scr_caught_gcfree(void *object) {
ScrCaught *c = object;
if (c->kind == SCR_EXC_STR) {
scr_str_release((ScrStr *)c->payload);
} else if (c->payload && c->release_fn && !c->trace_fn) {
c->release_fn(c->payload);
}
scr_obj_free_note();
scr_cyc_free(c);
}
ScrCaught *scr_exc_take(void) {
static ScrCycType cyc_type = { &scr_caught_trace_v, &scr_caught_gcfree, SCR_CYC_KIND_CAUGHT };
ScrCaught *c = scr_cyc_alloc(sizeof *c, &cyc_type);
c->rc = 1;
c->kind = scr_exc_kind;
c->f64 = scr_exc_f64;
c->b = scr_exc_bool;
c->payload = scr_exc_payload; /* ownership MOVES from the cell */
c->retain_fn = scr_exc_retain_fn;
c->release_fn = scr_exc_release_fn;
c->trace_fn = scr_exc_trace_fn;
scr_exc_kind = SCR_EXC_NONE; /* clear WITHOUT releasing (moved) */
scr_exc_payload = NULL;
scr_exc_retain_fn = NULL;
scr_exc_release_fn = NULL;
scr_exc_trace_fn = NULL;
scr_exc_cell_changed();
scr_obj_alloc_note();
return c;
}
ScrCaught *scr_caught_retain(ScrCaught *c) {
if (c) c->rc++;
return c;
}
void scr_caught_release(ScrCaught *c) {
if (!c) return;
if (--c->rc == 0) {
scr_cyc_dead(c);
if (c->kind == SCR_EXC_STR) {
scr_str_release((ScrStr *)c->payload);
} else if (c->kind == SCR_EXC_REF || c->kind == SCR_EXC_OBJ ||
c->kind == SCR_EXC_PRIMITIVE_REF) {
c->release_fn(c->payload);
}
scr_obj_free_note();
scr_cyc_free(c);
} else {
scr_cyc_released(c);
}
}
void scr_rethrow(const ScrCaught *c) {
switch (c->kind) {
case SCR_EXC_F64: scr_throw_f64(c->f64); break;
case SCR_EXC_BOOL: scr_throw_bool(c->b); break;
case SCR_EXC_STR: scr_throw_str(scr_str_retain((ScrStr *)c->payload)); break;
case SCR_EXC_REF:
scr_throw_ref(c->retain_fn(c->payload), c->retain_fn, c->release_fn, c->trace_fn);
break;
case SCR_EXC_PRIMITIVE_REF:
scr_throw_primitive_ref(c->retain_fn(c->payload), c->retain_fn, c->release_fn, c->trace_fn);
break;
case SCR_EXC_OBJ:
scr_throw_obj(c->retain_fn(c->payload), c->retain_fn, c->release_fn, c->trace_fn);
break;
case SCR_EXC_NONE:
break; /* unreachable: take() never stores NONE */
case SCR_EXC_TERMINATE:
case SCR_EXC_GENRET:
/* The generator-return sentinel, stashed across an exception-path
* finally (emitTryCatch): re-raising restores the bare kind — no
* payload exists. */
scr_exc_reset();
scr_exc_kind = c->kind;
SCR_EXC_RAISED();
break;
}
}
/* Explicit resource management's error merge: a SuppressedError whose
* `error` is the disposal error and whose `suppressed` is the body error.
* The snapshots wait in this table for the error's dyn view, so programs
* that never read them link no checked-dynamic code. */
typedef struct ScrSuppressedEntry {
const void *error_object;
ScrCaught *error;
ScrCaught *suppressed;
} ScrSuppressedEntry;
static SCR_TL ScrSuppressedEntry *scr_suppressed_entries = NULL;
static SCR_TL size_t scr_suppressed_len = 0;
static SCR_TL size_t scr_suppressed_cap = 0;
bool scr_exc_suppressed_values(const void *error_object, const ScrCaught **error,
const ScrCaught **suppressed) {
for (size_t i = 0; i < scr_suppressed_len; i++) {
if (scr_suppressed_entries[i].error_object != error_object) continue;
*error = scr_suppressed_entries[i].error;
*suppressed = scr_suppressed_entries[i].suppressed;
return true;
}
return false;
}
void scr_exc_suppressed_forget(const void *error_object) {
for (size_t i = 0; i < scr_suppressed_len; i++) {
if (scr_suppressed_entries[i].error_object != error_object) continue;
ScrSuppressedEntry entry = scr_suppressed_entries[i];
scr_suppressed_entries[i] = scr_suppressed_entries[--scr_suppressed_len];
scr_caught_release(entry.error);
scr_caught_release(entry.suppressed);
return;
}
}
void scr_exc_keep_first(ScrCaught *first) {
if (first->kind == SCR_EXC_GENRET) {
scr_caught_release(first);
return;
}
ScrCaught *dropped = scr_exc_take();
if (dropped) scr_caught_release(dropped);
scr_rethrow(first);
scr_caught_release(first);
}
void scr_exc_suppress(ScrCaught *suppressed) {
if (suppressed->kind == SCR_EXC_GENRET) {
/* A generator .return() is a return completion, not a thrown error.
* A failing disposer replaces it directly. */
scr_caught_release(suppressed);
return;
}
ScrCaught *error = scr_exc_take();
if (!error) {
scr_rethrow(suppressed);
scr_caught_release(suppressed);
return;
}
static const char text[] = "An error was suppressed during disposal";
ScrStr *message = scr_str_new(text, sizeof text - 1);
ScrError *e = scr_error_new(SCR_ERR_SUPPRESSED, message);
scr_str_release(message);
if (scr_suppressed_len == scr_suppressed_cap) {
size_t cap = scr_suppressed_cap ? scr_suppressed_cap * 2 : 4;
ScrSuppressedEntry *entries = realloc(scr_suppressed_entries, cap * sizeof *entries);
if (!entries) {
fputs("scriptc: out of memory\n", stderr);
abort();
}
scr_suppressed_entries = entries;
scr_suppressed_cap = cap;
}
scr_suppressed_entries[scr_suppressed_len++] = (ScrSuppressedEntry){e, error, suppressed};
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
}
bool scr_caught_instanceof(const ScrCaught *c, size_t pre, size_t post) {
if (c->kind != SCR_EXC_OBJ) return false;
/* Every hierarchy class shares the rc+vt prefix; ScrError names it. */
const ScrVt *vt = ((const ScrError *)c->payload)->vt;
return pre <= vt->pre && vt->pre <= post;
}
bool scr_caught_is_object(const ScrCaught *c) {
return c->kind == SCR_EXC_OBJ || c->kind == SCR_EXC_REF;
}
ScrStr *scr_caught_to_string(const ScrCaught *c) {
switch (c->kind) {
case SCR_EXC_F64: return scr_f64_to_scrstr(c->f64);
case SCR_EXC_BOOL: return scr_bool_to_scrstr(c->b);
case SCR_EXC_STR: return scr_str_retain((ScrStr *)c->payload);
case SCR_EXC_OBJ:
/* Error instances stringify by Error.prototype.toString — "name",
* "name: message", or "Error" for the empty halves. This IS Node's
* String(e): the stack is a property Node's console printing adds,
* never part of toString. Non-Error hierarchy objects fall through. */
if (scr_error_is(c->payload)) {
return scr_error_to_string((ScrError *)c->payload);
}
/* fall through */
case SCR_EXC_REF:
case SCR_EXC_PRIMITIVE_REF:
if (c->retain_fn == scr_dyn_retain_v) return scr_dyn_string_constructor(c->payload);
/* Object.prototype.toString — exact for thrown class instances
* without a toString override; thrown closures and union boxes print
* this too where Node would vary. Thrown records and arrays arrive as
* checked-dynamic values (above). */
return scr_str_new("[object Object]", 15);
case SCR_EXC_NONE:
case SCR_EXC_TERMINATE:
case SCR_EXC_GENRET: /* unreachable: take() never stores NONE/the sentinel */
break;
}
return scr_str_new("", 0);
}
#ifdef SCR_LIB
/* An exception that escaped user code into a generated entry wrapper: the
* host contract is that declared entries do not throw, so an escape is a
* contract-shaped failure exactly like a trap. Render the same "Uncaught
* ..." first line the executable epilogue prints (scr_exc_print_uncaught's
* arms, buffer-writing), release the payload, and route the text through
* the trap funnel — one failure channel at the boundary. The funnel
* (scr_library.c) assembles the rendered line into the structured
* trap-teaching form (code SC4013, the trapping entry's symbol) before
* delivery; only the 0x01-led verbatim path below bypasses assembly. */
void scr_library_check_exc(void) {
if (!scr_exc_pending()) return;
static SCR_TL char buf[1024]; /* the message is copied out before the payload dies */
/* The ratified verbatim rule: a thrown message that ALREADY begins with
* the structured trap-teaching marker (0x01) is delivered exactly as
* authored — no "Uncaught " prefix, no added newline — which is how a
* facade-authored structured teaching rides the throw channel through to
* the sink. Both throw idioms qualify: a thrown string, and an Error
* whose .message starts with the marker. Everything else keeps the
* baseline "Uncaught ..." shape below, whose first byte is printable —
* the guarantee the marker's unambiguity rests on. */
const ScrStr *marked = NULL;
if (scr_exc_kind == SCR_EXC_STR) {
marked = (const ScrStr *)scr_exc_payload;
} else if (scr_exc_kind == SCR_EXC_OBJ && scr_error_is(scr_exc_payload)) {
marked = ((const ScrError *)scr_exc_payload)->message;
}
if (marked != NULL && marked->len > 0 && (uint8_t)marked->data[0] == 0x01) {
size_t take = marked->len > sizeof buf ? sizeof buf : marked->len;
memcpy(buf, marked->data, take);
scr_exc_reset(); /* the payload is released before the funnel poisons */
scr_trap_len(buf, take);
}
size_t n = 0;
const char prefix[] = "Uncaught ";
memcpy(buf, prefix, sizeof prefix - 1);
n = sizeof prefix - 1;
const size_t cap = sizeof buf - 2; /* room for '\n' + NUL */
switch (scr_exc_kind) {
case SCR_EXC_F64:
n += scr_f64_to_str(scr_exc_f64, buf + n);
break;
case SCR_EXC_BOOL: {
const char *b = scr_exc_bool ? "true" : "false";
size_t bl = strlen(b);
memcpy(buf + n, b, bl);
n += bl;
break;
}
case SCR_EXC_STR: {
const ScrStr *s = (const ScrStr *)scr_exc_payload;
size_t take = s->len > cap - n ? cap - n : s->len;
memcpy(buf + n, s->data, take);
n += take;
break;
}
case SCR_EXC_OBJ:
if (scr_error_is(scr_exc_payload)) {
ScrStr *s = scr_error_to_string((ScrError *)scr_exc_payload);
size_t take = s->len > cap - n ? cap - n : s->len;
memcpy(buf + n, s->data, take);
n += take;
scr_str_release(s);
break;
}
/* fall through: non-Error hierarchy objects render like other refs */
case SCR_EXC_REF:
case SCR_EXC_PRIMITIVE_REF: {
const char obj[] = "[object]";
memcpy(buf + n, obj, sizeof obj - 1);
n += sizeof obj - 1;
break;
}
case SCR_EXC_NONE:
case SCR_EXC_GENRET: { /* unreachable through the pending() gate */
const char none[] = "(no exception)";
memcpy(buf + n, none, sizeof none - 1);
n += sizeof none - 1;
break;
}
}
buf[n++] = '\n';
buf[n] = '\0';
scr_exc_reset(); /* the payload is released before the funnel poisons */
scr_trap(buf);
}
#endif /* SCR_LIB */
void scr_throw_f64(double v) {
scr_exc_reset();
scr_exc_kind = SCR_EXC_F64;
scr_exc_f64 = v;
SCR_EXC_RAISED();
}
void scr_throw_bool(bool v) {
scr_exc_reset();
scr_exc_kind = SCR_EXC_BOOL;
scr_exc_bool = v;
SCR_EXC_RAISED();
}
void scr_throw_str(ScrStr *v) {
scr_exc_reset();
scr_exc_kind = SCR_EXC_STR;
scr_exc_payload = v; /* ownership moves in */
SCR_EXC_RAISED();
}
void scr_throw_ref(void *v, void *(*retain)(void *), void (*release)(void *),
ScrTraceFn trace) {
scr_exc_reset();
scr_exc_kind = SCR_EXC_REF;
scr_exc_payload = v; /* ownership moves in */
scr_exc_retain_fn = retain;
scr_exc_release_fn = release;
scr_exc_trace_fn = trace;
SCR_EXC_RAISED();
}
void scr_throw_primitive_ref(void *v, void *(*retain)(void *), void (*release)(void *),
ScrTraceFn trace) {
scr_throw_ref(v, retain, release, trace);
scr_exc_kind = SCR_EXC_PRIMITIVE_REF;
}
void scr_throw_ref_classified(void *v, void *(*retain)(void *), void (*release)(void *),
ScrTraceFn trace, bool object) {
scr_throw_ref(v, retain, release, trace);
if (!object) scr_exc_kind = SCR_EXC_PRIMITIVE_REF;
}
void scr_throw_obj(void *v, void *(*retain)(void *), void (*release)(void *),
ScrTraceFn trace) {
scr_throw_ref(v, retain, release, trace);
scr_exc_kind = SCR_EXC_OBJ;
}
/* The abnormal-exit code hint: main returns 1 after an uncaught throw or
* a reported unhandled rejection, and the process 'exit' listeners (the
* events unit's atexit, when linked) must see that code, like Node's.
* Lives HERE so the unit is self-contained (the reporters below and in
* scr_async.c both note it; scr_events.c reads it). */
static SCR_TL int scr_exit_code_hint = 0;
void scr_exit_code_note(int code) { scr_exit_code_hint = code; }
int scr_exit_code_hint_get(void) { return scr_exit_code_hint; }
SCR_TL int (*scr_uncaught_exception_hook)(bool from_promise) = NULL;
static SCR_TL bool scr_uncaught_handler_failed = false;
bool scr_exc_handle_uncaught(bool from_promise) {
#ifdef SCR_WORKERS
/* Checkpoint, not just test: the unwind that brought control here may
* have been a stop request the cell does not show yet. */
if (scr_context_checkpoint()) return false;
#endif
if (!scr_exc_pending()) return true;
if (!scr_uncaught_exception_hook || scr_uncaught_handler_failed) return false;
int result = scr_uncaught_exception_hook(from_promise);
if (result < 0) scr_uncaught_handler_failed = true;
return result > 0;
}
static bool scr_uncaught_print_dyn_primitive(const ScrDyn *d) {
switch (d->kind) {
case SCR_DYN_STR:
fwrite(d->v.str->data, 1, d->v.str->len, stderr);
return true;
case SCR_DYN_NUM: {
char buf[32];
size_t len = scr_f64_to_str(d->v.num, buf);
fwrite(buf, 1, len, stderr);
return true;
}
case SCR_DYN_BOOL:
fputs(d->v.b ? "true" : "false", stderr);
return true;
case SCR_DYN_NULL:
fputs("null", stderr);
return true;
case SCR_DYN_UNDEF:
fputs("undefined", stderr);
return true;
default:
return false;
}
}
/* An SC diagnostic code ("SC" and four digits) that `text` does not
* already name as "[SCnnnn". */
static bool scr_uncaught_unnamed_sc_code(const ScrStr *code, const ScrStr *text) {
if (code == NULL || code->len != 6 || code->data[0] != 'S' || code->data[1] != 'C') return false;
for (size_t i = 2; i < 6; i++)
if (code->data[i] < '0' || code->data[i] > '9') return false;
for (size_t i = 0; i + 7 <= text->len; i++)
if (text->data[i] == '[' && memcmp(text->data + i + 1, code->data, 6) == 0) return false;
return true;
}
void scr_exc_print_uncaught(void) {
#ifdef SCR_WORKERS
if (scr_context_checkpoint()) { scr_exc_clear(); return; }
if (scr_context_report_error) {
scr_exit_code_note(scr_uncaught_handler_failed ? 7 : 1);
scr_context_report_error();
return;
}
#endif
scr_exit_code_note(scr_uncaught_handler_failed ? 7 : 1);
/* Settle any runtime-internal stdout fragment before the stderr line when
* both share an fd. JavaScript-visible writes already flush themselves. */
fflush(stdout);
#ifdef SCR_NODE_ISLANDS
/* An error thrown by Node island code: Node's own report (the throw
* site's source line, the stack, the Node.js version). */
if (scr_exc_kind == SCR_EXC_OBJ && scr_error_is(scr_exc_payload) &&
scr_ni_report_uncaught((ScrError *)scr_exc_payload)) {
scr_exc_reset(); /* the payload counts as live until released */
scr_exc_cell_changed();
return;
}
#endif
fputs("Uncaught ", stderr);
switch (scr_exc_kind) {
case SCR_EXC_F64: {
char buf[32];
size_t len = scr_f64_to_str(scr_exc_f64, buf);
fwrite(buf, 1, len, stderr);
break;
}
case SCR_EXC_BOOL:
fputs(scr_exc_bool ? "true" : "false", stderr);
break;
case SCR_EXC_STR: {
const ScrStr *s = (const ScrStr *)scr_exc_payload;
fwrite(s->data, 1, s->len, stderr);
break;
}
case SCR_EXC_OBJ:
/* Error instances (builtin or user subclass) render like Node's first
* line: "name: message" (toString rules for the empty halves). Node
* appends a stack trace; scriptc has none (documented divergence). */
if (scr_error_is(scr_exc_payload)) {
const ScrError *error = (const ScrError *)scr_exc_payload;
ScrStr *s = scr_error_to_string((ScrError *)scr_exc_payload);
fwrite(s->data, 1, s->len, stderr);
/* A runtime refusal carries its scriptc diagnostic code on `code`;
* name it unless the message already does, as deferred compile-time
* refusals' "[SC1090 at file:line]" suffix does. */
if (scr_uncaught_unnamed_sc_code(error->code, s))
fprintf(stderr, " [%.*s]", (int)error->code->len, error->code->data);
scr_str_release(s);
break;
}
/* fall through: non-Error hierarchy objects render like other refs */
case SCR_EXC_REF:
case SCR_EXC_PRIMITIVE_REF:
/* A checked-dynamic value (`throw v` with v: unknown) prints its
* primitive the way Node's uncaught report does — a string's own
* text, numbers/booleans/null/undefined by their names. */
if (scr_exc_release_fn == scr_dyn_release_v && scr_exc_payload != NULL &&
scr_uncaught_print_dyn_primitive((const ScrDyn *)scr_exc_payload))
break;
fputs("[object]", stderr);
break;
case SCR_EXC_NONE: /* main only calls this when pending */
case SCR_EXC_GENRET: /* unreachable: the trampoline consumes the sentinel */
case SCR_EXC_TERMINATE:
fputs("(no exception)", stderr);
break;
}
fputc('\n', stderr);
scr_exc_reset(); /* the payload counts as live until released */
scr_exc_cell_changed();
}