mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
7213 lines
273 KiB
C
7213 lines
273 KiB
C
/* Async runtime: stackful fibers, promises, and a dependency-free event
|
|
* loop (microtask queue + timer min-heap).
|
|
*
|
|
* Model (see docs/ir.md):
|
|
* - An async function's body is ordinary compiled C, run on its own fiber
|
|
* (dedicated stack). Calling it runs the body EAGERLY until
|
|
* the first suspension (JS's synchronous-prefix rule), then control
|
|
* returns to the spawner with a +1 promise.
|
|
* - `await` on a pending promise parks the fiber on the promise's waiter
|
|
* list and switches back to whoever resumed it. Fulfillment moves waiters
|
|
* to the microtask queue (microtasks run before timers, like Node).
|
|
* - Each fiber carries its own exception cell (scr_exc_swap_cell) so a
|
|
* pending exception can't leak across concurrent execution contexts. A
|
|
* throw escaping an async body rejects its promise; awaiting a rejected
|
|
* promise re-throws into the awaiter.
|
|
* - Loop exhaustion with still-suspended fibers exits normally (Node's
|
|
* behavior); those stacks are deliberately not unwound, and the RC audit
|
|
* downgrades to a note in that case.
|
|
*/
|
|
#define _XOPEN_SOURCE 700
|
|
/* Anonymous mappings alongside the XSI ucontext API on Darwin and glibc. */
|
|
#ifndef _DARWIN_C_SOURCE
|
|
#define _DARWIN_C_SOURCE 1
|
|
#endif
|
|
#ifndef _DEFAULT_SOURCE
|
|
#define _DEFAULT_SOURCE 1
|
|
#endif
|
|
#include "scr_runtime.h"
|
|
#ifdef SCR_NODE_ISLANDS
|
|
#include "scr_node_bridge.h"
|
|
#endif
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#ifdef _WIN32
|
|
/* Windows arm: fibers come from the Win32 Fibers API (CreateFiberEx /
|
|
* SwitchToFiber — the direct ucontext analog: cooperatively scheduled,
|
|
* same thread, own stack); the idle sleep is nanosleep (mingw-w64 ships
|
|
* it, over Sleep). poll(2) has no Windows arm — see the sleep seam in
|
|
* scr_loop_run: the poller-backed units (net/dgram/watch) are not built
|
|
* for win32 targets (native-toolchain.ts gates them), so their fds never appear; the
|
|
* events unit DOES cross-compile (scr_events.c's win32 arm) and is
|
|
* served by a capped nanosleep — dispatch at the next turn's top, the
|
|
* cap bounding signal/stdin latency instead of a pollable wake fd. */
|
|
#include <windows.h>
|
|
#elif defined(__wasi__)
|
|
#include <poll.h>
|
|
#else
|
|
#include <poll.h>
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <sys/mman.h>
|
|
#include <ucontext.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#if defined(__has_feature)
|
|
#if __has_feature(address_sanitizer)
|
|
#define SCR_ASAN_FIBERS 1
|
|
void __sanitizer_start_switch_fiber(void **fake_stack_save, const void *bottom, size_t size);
|
|
void __sanitizer_finish_switch_fiber(void *fake_stack_save, const void **bottom_old, size_t *size_old);
|
|
/* LeakSanitizer scans thread stacks, not the mapped fiber stacks, so a
|
|
* suspended fiber's locals would look unreferenced at exit. Each stack is
|
|
* a root region while it exists. Weak: platforms whose ASan runtime has
|
|
* no leak checker leave them NULL. */
|
|
void __lsan_register_root_region(const void *p, size_t size) __attribute__((weak));
|
|
void __lsan_unregister_root_region(const void *p, size_t size) __attribute__((weak));
|
|
#endif
|
|
#endif
|
|
|
|
|
|
#if !defined(_WIN32) && !defined(__wasi__)
|
|
/* Keep stacks out of malloc's size classes. Reuse a bounded number during
|
|
* a burst of promise/generator work, then unmap the spares before the loop
|
|
* sleeps. This avoids both heap fragmentation and per-call mmap overhead.
|
|
* The sanitizer lane unmaps every finished stack immediately. */
|
|
#ifndef SCR_ASAN_FIBERS
|
|
#define SCR_FIBER_SPARES 64
|
|
static SCR_TL void *scr_fiber_spares[SCR_FIBER_SPARES];
|
|
static SCR_TL size_t scr_fiber_nspares;
|
|
static SCR_TL bool scr_fiber_cleanup_registered;
|
|
#endif
|
|
|
|
static void scr_fiber_stacks_clear(void) {
|
|
#ifndef SCR_ASAN_FIBERS
|
|
while (scr_fiber_nspares > 0) {
|
|
(void)munmap(scr_fiber_spares[--scr_fiber_nspares], SCR_FIBER_STACK);
|
|
scr_fiber_spares[scr_fiber_nspares] = NULL;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void *scr_fiber_stack_new(void) {
|
|
#ifndef SCR_ASAN_FIBERS
|
|
if (scr_fiber_nspares > 0) {
|
|
void *stack = scr_fiber_spares[--scr_fiber_nspares];
|
|
scr_fiber_spares[scr_fiber_nspares] = NULL;
|
|
return stack;
|
|
}
|
|
if (!scr_fiber_cleanup_registered) {
|
|
scr_fiber_cleanup_registered = true;
|
|
scr_atexit(scr_fiber_stacks_clear);
|
|
}
|
|
#endif
|
|
void *stack = mmap(NULL, SCR_FIBER_STACK, PROT_READ | PROT_WRITE,
|
|
MAP_PRIVATE | MAP_ANON, -1, 0);
|
|
if (stack == MAP_FAILED) scr_trap("scriptc: out of memory\n");
|
|
#ifdef SCR_ASAN_FIBERS
|
|
if (__lsan_register_root_region) __lsan_register_root_region(stack, SCR_FIBER_STACK);
|
|
#endif
|
|
return stack;
|
|
}
|
|
|
|
static void scr_fiber_stack_free(void *stack) {
|
|
if (!stack) return;
|
|
#ifndef SCR_ASAN_FIBERS
|
|
if (scr_fiber_nspares < SCR_FIBER_SPARES) {
|
|
scr_fiber_spares[scr_fiber_nspares++] = stack;
|
|
return;
|
|
}
|
|
#endif
|
|
#ifdef SCR_ASAN_FIBERS
|
|
if (__lsan_unregister_root_region) __lsan_unregister_root_region(stack, SCR_FIBER_STACK);
|
|
#endif
|
|
(void)munmap(stack, SCR_FIBER_STACK);
|
|
}
|
|
#endif
|
|
|
|
/* ── promises ─────────────────────────────────────────────────────────── */
|
|
|
|
enum { SCR_PROM_PENDING = 0, SCR_PROM_FULFILLED = 1, SCR_PROM_REJECTED = 2 };
|
|
|
|
#define SCR_CB_SYNC SIZE_MAX
|
|
#ifdef SCR_NODE_ISLANDS
|
|
#define SCR_CB_ANY SIZE_MAX /* all_idx of an any-settlement callback */
|
|
#endif
|
|
#define SCR_CB_BATCH (SIZE_MAX - 1)
|
|
|
|
typedef struct ScrFiber ScrFiber;
|
|
|
|
typedef struct { size_t rc; } ScrPromiseIdentity;
|
|
|
|
struct ScrPromise {
|
|
size_t rc; /* The cycle collector reads the reference count at offset zero. */
|
|
/* Shared identity cell for promises adopted across inner types (NULL
|
|
* until scr_promise_share_identity first links this promise). */
|
|
ScrPromiseIdentity *identity;
|
|
int state;
|
|
bool top_level_handled; /* uncaughtException handled entry evaluation */
|
|
/* Unhandled-rejection tracking: set when rejected, cleared on await. */
|
|
bool rejection_observed;
|
|
/* Set when the checkpoint report delivered THIS promise to
|
|
* 'unhandledRejection' listeners — a handler attached after that is
|
|
* Node's 'rejectionHandled' moment (scr_prom_observe below). */
|
|
bool reported_unhandled;
|
|
bool b; /* the boolean payload */
|
|
/* Payload (fulfillment value or rejection reason), ScrExcCell-style.
|
|
* trace_fn is non-NULL iff the REF payload type carries a cycle header
|
|
* (a promise settled with a cycle-capable value can be a cycle member —
|
|
* e.g. a closure that captures a box holding this very promise). */
|
|
ScrExcKind payload_kind; /* NONE = void fulfillment */
|
|
double f64;
|
|
void *payload;
|
|
void *(*retain_fn)(void *);
|
|
void (*release_fn)(void *);
|
|
ScrTraceFn trace_fn;
|
|
/* Fibers parked on this promise. */
|
|
ScrFiber **waiters; /* waiter_inline until a second fiber parks */
|
|
size_t nwaiters, waiters_cap;
|
|
ScrFiber *waiter_inline;
|
|
/* Callback waiters: fulfilled promises run adapt(dst, this) — an
|
|
* emitted adapter converting the payload into the destination's inner
|
|
* type — and rejections copy raw (reasons are dynamically tagged). Each
|
|
* entry owns a reference on its dst. Promise.all entries carry the
|
|
* shared countdown state (`all` non-NULL, `all_idx` the entry's INPUT
|
|
* index); the others leave both zeroed and keep the adapt dispatch.
|
|
* Combinator entries (Promise.all / Promise.race) are reaction jobs:
|
|
* `job_at` is the number of waiters parked before the entry subscribed,
|
|
* so settlement queues the job in subscription order among the waking
|
|
* fibers. Conversions (SCR_CB_SYNC) run inside the settle itself. */
|
|
struct ScrPromiseCbWaiter {
|
|
void (*adapt)(ScrPromise *dst, ScrPromise *src);
|
|
ScrPromise *dst;
|
|
struct ScrAllState *all;
|
|
size_t all_idx;
|
|
size_t job_at;
|
|
} *cbs;
|
|
size_t ncbs, cbs_cap;
|
|
};
|
|
/* The flags share the word after `state`, so a promise and its cycle header
|
|
* take a 144-byte block rather than a 160-byte one (one per pending
|
|
* `await`). */
|
|
#if UINTPTR_MAX == UINT64_MAX
|
|
_Static_assert(sizeof(ScrPromise) == 128, "ScrPromise packs its flags after state");
|
|
#endif
|
|
|
|
#ifdef SCR_RC_AUDIT
|
|
static SCR_TL long scr_live_promises = 0;
|
|
long scr_promise_live_count(void) { return scr_live_promises; }
|
|
#endif
|
|
|
|
/* ── Promise.all shared state ─────────────────────────────────────────
|
|
* One per Promise.all call: the values array filled per INPUT index as
|
|
* entries fulfill, the countdown of fulfillments still missing, and the
|
|
* per-element-kind store helper. The state holds +1 on `values` (NULL for
|
|
* void-element all) and is itself refcounted by the entries that still
|
|
* point at it (parked cb waiters plus the builder while it subscribes);
|
|
* the RESULT promise is NOT held here — each parked entry's cb.dst is the
|
|
* retained result, so promise teardown paths need no all-specific
|
|
* destination handling. */
|
|
typedef struct ScrAllState {
|
|
size_t rc;
|
|
size_t remaining;
|
|
ScrArr *values;
|
|
ScrAllStore store;
|
|
int mode; /* SCR_COMBINE_* */
|
|
ScrAnyFail fail; /* Promise.any's exhaustion (rejects the result) */
|
|
} ScrAllState;
|
|
|
|
static void scr_promise_all_state_release(ScrAllState *st) {
|
|
if (--st->rc == 0) {
|
|
if (st->values) scr_arr_release(st->values);
|
|
scr_mem_free(st);
|
|
}
|
|
}
|
|
|
|
static void scr_oom(void) {
|
|
fputs("scriptc: out of memory\n", stderr);
|
|
abort();
|
|
}
|
|
|
|
static void scr_promise_free_side(ScrPromise *p) {
|
|
if (p->identity && --p->identity->rc == 0) free(p->identity);
|
|
/* Most promises never park a fiber or a second waiter: skip the calls. */
|
|
if (p->waiters && p->waiters != &p->waiter_inline) free(p->waiters);
|
|
}
|
|
|
|
static void scr_promise_add_waiter(ScrPromise *p, ScrFiber *f) {
|
|
if (p->nwaiters == p->waiters_cap) {
|
|
if (p->waiters_cap == 0) {
|
|
p->waiters = &p->waiter_inline;
|
|
p->waiters_cap = 1;
|
|
} else {
|
|
size_t cap = p->waiters_cap < 4 ? 4 : p->waiters_cap * 2;
|
|
ScrFiber **grown = p->waiters == &p->waiter_inline
|
|
? malloc(cap * sizeof *grown)
|
|
: realloc(p->waiters, cap * sizeof *grown);
|
|
if (!grown) scr_oom();
|
|
if (p->waiters == &p->waiter_inline) grown[0] = p->waiter_inline;
|
|
p->waiters = grown;
|
|
p->waiters_cap = cap;
|
|
}
|
|
}
|
|
p->waiters[p->nwaiters++] = f;
|
|
}
|
|
|
|
static bool scr_promise_has_traced_children(const ScrPromise *p) {
|
|
return p->ncbs != 0 ||
|
|
((p->payload_kind == SCR_EXC_REF || p->payload_kind == SCR_EXC_OBJ ||
|
|
p->payload_kind == SCR_EXC_PRIMITIVE_REF) &&
|
|
p->trace_fn && p->payload);
|
|
}
|
|
|
|
static void scr_promise_trace(void *o, ScrTraceVisit visit, void *ctx) {
|
|
ScrPromise *p = (ScrPromise *)o;
|
|
/* Waiters are fibers (not refcounted objects) — the settled payload and
|
|
* the combinator destinations are the strong references this promise
|
|
* owns (destination promises are cycle-headered). */
|
|
if ((p->payload_kind == SCR_EXC_REF || p->payload_kind == SCR_EXC_OBJ ||
|
|
p->payload_kind == SCR_EXC_PRIMITIVE_REF) &&
|
|
p->trace_fn) {
|
|
visit(p->payload, ctx);
|
|
}
|
|
for (size_t i = 0; i < p->ncbs; i++) visit(p->cbs[i].dst, ctx);
|
|
}
|
|
|
|
/* Teardown for the collector: release the payload only when the trace does
|
|
* NOT visit it (the complement — see the contract in scr_runtime.h). The
|
|
* cb destinations are always visited, so only the array frees here. */
|
|
static void scr_promise_gcfree(void *o) {
|
|
ScrPromise *p = (ScrPromise *)o;
|
|
if (p->payload_kind == SCR_EXC_STR) {
|
|
scr_str_release((ScrStr *)p->payload);
|
|
} else if ((p->payload_kind == SCR_EXC_REF || p->payload_kind == SCR_EXC_OBJ ||
|
|
p->payload_kind == SCR_EXC_PRIMITIVE_REF) &&
|
|
p->payload && !p->trace_fn) {
|
|
p->release_fn(p->payload);
|
|
}
|
|
/* Parked Promise.all states are NOT traced children (their values array
|
|
* is reachable only through them) — release like the untraced payload
|
|
* above; the cb destinations stay untouched (trace visits them). */
|
|
for (size_t i = 0; i < p->ncbs; i++) {
|
|
if (p->cbs[i].all) scr_promise_all_state_release(p->cbs[i].all);
|
|
}
|
|
scr_promise_free_side(p);
|
|
if (p->cbs) free(p->cbs);
|
|
#ifdef SCR_RC_AUDIT
|
|
scr_live_promises--;
|
|
#endif
|
|
scr_cyc_free_inline(p);
|
|
}
|
|
|
|
ScrPromise *scr_promise_new(void) {
|
|
static ScrCycType cyc_type = { &scr_promise_trace, &scr_promise_gcfree, SCR_CYC_KIND_PROMISE };
|
|
ScrPromise *p = scr_cyc_alloc_inline(sizeof *p, &cyc_type); /* constant size: zeroing inlines */
|
|
p->rc = 1;
|
|
#ifdef SCR_RC_AUDIT
|
|
scr_live_promises++;
|
|
#endif
|
|
return p;
|
|
}
|
|
|
|
void scr_promise_share_identity(ScrPromise *dst, ScrPromise *src) {
|
|
if (dst == src) return;
|
|
if (!src->identity) {
|
|
src->identity = malloc(sizeof *src->identity);
|
|
if (!src->identity) scr_oom();
|
|
src->identity->rc = 1;
|
|
}
|
|
if (dst->identity == src->identity) return;
|
|
if (dst->identity && --dst->identity->rc == 0) free(dst->identity);
|
|
dst->identity = src->identity;
|
|
dst->identity->rc++;
|
|
}
|
|
|
|
bool scr_promise_identity_equal(ScrPromise *a, ScrPromise *b) {
|
|
return a == b || (a->identity != NULL && a->identity == b->identity);
|
|
}
|
|
|
|
/* The 'rejectionHandled' hook (scr_async_dyn.c installs it at listener
|
|
* registration — the scr_urj_deliver_fn pattern, so listener-free
|
|
* binaries keep their size class): called when a promise the checkpoint
|
|
* report already delivered as unhandled gains a handler. */
|
|
SCR_TL void (*scr_rjh_notify_fn)(ScrPromise *p) = NULL;
|
|
|
|
/* Every handler attach funnels here: mark the rejection observed, and
|
|
* fire Node's 'rejectionHandled' when the attach arrived AFTER the
|
|
* report delivered this promise to 'unhandledRejection' listeners (the
|
|
* model's one late-handling window — earlier handling keeps the promise
|
|
* out of the report entirely). The flag clears on the first attach, so
|
|
* one report fires at most one 'rejectionHandled' — Node's pairing. */
|
|
static void scr_prom_observe(ScrPromise *p) {
|
|
p->rejection_observed = true;
|
|
if (p->reported_unhandled) {
|
|
p->reported_unhandled = false;
|
|
if (scr_rjh_notify_fn != NULL) scr_rjh_notify_fn(p);
|
|
}
|
|
}
|
|
|
|
/* The attach-time handled mark (scr_async_dyn.c's dyn then/catch with a
|
|
* rejection handler, plus the module loader's ownership of every module
|
|
* evaluation promise): Node marks a promise handled at ATTACH, including
|
|
* while it is still pending, not when a later reaction runs. The pending
|
|
* mark is essential for a module dependency whose importer aborts while
|
|
* evaluating a later sibling — the loader still owns that dependency's
|
|
* eventual rejection, so it must never surface as an unrelated unhandled
|
|
* rejection. */
|
|
void scr_promise_mark_handled(ScrPromise *p) {
|
|
scr_prom_observe(p);
|
|
}
|
|
|
|
ScrPromise *scr_promise_retain(ScrPromise *p) {
|
|
if (p && p->rc != SIZE_MAX) {
|
|
p->rc++;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
static void scr_promise_release_payload(ScrPromise *p) {
|
|
if (p->payload_kind == SCR_EXC_STR) scr_str_release((ScrStr *)p->payload);
|
|
else if ((p->payload_kind == SCR_EXC_REF || p->payload_kind == SCR_EXC_OBJ ||
|
|
p->payload_kind == SCR_EXC_PRIMITIVE_REF) &&
|
|
p->payload) p->release_fn(p->payload);
|
|
p->payload_kind = SCR_EXC_NONE;
|
|
p->payload = NULL;
|
|
}
|
|
|
|
void scr_promise_release(ScrPromise *p) {
|
|
if (!p || p->rc == SIZE_MAX) return;
|
|
if (--p->rc == 0) {
|
|
scr_cyc_dead(p);
|
|
scr_promise_release_payload(p);
|
|
scr_promise_free_side(p);
|
|
for (size_t i = 0; i < p->ncbs; i++) {
|
|
scr_promise_release(p->cbs[i].dst);
|
|
if (p->cbs[i].all) scr_promise_all_state_release(p->cbs[i].all);
|
|
}
|
|
if (p->cbs) free(p->cbs);
|
|
#ifdef SCR_RC_AUDIT
|
|
scr_live_promises--;
|
|
#endif
|
|
scr_cyc_free_inline(p);
|
|
} else if (scr_promise_has_traced_children(p)) {
|
|
scr_cyc_released(p); /* possible cycle root; may collect — p is done */
|
|
}
|
|
/* A promise the trace walks nothing from cannot be on a cycle at this
|
|
* decrement, so it is not a candidate root. If it later gains traced
|
|
* children (a cycle-capable payload or a combinator destination) while
|
|
* still referenced, the decrement that could strand a cycle through it
|
|
* sees them and buffers it then. */
|
|
}
|
|
|
|
void *scr_promise_retain_v(void *p) { return scr_promise_retain((ScrPromise *)p); }
|
|
void scr_promise_release_v(void *p) { scr_promise_release((ScrPromise *)p); }
|
|
void scr_promise_trace_v(void *p, ScrTraceVisit visit, void *ctx) {
|
|
scr_promise_trace(p, visit, ctx);
|
|
}
|
|
|
|
/* ── fibers and the scheduler ─────────────────────────────────────────── */
|
|
|
|
/* One saved execution context. x86_64/AArch64 POSIX: the callee-saved
|
|
* register file (scr_ctx_swap below). Other POSIX targets: a ucontext_t
|
|
* (swapcontext saves the outgoing context INTO the `from` slot). Windows:
|
|
* the fiber HANDLE — SwitchToFiber saves the outgoing state inside the
|
|
* fiber object itself, so the slot only needs to name the destination;
|
|
* `from` is unused. */
|
|
#if !defined(_WIN32) && !defined(__wasi__) && (defined(__x86_64__) || defined(__aarch64__))
|
|
#define SCR_FAST_CTX 1
|
|
#endif
|
|
|
|
#if defined(SCR_FAST_CTX) && defined(__x86_64__)
|
|
/* rsp, rbp, rbx, r12..r15, resume address; then MXCSR and the x87 control
|
|
* word (callee-saved control state in the SysV ABI). */
|
|
typedef struct {
|
|
void *gpr[8];
|
|
uint32_t mxcsr;
|
|
uint16_t fpcw;
|
|
} ScrCtx;
|
|
#elif defined(SCR_FAST_CTX)
|
|
/* x19..x28, x29 (fp), x30 (lr), sp, d8..d15. */
|
|
typedef struct {
|
|
uint64_t reg[13];
|
|
uint64_t fpr[8];
|
|
} ScrCtx;
|
|
#elif defined(_WIN32)
|
|
typedef void *ScrCtx;
|
|
#elif defined(__wasi__)
|
|
typedef unsigned char ScrCtx;
|
|
#else
|
|
typedef ucontext_t ScrCtx;
|
|
#endif
|
|
|
|
#ifdef SCR_FAST_CTX
|
|
/* User-space context switch: the fibers only need the ABI's callee-saved
|
|
* state preserved across a cooperative switch. swapcontext additionally
|
|
* saves and restores the process signal mask (one rt_sigprocmask syscall
|
|
* per switch on glibc; sigprocmask + sigaltstack + getrlimit on Darwin),
|
|
* which dominated await-heavy programs. The runtime never uses a context
|
|
* to change the signal mask, so the mask simply stays the process's.
|
|
*
|
|
* scr_ctx_swap(from, to) stores the callee-saved registers, the stack
|
|
* pointer, and the resume address into *from, then loads *to and resumes
|
|
* there. The register file lives in the ScrCtx itself — inside the heap
|
|
* ScrFiber for fibers, on the C stack for a resumer — exactly where
|
|
* ucontext_t kept it, so LeakSanitizer still sees pointers a suspended
|
|
* fiber holds in callee-saved registers. A fresh fiber's ScrCtx is seeded
|
|
* by scr_fiber_context_init so its first switch-in enters the trampoline.
|
|
* x18 (the Darwin platform register) is never touched. */
|
|
#if defined(__APPLE__)
|
|
#define SCR_CTX_FN(name) \
|
|
".globl _" name "\n.private_extern _" name "\n.p2align 4\n_" name ":\n"
|
|
#define SCR_CTX_END(name) ""
|
|
#elif defined(__x86_64__)
|
|
#define SCR_CTX_FN(name) \
|
|
".globl " name "\n.hidden " name "\n.type " name ", @function\n.p2align 4\n" name ":\n"
|
|
#define SCR_CTX_END(name) ".size " name ", .-" name "\n"
|
|
#else
|
|
#define SCR_CTX_FN(name) \
|
|
".globl " name "\n.hidden " name "\n.type " name ", %function\n.p2align 4\n" name ":\n"
|
|
#define SCR_CTX_END(name) ".size " name ", .-" name "\n"
|
|
#endif
|
|
|
|
#if defined(__x86_64__)
|
|
__asm__(".text\n"
|
|
SCR_CTX_FN("scr_ctx_swap")
|
|
#ifdef __CET__
|
|
" endbr64\n"
|
|
#endif
|
|
" movq (%rsp), %rax\n" /* resume address */
|
|
" leaq 8(%rsp), %rcx\n" /* rsp after our return */
|
|
" movq %rcx, 0(%rdi)\n"
|
|
" movq %rbp, 8(%rdi)\n"
|
|
" movq %rbx, 16(%rdi)\n"
|
|
" movq %r12, 24(%rdi)\n"
|
|
" movq %r13, 32(%rdi)\n"
|
|
" movq %r14, 40(%rdi)\n"
|
|
" movq %r15, 48(%rdi)\n"
|
|
" movq %rax, 56(%rdi)\n"
|
|
" stmxcsr 64(%rdi)\n"
|
|
" fnstcw 68(%rdi)\n"
|
|
" movq 0(%rsi), %rsp\n"
|
|
" movq 8(%rsi), %rbp\n"
|
|
" movq 16(%rsi), %rbx\n"
|
|
" movq 24(%rsi), %r12\n"
|
|
" movq 32(%rsi), %r13\n"
|
|
" movq 40(%rsi), %r14\n"
|
|
" movq 48(%rsi), %r15\n"
|
|
" ldmxcsr 64(%rsi)\n"
|
|
" fldcw 68(%rsi)\n"
|
|
" jmpq *56(%rsi)\n"
|
|
SCR_CTX_END("scr_ctx_swap"));
|
|
#else
|
|
__asm__(".text\n"
|
|
SCR_CTX_FN("scr_ctx_swap")
|
|
" stp x19, x20, [x0, #0]\n"
|
|
" stp x21, x22, [x0, #16]\n"
|
|
" stp x23, x24, [x0, #32]\n"
|
|
" stp x25, x26, [x0, #48]\n"
|
|
" stp x27, x28, [x0, #64]\n"
|
|
" stp x29, x30, [x0, #80]\n"
|
|
" mov x9, sp\n"
|
|
" str x9, [x0, #96]\n"
|
|
" stp d8, d9, [x0, #104]\n"
|
|
" stp d10, d11, [x0, #120]\n"
|
|
" stp d12, d13, [x0, #136]\n"
|
|
" stp d14, d15, [x0, #152]\n"
|
|
" ldp x19, x20, [x1, #0]\n"
|
|
" ldp x21, x22, [x1, #16]\n"
|
|
" ldp x23, x24, [x1, #32]\n"
|
|
" ldp x25, x26, [x1, #48]\n"
|
|
" ldp x27, x28, [x1, #64]\n"
|
|
" ldp x29, x30, [x1, #80]\n"
|
|
" ldr x9, [x1, #96]\n"
|
|
" mov sp, x9\n"
|
|
" ldp d8, d9, [x1, #104]\n"
|
|
" ldp d10, d11, [x1, #120]\n"
|
|
" ldp d12, d13, [x1, #136]\n"
|
|
" ldp d14, d15, [x1, #152]\n"
|
|
" ret\n"
|
|
SCR_CTX_END("scr_ctx_swap")
|
|
/* First switch-in of a fresh fiber: x19 holds the trampoline. Clear
|
|
* fp and lr so unwinders stop at the fiber's base, and branch
|
|
* through x16 (a valid BTI `bti c` landing source). */
|
|
SCR_CTX_FN("scr_ctx_boot")
|
|
" mov x16, x19\n"
|
|
" mov x29, xzr\n"
|
|
" mov x30, xzr\n"
|
|
" br x16\n"
|
|
SCR_CTX_END("scr_ctx_boot"));
|
|
extern void scr_ctx_boot(void);
|
|
#endif
|
|
#if defined(__x86_64__)
|
|
_Static_assert(offsetof(ScrCtx, mxcsr) == 64 && offsetof(ScrCtx, fpcw) == 68,
|
|
"scr_ctx_swap's x86_64 register-file offsets");
|
|
#else
|
|
_Static_assert(offsetof(ScrCtx, fpr) == 104, "scr_ctx_swap's AArch64 register-file offsets");
|
|
#endif
|
|
extern void scr_ctx_swap(ScrCtx *from, ScrCtx *to);
|
|
#endif
|
|
|
|
struct ScrFiber {
|
|
#ifdef SCR_WORKERS
|
|
struct ScrFiber *context_next;
|
|
struct ScrFiber **context_previous;
|
|
ScrPromise *waiting;
|
|
#endif
|
|
ScrCtx ctx;
|
|
/* wasm32 uses LLVM's stackless switched-coroutine frame instead of a
|
|
* native saved stack. The compiler emits these two generic wrappers in
|
|
* every WASI module. */
|
|
void *coro;
|
|
ScrCtx *return_to; /* whoever resumed us last (spawner or the loop) */
|
|
char *stack; /* POSIX only; Windows fibers own their stack (NULL) */
|
|
ScrPromise *promise; /* the promise this fiber settles (owned +1); NULL
|
|
* for generator fibers (gen non-NULL instead) */
|
|
ScrGen *gen; /* the generator this fiber runs (borrowed — the
|
|
* ScrGen owns the fiber, never the reverse) */
|
|
ScrExcCell exc;
|
|
/* AsyncLocalStorage context (owned; NULL = empty). Inherited from the
|
|
* SPAWNER at spawn (Node's init-time capture), swapped active with the
|
|
* fiber (the exc-cell pattern) so run()'s window rides awaits. */
|
|
ScrAlsCtx *als;
|
|
/* The lifetime region the body was spawned in (scr_region_fiber_hooks;
|
|
* NULL = the default region): the loop makes it current again whenever
|
|
* it resumes the fiber, stackful or stackless. Async bodies only;
|
|
* generators run in their resumer's region. */
|
|
void *region;
|
|
bool done;
|
|
/* A native synchronous generator compiled as an LLVM coroutine (see coro
|
|
* above): no stack or saved context; resumes call into the frame on the
|
|
* resumer's stack. */
|
|
bool stackless;
|
|
/* The fiber a stackless hop resumed this body from (scr_gen_coro_hop),
|
|
* parked here so the hop keeps fewer values live across the body. */
|
|
struct ScrFiber *resumer;
|
|
/* Fiberless async bodies (scr_async_inline_enter) currently running on
|
|
* this fiber's stack. Such a body was proven never to park, so a park
|
|
* or hop while this is nonzero is a compiler bug — trap instead of
|
|
* resuming the caller out of order. */
|
|
unsigned inline_depth;
|
|
/* Trampoline args: the spawn wrapper stores a pointer to a stack-local
|
|
* argpack; the trampoline copies it out before the spawner resumes. */
|
|
void *argpack;
|
|
void (*entry)(ScrFiber *self, void *argpack);
|
|
/* queueMicrotask entries: a stackless pseudo-fiber — the READY queue's
|
|
* FIFO IS the microtask queue, so a closure rides it as a fiber-shaped
|
|
* envelope and scr_resume_fiber runs it on the main stack (a throw is
|
|
* an UNCAUGHT exception, like Node's queueMicrotask, never a
|
|
* rejection). Owned; NULL on real fibers. */
|
|
ScrClosure *micro_cb;
|
|
/* Async-generator settlement jobs: stackless READY-queue envelopes.
|
|
* The retained generator is pumped on the main stack. */
|
|
ScrGen *gen_job;
|
|
/* Promise.all / Promise.race reaction jobs (cb_job set): stackless
|
|
* READY-queue envelopes carrying the settled entry (owned) and its
|
|
* callback waiter (which owns its destination and countdown state). A
|
|
* combinator batch job (cb.job_at == SCR_CB_BATCH) carries the batch's
|
|
* first deciding entry, or none. */
|
|
bool cb_job;
|
|
ScrPromise *cb_src;
|
|
struct ScrPromiseCbWaiter cb;
|
|
/* Set during the eager prefix of a fiber spawned by an await-call entry
|
|
* (scr_async_spawn_await_*): the spawner that awaits this fiber's promise
|
|
* as soon as the spawn returns. The first suspension parks it on the
|
|
* promise right away (scr_fiber_park_awaiters) instead of switching back
|
|
* just so it can park itself. */
|
|
struct ScrFiber *awaiter;
|
|
/* This fiber was parked by such a child; set before the switch away. */
|
|
bool await_parked;
|
|
/* A stackless body spawned for a direct await (scr_async_spawn_direct):
|
|
* its promise lives in the same block (ScrDirectFiber) and nothing else
|
|
* sees it. Once the awaiting coroutine suspends it is direct_awaiter, the
|
|
* finished body wakes it in place of the promise's settlement, and the
|
|
* awaiter destroys the record when it takes the result. */
|
|
bool direct;
|
|
struct ScrFiber *direct_awaiter;
|
|
/* While an async body runs its synchronous prefix (until its first await
|
|
* or its end), the AsyncLocalStorage slot of the execution that spawned
|
|
* it; NULL afterwards (scr_als_prefix_end). */
|
|
ScrAlsCtx **prefix_slot;
|
|
#ifdef SCR_ASAN_FIBERS
|
|
void *fake_stack;
|
|
/* Live stackless async fibers (scr_async_spawn_stackless) form a list
|
|
* rooted in a global, the stack root regions' counterpart: a suspended
|
|
* coroutine frame that never resumes stays reachable for the leak check
|
|
* as an abandoned fiber stack does. */
|
|
struct ScrFiber *stackless_next;
|
|
struct ScrFiber **stackless_previous;
|
|
#endif
|
|
};
|
|
|
|
/* ── AsyncLocalStorage (node:async_hooks) ─────────────────────────────
|
|
* Stores are process-lived ids; the CONTEXT is an immutable refcounted
|
|
* snapshot of (store id → dyn value) entries. One ACTIVE SLOT pointer
|
|
* (the exc-cell pattern): main owns a static slot, every fiber owns its
|
|
* own field, scr_switch repoints the active slot — so run()'s window
|
|
* rides the fiber across awaits, and a spawned fiber INHERITS the
|
|
* spawner's snapshot (Node's init-time capture through AsyncResource).
|
|
* Snapshots are immutable: enter builds a fresh one (entries retained),
|
|
* restore swaps the previous back — timer capture retains pointers. */
|
|
|
|
/* The ScrAlsCtx layout and the API over it live in scr_runtime.h /
|
|
* scr_async_dyn.c (gated — the size-class stance); this always-linked
|
|
* core keeps only what the fiber machinery and the test runner touch: the
|
|
* active slot, the snapshot RC pair, enter/restore, and the
|
|
* switch/spawn/destroy wiring. */
|
|
static SCR_TL ScrAlsCtx *scr_als_main_slot = NULL;
|
|
#ifdef SCR_WORKERS
|
|
SCR_TL ScrAlsCtx **scr_als_active;
|
|
ScrAlsCtx **scr_als_slot(void) {
|
|
return scr_als_active ? scr_als_active : &scr_als_main_slot;
|
|
}
|
|
#else
|
|
ScrAlsCtx **scr_als_active = &scr_als_main_slot;
|
|
#endif
|
|
|
|
ScrAlsCtx *scr_als_ctx_retain(ScrAlsCtx *c) {
|
|
if (c) c->rc++;
|
|
return c;
|
|
}
|
|
|
|
void scr_als_ctx_release(ScrAlsCtx *c) {
|
|
if (!c || --c->rc != 0) return;
|
|
for (size_t i = 0; i < c->len; i++) scr_dyn_release(c->entries[i].value);
|
|
free(c);
|
|
}
|
|
|
|
/* enterWith on the main stack lasts until that code returns to the loop,
|
|
* so the store does not reach callbacks scheduled without it: the loop
|
|
* clears the main slot when the main module's body returns, and an
|
|
* enterWith inside a callback the loop runs puts a tick that clears it at
|
|
* the front of the tick queue, so it runs as soon as that callback
|
|
* returns. A store that an async callee's prefix or a generator step
|
|
* enters becomes the caller's at scr_als_prefix_end and is cleared the
|
|
* same way. */
|
|
static SCR_TL bool scr_loop_started = false;
|
|
|
|
static void scr_als_main_clear(void) {
|
|
scr_als_ctx_release(scr_als_main_slot);
|
|
scr_als_main_slot = NULL;
|
|
}
|
|
|
|
static void scr_next_tick_raw_first(void (*fn)(void));
|
|
|
|
/* A new store on the main slot: one a loop callback enters ends with it. */
|
|
static void scr_als_main_entered(void) {
|
|
if (scr_loop_started) scr_next_tick_raw_first(scr_als_main_clear);
|
|
}
|
|
|
|
void scr_als_entered_with(void) {
|
|
if (SCR_ALS_SLOT() == &scr_als_main_slot) scr_als_main_entered();
|
|
}
|
|
|
|
/* The end of an async body's synchronous prefix: its first await, or its
|
|
* end. Node runs that prefix inside the caller's execution, so a store it
|
|
* entered (enterWith) is still the caller's store after the call; the body
|
|
* keeps its own copy of the context for what runs after its await. */
|
|
static void scr_als_prefix_end(ScrFiber *f) {
|
|
ScrAlsCtx **slot = f->prefix_slot;
|
|
if (slot == NULL) return;
|
|
f->prefix_slot = NULL;
|
|
if (*slot != f->als) {
|
|
scr_als_ctx_release(*slot);
|
|
*slot = scr_als_ctx_retain(f->als);
|
|
if (f->als != NULL && slot == &scr_als_main_slot) scr_als_main_entered();
|
|
}
|
|
}
|
|
|
|
/* A synchronous generator has no context of its own: each step runs in its
|
|
* resumer's execution, as Node runs it. A fiber-backed one switches to its
|
|
* own slot, so its step starts from the resumer's store and a store it
|
|
* enters is the resumer's after the step (scr_als_prefix_end on the way
|
|
* back); a stackless one runs in the resumer's slot (scr_gen_coro_hop). */
|
|
static void scr_als_step_begin(ScrFiber *f) {
|
|
ScrAlsCtx **slot = SCR_ALS_SLOT();
|
|
if (f->als != *slot) {
|
|
scr_als_ctx_release(f->als);
|
|
f->als = scr_als_ctx_retain(*slot);
|
|
}
|
|
f->prefix_slot = slot;
|
|
}
|
|
|
|
/* The lifetime region a new async fiber carries (ScrFiber.region). */
|
|
static inline void *scr_fiber_region_capture(void) {
|
|
const ScrRgnFiberHooks *regions = atomic_load_explicit(&scr_region_fiber_hooks, memory_order_relaxed);
|
|
return regions != NULL ? regions->capture() : NULL;
|
|
}
|
|
|
|
/* Fresh snapshot storage. */
|
|
ScrAlsCtx *scr_als_ctx_alloc(size_t len) {
|
|
ScrAlsCtx *c = malloc(sizeof *c + len * sizeof(ScrAlsEntry));
|
|
if (!c) scr_oom();
|
|
c->rc = 1;
|
|
c->len = len;
|
|
return c;
|
|
}
|
|
|
|
/* Fresh snapshot with (id → value) replaced-or-appended (value borrowed,
|
|
* retained in), installed as the active context; the PREVIOUS snapshot
|
|
* returns (ownership moves out) for scr_als_restore. The test runner
|
|
* enters its own entry through this too, so it lives in the core. */
|
|
ScrAlsCtx *scr_als_enter(double id, ScrDyn *value) {
|
|
ScrAlsCtx *prev = *SCR_ALS_SLOT();
|
|
size_t n = prev ? prev->len : 0;
|
|
bool have = false;
|
|
for (size_t i = 0; i < n; i++) {
|
|
if (prev->entries[i].id == id) { have = true; break; }
|
|
}
|
|
ScrAlsCtx *next = scr_als_ctx_alloc(have ? n : n + 1);
|
|
size_t w = 0;
|
|
for (size_t i = 0; i < n; i++) {
|
|
if (prev->entries[i].id == id) continue;
|
|
next->entries[w].id = prev->entries[i].id;
|
|
next->entries[w].value = scr_dyn_retain(prev->entries[i].value);
|
|
w++;
|
|
}
|
|
next->entries[w].id = id;
|
|
next->entries[w].value = scr_dyn_retain(value);
|
|
*SCR_ALS_SLOT() = next;
|
|
return prev; /* ownership moves to the caller */
|
|
}
|
|
|
|
void scr_als_restore(ScrAlsCtx *prev) {
|
|
scr_als_ctx_release(*SCR_ALS_SLOT());
|
|
*SCR_ALS_SLOT() = prev; /* ownership moves back in */
|
|
}
|
|
|
|
/* The value of `id` in the active snapshot (borrowed), or NULL. */
|
|
ScrDyn *scr_als_peek(double id) {
|
|
const ScrAlsCtx *c = *SCR_ALS_SLOT();
|
|
if (c) {
|
|
for (size_t i = 0; i < c->len; i++) {
|
|
if (c->entries[i].id == id) return c->entries[i].value;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static SCR_TL ScrCtx scr_loop_ctx; /* the main stack (scheduler home) */
|
|
static SCR_TL ScrFiber *scr_current = NULL;
|
|
/* A finished fiber whose last switch went to the loop (scr_resume_fiber). */
|
|
static SCR_TL ScrFiber *scr_loop_reap = NULL;
|
|
static SCR_TL long scr_fibers_live = 0;
|
|
static SCR_TL long scr_fibers_abandoned = 0;
|
|
|
|
#ifdef SCR_WORKERS
|
|
static SCR_TL ScrFiber *scr_context_fibers;
|
|
static void scr_fiber_track(ScrFiber *fiber) {
|
|
fiber->context_next = scr_context_fibers;
|
|
fiber->context_previous = &scr_context_fibers;
|
|
if (scr_context_fibers) scr_context_fibers->context_previous = &fiber->context_next;
|
|
scr_context_fibers = fiber;
|
|
}
|
|
#else
|
|
static void scr_fiber_track(ScrFiber *fiber) { (void)fiber; }
|
|
#endif
|
|
|
|
long scr_abandoned_fiber_count(void) { return scr_fibers_abandoned; }
|
|
|
|
#ifdef SCR_ASAN_FIBERS
|
|
/* Deliberately abandoned fibers stay referenced for the process lifetime,
|
|
* as their stacks do: the sanitizer lane's leak check then reports only
|
|
* memory nothing can reach, matching the RC audit's abandoned-fiber note. */
|
|
static SCR_TL ScrFiber **scr_abandoned_fibers;
|
|
static SCR_TL size_t scr_abandoned_len, scr_abandoned_cap;
|
|
static void scr_fiber_keep_abandoned(ScrFiber *fiber) {
|
|
if (scr_abandoned_len == scr_abandoned_cap) {
|
|
size_t cap = scr_abandoned_cap ? scr_abandoned_cap * 2 : 8;
|
|
ScrFiber **grown = realloc(scr_abandoned_fibers, cap * sizeof *grown);
|
|
if (!grown) scr_oom();
|
|
scr_abandoned_fibers = grown;
|
|
scr_abandoned_cap = cap;
|
|
}
|
|
scr_abandoned_fibers[scr_abandoned_len++] = fiber;
|
|
}
|
|
|
|
static SCR_TL ScrFiber *scr_stackless_fibers;
|
|
#endif
|
|
|
|
void *scr_fiber_self(void) { return scr_current; }
|
|
|
|
#ifndef __wasi__
|
|
/* LLVM's switched-resume coroutine frame begins with its resume and destroy
|
|
* functions (what llvm.coro.resume / llvm.coro.destroy call). */
|
|
typedef void (*ScrCoroFn)(void *);
|
|
static inline void scr_coro_resume(void *handle) { ((ScrCoroFn *)handle)[0](handle); }
|
|
static inline void scr_coro_destroy(void *handle) { ((ScrCoroFn *)handle)[1](handle); }
|
|
|
|
void scr_coro_started(void *handle) {
|
|
if (scr_current == NULL || !scr_current->stackless) {
|
|
fputs("scriptc: internal error: coroutine started outside a stackless generator\n", stderr);
|
|
abort();
|
|
}
|
|
scr_current->coro = handle;
|
|
}
|
|
#endif
|
|
|
|
#ifdef __wasi__
|
|
extern void scr_wasi_coro_resume(void *handle);
|
|
extern void scr_wasi_coro_destroy(void *handle);
|
|
|
|
void scr_wasi_coro_started(void *handle) {
|
|
if (scr_current == NULL) {
|
|
fputs("scriptc: internal error: coroutine started outside a fiber\n", stderr);
|
|
abort();
|
|
}
|
|
scr_current->coro = handle;
|
|
}
|
|
#endif
|
|
|
|
/* Microtask queue: ready fibers, FIFO. */
|
|
static SCR_TL ScrFiber **scr_ready = NULL;
|
|
static SCR_TL size_t scr_ready_head = 0, scr_ready_len = 0, scr_ready_cap = 0;
|
|
|
|
#ifdef SCR_NODE_ISLANDS
|
|
/* The hosted loop (scope-B NI-3, defined below the loop hooks): once Node
|
|
* is loaded, the runtime's queues live on Node's and this names the
|
|
* libnode bridge that schedules them. NULL until then, and in programs
|
|
* that never load Node. */
|
|
static SCR_TL const ScrNodeBridge *scr_loop_hosted = NULL;
|
|
/* Runtime ticks and microtasks queued on Node and not yet run: the
|
|
* dispatch stations' yield signal (scr_loop_has_ready). */
|
|
static SCR_TL size_t scr_hosted_queued = 0;
|
|
/* The end of the check phase the loop pass is running (0: none). */
|
|
static SCR_TL size_t scr_check_end = 0;
|
|
/* Where the loop pass is, for a switch in the middle of it: the turn
|
|
* continues at Node's poll phase after a timers or I/O phase, and a
|
|
* microtask drain in progress finishes before the ticks. */
|
|
enum { SCR_PASS_START, SCR_PASS_IO, SCR_PASS_TIMERS, SCR_PASS_CHECK };
|
|
static SCR_TL int scr_pass_phase = SCR_PASS_START;
|
|
static SCR_TL bool scr_in_microtask = false;
|
|
static SCR_TL bool scr_hosted_continue_turn = false;
|
|
static SCR_TL bool scr_hosted_microtasks_first = false;
|
|
#define SCR_PASS_PHASE(phase) (scr_pass_phase = (phase))
|
|
#define SCR_RESUME_JOB(f) (scr_in_microtask = true, scr_resume_fiber(f), scr_in_microtask = false)
|
|
static void scr_hosted_microtask(ScrFiber *f);
|
|
/* A microtask turn may continue in place only while the runtime's own
|
|
* queue is the whole queue: hosted, V8's queue may hold Node's jobs. */
|
|
#define SCR_READY_IDLE() (scr_ready_len == 0 && scr_loop_hosted == NULL)
|
|
#else
|
|
#define SCR_READY_IDLE() (scr_ready_len == 0)
|
|
#define SCR_PASS_PHASE(phase) ((void)0)
|
|
#define SCR_RESUME_JOB(f) scr_resume_fiber(f)
|
|
#endif
|
|
|
|
static void scr_ready_push(ScrFiber *f) {
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) {
|
|
scr_hosted_microtask(f);
|
|
return;
|
|
}
|
|
#endif
|
|
if (scr_ready_head + scr_ready_len == scr_ready_cap) {
|
|
if (scr_ready_len > 0) {
|
|
memmove(scr_ready, scr_ready + scr_ready_head, scr_ready_len * sizeof *scr_ready);
|
|
}
|
|
scr_ready_head = 0;
|
|
if (scr_ready_len == scr_ready_cap) {
|
|
scr_ready_cap = scr_ready_cap ? scr_ready_cap * 2 : 16;
|
|
scr_ready = realloc(scr_ready, scr_ready_cap * sizeof *scr_ready);
|
|
if (!scr_ready) scr_oom();
|
|
}
|
|
}
|
|
scr_ready[scr_ready_head + scr_ready_len++] = f;
|
|
}
|
|
|
|
static void scr_async_gen_job(ScrGen *g);
|
|
static void scr_async_gen_resume_ready(ScrGen *g);
|
|
|
|
/* Timer min-heap, FIFO tiebreak via sequence numbers. `id` is nonzero only
|
|
* for setInterval entries (the clearInterval handle; setTimeout has no
|
|
* clear surface, so plain timeouts never need one) and `repeat_ms` is the
|
|
* interval's period — a fired interval re-enters the heap with a fresh
|
|
* deadline and seq, so ties against later timers stay FIFO like Node's. */
|
|
typedef struct {
|
|
double deadline_ms;
|
|
unsigned long seq;
|
|
ScrClosure *cb; /* owned */
|
|
double repeat_ms;
|
|
unsigned long id;
|
|
bool reffed; /* keeps the loop alive; unref() clears it (Node semantics) */
|
|
double delay_ms; /* the original delay — refresh() re-arms to now + this */
|
|
} ScrTimer;
|
|
|
|
static SCR_TL ScrTimer *scr_timers = NULL;
|
|
static SCR_TL size_t scr_ntimers = 0, scr_timers_cap = 0;
|
|
static SCR_TL unsigned long scr_timer_seq = 0;
|
|
/* REF'd armed timers — the loop's timer-liveness count (an unref'd timer
|
|
* sits in the heap and still FIRES if the loop runs for other reasons, but
|
|
* does not by itself keep the loop alive). Kept in sync by push/pop/remove
|
|
* and by ref/unref. */
|
|
static SCR_TL size_t scr_reffed_timers = 0;
|
|
|
|
double scr_now_ms(void) {
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
|
|
}
|
|
|
|
static bool scr_timer_before(const ScrTimer *a, const ScrTimer *b) {
|
|
if (a->deadline_ms != b->deadline_ms) return a->deadline_ms < b->deadline_ms;
|
|
return a->seq < b->seq;
|
|
}
|
|
|
|
/* Heap slot of every handle-carrying timer (id != 0), so clear, ref,
|
|
* unref, refresh, and hasRef find their entry without scanning the heap.
|
|
* The scan made a server that arms and clears a timeout per request
|
|
* quadratic in the number of live timers. Open addressing with linear
|
|
* probing; deletion shifts the probe run back (no tombstones). Every heap
|
|
* write goes through scr_timer_set so the slots stay exact. */
|
|
typedef struct {
|
|
unsigned long id; /* 0 = empty */
|
|
size_t slot;
|
|
} ScrTimerSlot;
|
|
static SCR_TL ScrTimerSlot *scr_timer_slots = NULL;
|
|
static SCR_TL size_t scr_timer_slots_cap = 0, scr_timer_slots_len = 0;
|
|
|
|
static size_t scr_timer_slot_home(unsigned long id) {
|
|
uint64_t h = (uint64_t)id * UINT64_C(0x9E3779B97F4A7C15);
|
|
return (size_t)(h >> 32) & (scr_timer_slots_cap - 1);
|
|
}
|
|
|
|
static size_t scr_timer_slot_find(unsigned long id) {
|
|
if (scr_timer_slots_cap == 0) return SIZE_MAX;
|
|
size_t mask = scr_timer_slots_cap - 1;
|
|
for (size_t i = scr_timer_slot_home(id); scr_timer_slots[i].id != 0; i = (i + 1) & mask) {
|
|
if (scr_timer_slots[i].id == id) return i;
|
|
}
|
|
return SIZE_MAX;
|
|
}
|
|
|
|
static void scr_timer_slot_put(unsigned long id, size_t slot);
|
|
|
|
static void scr_timer_slots_grow(void) {
|
|
ScrTimerSlot *old = scr_timer_slots;
|
|
size_t old_cap = scr_timer_slots_cap;
|
|
scr_timer_slots_cap = old_cap ? old_cap * 2 : 64;
|
|
scr_timer_slots = calloc(scr_timer_slots_cap, sizeof *scr_timer_slots);
|
|
if (!scr_timer_slots) scr_oom();
|
|
scr_timer_slots_len = 0;
|
|
for (size_t i = 0; i < old_cap; i++) {
|
|
if (old[i].id != 0) scr_timer_slot_put(old[i].id, old[i].slot);
|
|
}
|
|
free(old);
|
|
}
|
|
|
|
static void scr_timer_slot_put(unsigned long id, size_t slot) {
|
|
if ((scr_timer_slots_len + 1) * 2 > scr_timer_slots_cap) scr_timer_slots_grow();
|
|
size_t mask = scr_timer_slots_cap - 1;
|
|
size_t i = scr_timer_slot_home(id);
|
|
while (scr_timer_slots[i].id != 0 && scr_timer_slots[i].id != id) i = (i + 1) & mask;
|
|
if (scr_timer_slots[i].id == 0) scr_timer_slots_len++;
|
|
scr_timer_slots[i].id = id;
|
|
scr_timer_slots[i].slot = slot;
|
|
}
|
|
|
|
static void scr_timer_slot_delete(unsigned long id) {
|
|
if (id == 0) return;
|
|
size_t i = scr_timer_slot_find(id);
|
|
if (i == SIZE_MAX) return;
|
|
size_t mask = scr_timer_slots_cap - 1;
|
|
for (size_t j = (i + 1) & mask; scr_timer_slots[j].id != 0; j = (j + 1) & mask) {
|
|
size_t home = scr_timer_slot_home(scr_timer_slots[j].id);
|
|
/* Entry j stays when its home lies cyclically in (i, j]. */
|
|
bool stays = i <= j ? (i < home && home <= j) : (i < home || home <= j);
|
|
if (stays) continue;
|
|
scr_timer_slots[i] = scr_timer_slots[j];
|
|
i = j;
|
|
}
|
|
scr_timer_slots[i].id = 0;
|
|
scr_timer_slots_len--;
|
|
}
|
|
|
|
static void scr_timer_set(size_t i, ScrTimer t) {
|
|
scr_timers[i] = t;
|
|
if (t.id != 0) scr_timer_slot_put(t.id, i);
|
|
}
|
|
|
|
static void scr_timer_swap(size_t i, size_t j) {
|
|
ScrTimer tmp = scr_timers[i];
|
|
scr_timer_set(i, scr_timers[j]);
|
|
scr_timer_set(j, tmp);
|
|
}
|
|
|
|
#ifdef SCR_NODE_ISLANDS
|
|
static void scr_hosted_timer_push(ScrTimer t);
|
|
/* The hosted timer, immediate and tick operations (defined with the
|
|
* hosted loop below). Each answers false when the handle names nothing. */
|
|
enum { SCR_HOSTED_CLEAR, SCR_HOSTED_REF, SCR_HOSTED_UNREF, SCR_HOSTED_REFRESH, SCR_HOSTED_HAS_REF };
|
|
static bool scr_hosted_timer_op(unsigned long id, int op);
|
|
static double scr_hosted_set_immediate(ScrClosure *cb);
|
|
static bool scr_hosted_immediate_op(unsigned long id, int op);
|
|
static size_t scr_hosted_count(int kind);
|
|
static void scr_hosted_teardown(void);
|
|
#endif
|
|
|
|
static void scr_timer_push(ScrTimer t) {
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) {
|
|
scr_hosted_timer_push(t);
|
|
return;
|
|
}
|
|
#endif
|
|
if (scr_ntimers == scr_timers_cap) {
|
|
scr_timers_cap = scr_timers_cap ? scr_timers_cap * 2 : 16;
|
|
scr_timers = realloc(scr_timers, scr_timers_cap * sizeof *scr_timers);
|
|
if (!scr_timers) scr_oom();
|
|
}
|
|
if (t.reffed) scr_reffed_timers++;
|
|
size_t i = scr_ntimers++;
|
|
scr_timer_set(i, t);
|
|
while (i > 0) {
|
|
size_t parent = (i - 1) / 2;
|
|
if (!scr_timer_before(&scr_timers[i], &scr_timers[parent])) break;
|
|
scr_timer_swap(i, parent);
|
|
i = parent;
|
|
}
|
|
}
|
|
|
|
static ScrTimer scr_timer_pop(void) {
|
|
ScrTimer top = scr_timers[0];
|
|
if (top.reffed && scr_reffed_timers > 0) scr_reffed_timers--;
|
|
scr_timer_slot_delete(top.id);
|
|
if (--scr_ntimers == 0) return top;
|
|
scr_timer_set(0, scr_timers[scr_ntimers]);
|
|
size_t i = 0;
|
|
for (;;) {
|
|
size_t l = 2 * i + 1, r = 2 * i + 2, min = i;
|
|
if (l < scr_ntimers && scr_timer_before(&scr_timers[l], &scr_timers[min])) min = l;
|
|
if (r < scr_ntimers && scr_timer_before(&scr_timers[r], &scr_timers[min])) min = r;
|
|
if (min == i) break;
|
|
scr_timer_swap(i, min);
|
|
i = min;
|
|
}
|
|
return top;
|
|
}
|
|
|
|
/* Node's delay coercion (lib/internal/timers.js): NaN/negative/sub-ms
|
|
* clamp to 1, the delay TRUNCATES to integer milliseconds (setTimeout(cb,
|
|
* 1.8) and setTimeout(cb, 1.1) both land in the 1ms bucket, so a batch of
|
|
* fractional delays fires in REGISTRATION order — test-timers-non-integer-
|
|
* delay's contract), and anything past TIMEOUT_MAX (2^31-1) becomes 1. */
|
|
static double scr_timer_coerce_ms(double ms) {
|
|
if (!(ms >= 1)) return 1;
|
|
if (ms > 2147483647.0) return 1; /* past TIMEOUT_MAX (also +Infinity) */
|
|
return (double)(unsigned long long)ms; /* trunc: positive, finite, in range */
|
|
}
|
|
|
|
void scr_set_timeout(ScrClosure *cb, double ms) {
|
|
ms = scr_timer_coerce_ms(ms);
|
|
ScrTimer t = {scr_now_ms() + ms, scr_timer_seq++, cb /* ownership moves in */, 0, 0, true, ms};
|
|
scr_timer_push(t);
|
|
}
|
|
|
|
/* ── setInterval / clearInterval ─────────────────────────────────────
|
|
* Intervals ride the same heap: the entry carries its period and a
|
|
* nonzero id (the compiled handle — Node's Timeout object reduced to the
|
|
* number the fallback declarations promise). A live interval keeps the
|
|
* loop alive exactly because it sits in the heap; clearInterval removes
|
|
* the entry EAGERLY (a lazily-cancelled entry would hold the loop open
|
|
* until its deadline — an interval cleared at t=0 with a one-hour period
|
|
* must let the program exit NOW, like Node). Clearing the interval whose
|
|
* callback is currently running (the self-clearing spinner pattern) is
|
|
* handled by the firing pair below: the entry is out of the heap while
|
|
* its callback runs, so the run loop re-pushes it only if the callback
|
|
* did not clear it. */
|
|
|
|
static SCR_TL unsigned long scr_interval_next_id = 1;
|
|
static SCR_TL unsigned long scr_firing_id = 0; /* interval currently running */
|
|
static SCR_TL bool scr_firing_cleared = false;
|
|
static SCR_TL bool scr_firing_reffed = true; /* the running interval's ref state */
|
|
static SCR_TL bool scr_firing_refresh = false; /* refresh() called mid-callback */
|
|
|
|
/* Removes heap entry i (swap-with-last, then sift whichever way the moved
|
|
* entry needs). */
|
|
static void scr_timer_remove_at(size_t i) {
|
|
if (scr_timers[i].reffed && scr_reffed_timers > 0) scr_reffed_timers--;
|
|
scr_timer_slot_delete(scr_timers[i].id);
|
|
if (i >= --scr_ntimers) return;
|
|
scr_timer_set(i, scr_timers[scr_ntimers]);
|
|
/* Sift up if the moved entry beats its parent, else sift down. */
|
|
while (i > 0) {
|
|
size_t parent = (i - 1) / 2;
|
|
if (!scr_timer_before(&scr_timers[i], &scr_timers[parent])) break;
|
|
scr_timer_swap(i, parent);
|
|
i = parent;
|
|
}
|
|
for (;;) {
|
|
size_t l = 2 * i + 1, r = 2 * i + 2, min = i;
|
|
if (l < scr_ntimers && scr_timer_before(&scr_timers[l], &scr_timers[min])) min = l;
|
|
if (r < scr_ntimers && scr_timer_before(&scr_timers[r], &scr_timers[min])) min = r;
|
|
if (min == i) break;
|
|
scr_timer_swap(i, min);
|
|
i = min;
|
|
}
|
|
}
|
|
|
|
double scr_set_interval(ScrClosure *cb, double ms) {
|
|
ms = scr_timer_coerce_ms(ms); /* Node's clamp-and-trunc, like setTimeout */
|
|
unsigned long id = scr_interval_next_id++;
|
|
ScrTimer t = {scr_now_ms() + ms, scr_timer_seq++, cb /* ownership moves in */, ms, id, true, ms};
|
|
scr_timer_push(t);
|
|
return (double)id;
|
|
}
|
|
|
|
SCR_TL void (*scr_mt_abort_timeout_arm)(ScrClosure *cb, double ms) = NULL;
|
|
SCR_TL int scr_mt_wait_state = 0;
|
|
|
|
double scr_set_timeout_handle(ScrClosure *cb, double ms) {
|
|
ms = scr_timer_coerce_ms(ms);
|
|
unsigned long id = scr_interval_next_id++;
|
|
ScrTimer t = {scr_now_ms() + ms, scr_timer_seq++, cb /* ownership moves in */, 0, id, true, ms};
|
|
scr_timer_push(t);
|
|
return (double)id;
|
|
}
|
|
|
|
/* process.env-style timer ref bookkeeping: unref() drops a timer from the
|
|
* loop's liveness count (it still fires if the loop runs on), ref() puts
|
|
* it back. Node-exact for the static timer surface; NaN/0/absent handles
|
|
* are tolerated (never a handle). The one-shot setTimeout with NO clear
|
|
* handle (id 0) cannot be unref'd by id — the compiler routes .unref()
|
|
* only over handle-returning timers, so id 0 never reaches here. */
|
|
static ScrTimer *scr_timer_find(unsigned long id) {
|
|
size_t i = scr_timer_slot_find(id);
|
|
return i == SIZE_MAX ? NULL : &scr_timers[scr_timer_slots[i].slot];
|
|
}
|
|
|
|
void scr_timer_unref(double handle) {
|
|
if (!(handle >= 1)) return;
|
|
unsigned long id = (unsigned long)handle;
|
|
if (id == scr_firing_id) {
|
|
/* Firing interval unref'ing itself: the re-arm carries the flag. */
|
|
scr_firing_reffed = false;
|
|
return;
|
|
}
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) {
|
|
scr_hosted_timer_op(id, SCR_HOSTED_UNREF);
|
|
return;
|
|
}
|
|
#endif
|
|
ScrTimer *t = scr_timer_find(id);
|
|
if (t && t->reffed) {
|
|
t->reffed = false;
|
|
if (scr_reffed_timers > 0) scr_reffed_timers--;
|
|
}
|
|
}
|
|
|
|
void scr_timer_ref(double handle) {
|
|
if (!(handle >= 1)) return;
|
|
unsigned long id = (unsigned long)handle;
|
|
if (id == scr_firing_id) {
|
|
scr_firing_reffed = true;
|
|
return;
|
|
}
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) {
|
|
scr_hosted_timer_op(id, SCR_HOSTED_REF);
|
|
return;
|
|
}
|
|
#endif
|
|
ScrTimer *t = scr_timer_find(id);
|
|
if (t && !t->reffed) {
|
|
t->reffed = true;
|
|
scr_reffed_timers++;
|
|
}
|
|
}
|
|
|
|
/* refresh(): re-arm to now + the original delay — Node's Timeout.refresh.
|
|
* A HEAP entry (armed timeout or interval between ticks) re-enters the
|
|
* heap with a fresh deadline and seq (FIFO against later same-deadline
|
|
* timers, like a brand-new timer); the timer whose callback is RUNNING
|
|
* sets the firing flag and the loop re-arms the one-shot after the
|
|
* callback returns (the interval re-arm already does). A one-shot that
|
|
* fired on an earlier turn is gone — its closure released — so refresh
|
|
* of a dead handle is a tolerated no-op (SEMANTICS: Node re-activates
|
|
* even a fired Timeout; the compiled handle cannot). */
|
|
void scr_timer_refresh(double handle) {
|
|
if (!(handle >= 1)) return;
|
|
unsigned long id = (unsigned long)handle;
|
|
if (id == scr_firing_id) {
|
|
scr_firing_refresh = true;
|
|
return;
|
|
}
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) {
|
|
scr_hosted_timer_op(id, SCR_HOSTED_REFRESH);
|
|
return;
|
|
}
|
|
#endif
|
|
ScrTimer *found = scr_timer_find(id);
|
|
if (found) {
|
|
ScrTimer t = *found;
|
|
scr_timer_remove_at((size_t)(found - scr_timers));
|
|
t.deadline_ms = scr_now_ms() + t.delay_ms;
|
|
t.seq = scr_timer_seq++;
|
|
scr_timer_push(t);
|
|
}
|
|
}
|
|
|
|
/* hasRef(): true iff the handle names a live, still-reffed timer. */
|
|
bool scr_timer_has_ref(double handle) {
|
|
if (!(handle >= 1)) return false;
|
|
unsigned long id = (unsigned long)handle;
|
|
if (id == scr_firing_id) return scr_firing_reffed;
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) return scr_hosted_timer_op(id, SCR_HOSTED_HAS_REF);
|
|
#endif
|
|
ScrTimer *t = scr_timer_find(id);
|
|
return t != NULL && t->reffed;
|
|
}
|
|
|
|
/* ── process.getActiveResourcesInfo (the loop's own bookkeeping) ──────
|
|
* 'Timeout' per armed heap timer — plus the firing, uncleared one (Node
|
|
* counts a Timeout as active while its callback runs, until clearTimeout
|
|
* drops it) — and 'Immediate' per queued, unfired immediate (a FIRED
|
|
* immediate no longer counts, Node's current answer inside the
|
|
* callback). Resource kinds this runtime does not model as loop handles
|
|
* (TCP wraps, FS requests, ...) are absent — SEMANTICS.md names the
|
|
* divergence. Result +1. */
|
|
static SCR_TL size_t scr_pending_immediates; /* defined below with the queue */
|
|
ScrArr *scr_active_resources(void) {
|
|
ScrArr *arr = scr_arr_new(SCR_ELEM_STR, 8);
|
|
size_t timeouts = scr_ntimers + ((scr_firing_id != 0 && !scr_firing_cleared) ? 1 : 0);
|
|
#ifdef SCR_NODE_ISLANDS
|
|
/* Hosted: the runtime's own (Node's are Node's to report). */
|
|
size_t immediates = scr_pending_immediates;
|
|
if (scr_loop_hosted != NULL) {
|
|
timeouts = scr_hosted_count(0);
|
|
immediates = scr_hosted_count(1);
|
|
}
|
|
for (size_t i = 0; i < timeouts; i++) scr_arr_push_ref(arr, scr_str_new("Timeout", 7));
|
|
for (size_t i = 0; i < immediates; i++) scr_arr_push_ref(arr, scr_str_new("Immediate", 9));
|
|
#else
|
|
for (size_t i = 0; i < timeouts; i++) scr_arr_push_ref(arr, scr_str_new("Timeout", 7));
|
|
for (size_t i = 0; i < scr_pending_immediates; i++) scr_arr_push_ref(arr, scr_str_new("Immediate", 9));
|
|
#endif
|
|
return arr;
|
|
}
|
|
|
|
/* ── process.nextTick (the user tick queue) ───────────────────────────
|
|
* A FIFO of user callbacks drained BEFORE promise jobs at every loop
|
|
* checkpoint, to joint exhaustion with them — Node's tick-then-microtask
|
|
* order. Pending ticks are always-ready work: the loop neither sleeps
|
|
* nor exits while any exist. Ticks scheduled from 'exit' listeners (or
|
|
* left queued on the uncaught paths) never run — the teardown releases
|
|
* them, like Node dropping the queue at exit. */
|
|
typedef struct ScrNtick {
|
|
ScrClosure *cb; /* owned; NULL for a raw C-hook entry */
|
|
ScrFsRenameFn error_cb; /* process-write success adapter, or NULL */
|
|
void (*raw)(void); /* hook when cb == NULL (stream markers) */
|
|
struct ScrNtick *next;
|
|
} ScrNtick;
|
|
|
|
static SCR_TL ScrNtick *scr_nt_head = NULL;
|
|
static SCR_TL ScrNtick *scr_nt_tail = NULL;
|
|
|
|
/* Public: releases every queued tick without running it — the loop-exit
|
|
* teardown below AND the exit-listener runner (scr_events.c) both call
|
|
* it, because 'exit' listeners run AFTER the loop's teardown and may
|
|
* enqueue ticks that must never run (Node) yet must not leak. */
|
|
|
|
#ifdef SCR_NODE_ISLANDS
|
|
static void scr_hosted_tick(ScrNtick *t);
|
|
#endif
|
|
|
|
void scr_next_tick(ScrClosure *cb /*moves*/) {
|
|
ScrNtick *t = calloc(1, sizeof *t);
|
|
if (!t) scr_oom();
|
|
t->cb = cb;
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) {
|
|
scr_hosted_tick(t);
|
|
return;
|
|
}
|
|
#endif
|
|
if (scr_nt_tail) scr_nt_tail->next = t;
|
|
else scr_nt_head = t;
|
|
scr_nt_tail = t;
|
|
}
|
|
|
|
/* stdout/stderr write completions are Node nextTicks too, but their
|
|
* callback receives the success `null` argument. Reuse the error-first
|
|
* adapter ABI also used by fs.rename: emitted adapters construct the
|
|
* program's Error | null union, and NULL selects its null arm. */
|
|
void scr_process_write_callback(ScrClosure *cb /*moves*/, ScrFsRenameFn fn) {
|
|
ScrNtick *t = calloc(1, sizeof *t);
|
|
if (!t) scr_oom();
|
|
t->cb = cb;
|
|
t->error_cb = fn;
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) {
|
|
scr_hosted_tick(t);
|
|
return;
|
|
}
|
|
#endif
|
|
if (scr_nt_tail) scr_nt_tail->next = t;
|
|
else scr_nt_head = t;
|
|
scr_nt_tail = t;
|
|
}
|
|
|
|
/* A RAW C-hook tick: the stream unit enqueues one marker per deferred
|
|
* stream emission so those emissions interleave with user nextTicks in
|
|
* true FIFO order — in Node they ARE nextTicks (resume_, emitReadable_,
|
|
* endReadableNT, ...). The hook dispatches exactly one stream tick;
|
|
* teardown just drops markers (the stream queue owns its entries). */
|
|
void scr_next_tick_raw(void (*fn)(void)) {
|
|
ScrNtick *t = calloc(1, sizeof *t);
|
|
if (!t) scr_oom();
|
|
t->raw = fn;
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) {
|
|
scr_hosted_tick(t);
|
|
return;
|
|
}
|
|
#endif
|
|
if (scr_nt_tail) scr_nt_tail->next = t;
|
|
else scr_nt_head = t;
|
|
scr_nt_tail = t;
|
|
}
|
|
|
|
/* A raw tick at the front of the queue: it runs before the ticks already
|
|
* queued. */
|
|
static void scr_next_tick_raw_first(void (*fn)(void)) {
|
|
ScrNtick *t = calloc(1, sizeof *t);
|
|
if (!t) scr_oom();
|
|
t->raw = fn;
|
|
/* Hosted, Node's tick queue has no front: the tick queues last. */
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) {
|
|
scr_hosted_tick(t);
|
|
return;
|
|
}
|
|
#endif
|
|
t->next = scr_nt_head;
|
|
scr_nt_head = t;
|
|
if (scr_nt_tail == NULL) scr_nt_tail = t;
|
|
}
|
|
|
|
void scr_nticks_teardown(void) {
|
|
while (scr_nt_head != NULL) {
|
|
ScrNtick *t = scr_nt_head;
|
|
scr_nt_head = t->next;
|
|
if (t->cb) scr_closure_release(t->cb);
|
|
free(t);
|
|
}
|
|
scr_nt_tail = NULL;
|
|
}
|
|
|
|
static void scr_immediates_teardown(void);
|
|
void scr_timers_teardown(void) {
|
|
#ifdef SCR_NODE_ISLANDS
|
|
scr_hosted_teardown();
|
|
#endif
|
|
for (size_t i = 0; i < scr_ntimers; i++) scr_closure_release(scr_timers[i].cb);
|
|
scr_ntimers = 0;
|
|
scr_reffed_timers = 0;
|
|
free(scr_timer_slots);
|
|
scr_timer_slots = NULL;
|
|
scr_timer_slots_cap = scr_timer_slots_len = 0;
|
|
scr_immediates_teardown();
|
|
scr_nticks_teardown();
|
|
}
|
|
|
|
void scr_clear_interval(double handle) {
|
|
if (!(handle >= 1)) return; /* NaN/0/negative: never a handle; Node tolerates */
|
|
unsigned long id = (unsigned long)handle;
|
|
if (id == scr_firing_id) {
|
|
scr_firing_cleared = true; /* the run loop drops the callback */
|
|
return;
|
|
}
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) {
|
|
scr_hosted_timer_op(id, SCR_HOSTED_CLEAR);
|
|
return;
|
|
}
|
|
#endif
|
|
ScrTimer *found = scr_timer_find(id);
|
|
if (found) {
|
|
scr_closure_release(found->cb);
|
|
scr_timer_remove_at((size_t)(found - scr_timers));
|
|
}
|
|
}
|
|
|
|
/* ── immediates (Node's check phase) ──────────────────────────────────
|
|
* setImmediate callbacks run once per loop turn, AFTER due timers, in
|
|
* FIFO order — and immediates queued while the phase runs wait for the
|
|
* NEXT turn (the phase snapshots its end index). The queue is an
|
|
* append-only array with a head cursor; clearImmediate NULLs the slot in
|
|
* place (order and indices stay stable mid-phase). Ids ride their own
|
|
* space — clearTimeout of an Immediate is a no-op, like Node. */
|
|
typedef struct {
|
|
unsigned long id;
|
|
ScrClosure *cb; /* owned; NULL once fired or cleared */
|
|
bool reffed; /* keeps the loop alive; unref() clears it (Node semantics) */
|
|
} ScrImmediate;
|
|
|
|
static SCR_TL ScrImmediate *scr_immediates = NULL;
|
|
static SCR_TL size_t scr_nimmediates = 0, scr_immediates_head = 0, scr_immediates_cap = 0;
|
|
static SCR_TL unsigned long scr_immediate_seq = 0;
|
|
/* Pending (queued, uncleared) immediates and the reffed subset — the
|
|
* loop's no-sleep signal and liveness count respectively. */
|
|
static SCR_TL size_t scr_pending_immediates = 0;
|
|
static SCR_TL size_t scr_reffed_immediates = 0;
|
|
|
|
double scr_set_immediate(ScrClosure *cb) {
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) return scr_hosted_set_immediate(cb);
|
|
#endif
|
|
if (scr_nimmediates == scr_immediates_cap) {
|
|
scr_immediates_cap = scr_immediates_cap ? scr_immediates_cap * 2 : 16;
|
|
scr_immediates = realloc(scr_immediates, scr_immediates_cap * sizeof *scr_immediates);
|
|
if (!scr_immediates) scr_oom();
|
|
}
|
|
unsigned long id = ++scr_immediate_seq;
|
|
scr_immediates[scr_nimmediates].id = id;
|
|
scr_immediates[scr_nimmediates].cb = cb; /* ownership moves in */
|
|
scr_immediates[scr_nimmediates].reffed = true;
|
|
scr_nimmediates++;
|
|
scr_pending_immediates++;
|
|
scr_reffed_immediates++;
|
|
return (double)id;
|
|
}
|
|
|
|
static ScrImmediate *scr_immediate_find(unsigned long id) {
|
|
for (size_t i = scr_immediates_head; i < scr_nimmediates; i++) {
|
|
if (scr_immediates[i].id == id && scr_immediates[i].cb != NULL) return &scr_immediates[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
void scr_clear_immediate(double handle) {
|
|
if (!(handle >= 1)) return; /* NaN/0/negative: never a handle; Node tolerates */
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) {
|
|
scr_hosted_immediate_op((unsigned long)handle, SCR_HOSTED_CLEAR);
|
|
return;
|
|
}
|
|
#endif
|
|
ScrImmediate *im = scr_immediate_find((unsigned long)handle);
|
|
if (im == NULL) return; /* already fired/cleared: no-op, like Node */
|
|
scr_closure_release(im->cb);
|
|
im->cb = NULL;
|
|
if (scr_pending_immediates > 0) scr_pending_immediates--;
|
|
if (im->reffed) {
|
|
im->reffed = false;
|
|
if (scr_reffed_immediates > 0) scr_reffed_immediates--;
|
|
}
|
|
}
|
|
|
|
/* Immediate.unref()/ref()/hasRef(): the Timeout trio's exact story over
|
|
* the immediate queue. A fired or cleared handle is a tolerated no-op
|
|
* (Node's Immediate methods on a dead handle do nothing). */
|
|
void scr_immediate_unref(double handle) {
|
|
if (!(handle >= 1)) return;
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) {
|
|
scr_hosted_immediate_op((unsigned long)handle, SCR_HOSTED_UNREF);
|
|
return;
|
|
}
|
|
#endif
|
|
ScrImmediate *im = scr_immediate_find((unsigned long)handle);
|
|
if (im && im->reffed) {
|
|
im->reffed = false;
|
|
if (scr_reffed_immediates > 0) scr_reffed_immediates--;
|
|
}
|
|
}
|
|
|
|
void scr_immediate_ref(double handle) {
|
|
if (!(handle >= 1)) return;
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) {
|
|
scr_hosted_immediate_op((unsigned long)handle, SCR_HOSTED_REF);
|
|
return;
|
|
}
|
|
#endif
|
|
ScrImmediate *im = scr_immediate_find((unsigned long)handle);
|
|
if (im && !im->reffed) {
|
|
im->reffed = true;
|
|
scr_reffed_immediates++;
|
|
}
|
|
}
|
|
|
|
bool scr_immediate_has_ref(double handle) {
|
|
if (!(handle >= 1)) return false;
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) return scr_hosted_immediate_op((unsigned long)handle, SCR_HOSTED_HAS_REF);
|
|
#endif
|
|
ScrImmediate *im = scr_immediate_find((unsigned long)handle);
|
|
return im != NULL && im->reffed;
|
|
}
|
|
|
|
/* The teardown sweep's immediate arm (called from scr_timers_teardown):
|
|
* unref'd immediates left queued at normal loop exit never fire (Node),
|
|
* so their closures release here or the RC audit counts them as leaks. */
|
|
static void scr_immediates_teardown(void) {
|
|
for (size_t i = scr_immediates_head; i < scr_nimmediates; i++) {
|
|
if (scr_immediates[i].cb != NULL) scr_closure_release(scr_immediates[i].cb);
|
|
}
|
|
scr_nimmediates = 0;
|
|
scr_immediates_head = 0;
|
|
scr_pending_immediates = 0;
|
|
scr_reffed_immediates = 0;
|
|
}
|
|
|
|
/* Unhandled rejections: rejected promises retained here until observed. */
|
|
static SCR_TL ScrPromise **scr_maybe_unhandled = NULL;
|
|
static SCR_TL size_t scr_nunhandled = 0, scr_unhandled_cap = 0;
|
|
|
|
static void scr_track_rejection(ScrPromise *p) {
|
|
if (scr_nunhandled == scr_unhandled_cap) {
|
|
scr_unhandled_cap = scr_unhandled_cap ? scr_unhandled_cap * 2 : 8;
|
|
scr_maybe_unhandled = realloc(scr_maybe_unhandled, scr_unhandled_cap * sizeof *scr_maybe_unhandled);
|
|
if (!scr_maybe_unhandled) scr_oom();
|
|
}
|
|
scr_maybe_unhandled[scr_nunhandled++] = scr_promise_retain(p);
|
|
}
|
|
|
|
/* ── context switching (ASan-annotated) ───────────────────────────────── */
|
|
|
|
#ifdef SCR_ASAN_FIBERS
|
|
static SCR_TL void *scr_main_fake_stack; /* fake-stack slot for the main context */
|
|
#endif
|
|
|
|
#ifdef _WIN32
|
|
/* The current context's handle, converting the main thread to a fiber on
|
|
* first need (SwitchToFiber requires the caller BE a fiber; the runtime is
|
|
* single-threaded, so one lazy conversion covers the process). Also the
|
|
* loop-home keeper: whenever main is the caller this IS scr_loop_ctx's
|
|
* value — assigned at the conversion so parked fibers can always switch
|
|
* back to it. */
|
|
static void *scr_win_self(void) {
|
|
static bool converted = false;
|
|
if (!converted) {
|
|
scr_loop_ctx = ConvertThreadToFiber(NULL);
|
|
if (scr_loop_ctx == NULL) {
|
|
fputs("scriptc: ConvertThreadToFiber failed\n", stderr);
|
|
abort();
|
|
}
|
|
converted = true;
|
|
}
|
|
return GetCurrentFiber();
|
|
}
|
|
#endif
|
|
|
|
#ifdef SCR_NODE_ISLANDS
|
|
/* The lowest frame of Node's stack that is still live while a fiber runs:
|
|
* where runtime code on Node's stack (a job or host callback, however deep
|
|
* it went) last switched to a fiber. A call into Node from that fiber runs
|
|
* on Node's stack below it (scr_node_island.c), never over those frames.
|
|
* 0: none. scr_node_stack_has (set when Node starts) tells Node's stack. */
|
|
uintptr_t scr_node_stack_low = 0;
|
|
bool (*scr_node_stack_has)(uintptr_t sp) = NULL;
|
|
#endif
|
|
|
|
static void scr_switch(ScrCtx *from, ScrCtx *to, ScrFiber *to_fiber) {
|
|
#ifdef SCR_ASAN_FIBERS
|
|
ScrFiber *from_fiber = scr_current;
|
|
#endif
|
|
#ifdef SCR_NODE_ISLANDS
|
|
uintptr_t node_low = scr_node_stack_low;
|
|
if (to_fiber != NULL && scr_node_stack_has != NULL) {
|
|
uintptr_t here = (uintptr_t)__builtin_frame_address(0);
|
|
if (scr_node_stack_has(here)) scr_node_stack_low = here;
|
|
}
|
|
#endif
|
|
scr_current = to_fiber;
|
|
scr_exc_swap_cell(to_fiber ? &to_fiber->exc : NULL);
|
|
scr_als_active = to_fiber ? &to_fiber->als : &scr_als_main_slot;
|
|
#if !defined(_WIN32) && !defined(__wasi__)
|
|
/* The stack guard follows the running stack. A fresh fiber starts with
|
|
* its own; every resumed context restores the guard it saved here. */
|
|
uintptr_t guard = scr_stack_guard;
|
|
if (to_fiber) SCR_STACK_GUARD_SET(scr_stack_guard_for((uintptr_t)to_fiber->stack, SCR_FIBER_STACK));
|
|
#endif
|
|
#ifdef _WIN32
|
|
/* SwitchToFiber snapshots the outgoing fiber's state inside its own
|
|
* fiber object — `from` has nothing to record. */
|
|
(void)from;
|
|
SwitchToFiber(*to);
|
|
#elif defined(__wasi__)
|
|
(void)from;
|
|
(void)to;
|
|
scr_trap("scriptc: internal error: native stack switch reached on WASI\n");
|
|
#elif defined(SCR_ASAN_FIBERS)
|
|
void **save = from_fiber ? &from_fiber->fake_stack : &scr_main_fake_stack;
|
|
const void *bottom = to_fiber ? to_fiber->stack : NULL;
|
|
size_t size = to_fiber ? SCR_FIBER_STACK : 0;
|
|
__sanitizer_start_switch_fiber(save, bottom, size);
|
|
#ifdef SCR_FAST_CTX
|
|
scr_ctx_swap(from, to);
|
|
#else
|
|
swapcontext(from, to);
|
|
#endif
|
|
const void *old_bottom;
|
|
size_t old_size;
|
|
__sanitizer_finish_switch_fiber(
|
|
scr_current ? scr_current->fake_stack : scr_main_fake_stack, &old_bottom, &old_size);
|
|
#elif defined(SCR_FAST_CTX)
|
|
scr_ctx_swap(from, to);
|
|
#else
|
|
swapcontext(from, to);
|
|
#endif
|
|
#if !defined(_WIN32) && !defined(__wasi__)
|
|
SCR_STACK_GUARD_SET(guard);
|
|
#endif
|
|
#ifdef SCR_NODE_ISLANDS
|
|
scr_node_stack_low = node_low; /* back on the stack that switched away */
|
|
#endif
|
|
}
|
|
|
|
#ifdef SCR_NODE_ISLANDS
|
|
/* A nested entry into Node (scr_node_island.c) runs on Node's stack in a
|
|
* context of its own while the fiber that made the call stays suspended in
|
|
* it. Runtime code that Node calls back there runs as the loop's code does:
|
|
* no current fiber, the main exception cell and ALS slot. It may resume
|
|
* other fibers, which saves the resumer's context in scr_loop_ctx (and,
|
|
* under ASan, its fake stack in the main slot); those belong to whoever
|
|
* resumed the calling fiber, so they are saved here and put back when the
|
|
* nested entry returns, or the calling fiber's next park would return to a
|
|
* context that no longer exists. */
|
|
typedef struct {
|
|
ScrFiber *current;
|
|
ScrAlsCtx **als;
|
|
ScrCtx loop_ctx;
|
|
#ifdef SCR_ASAN_FIBERS
|
|
void *main_fake;
|
|
#endif
|
|
} ScrNestedSave;
|
|
|
|
void *scr_loop_nested_enter(void) {
|
|
ScrNestedSave *s = malloc(sizeof *s);
|
|
if (s == NULL) scr_oom();
|
|
s->current = scr_current;
|
|
s->als = scr_als_active;
|
|
s->loop_ctx = scr_loop_ctx;
|
|
#ifdef SCR_ASAN_FIBERS
|
|
s->main_fake = scr_main_fake_stack;
|
|
scr_main_fake_stack = NULL;
|
|
#endif
|
|
scr_current = NULL;
|
|
scr_exc_swap_cell(NULL);
|
|
scr_als_active = &scr_als_main_slot;
|
|
return s;
|
|
}
|
|
|
|
void scr_loop_nested_leave(void *save) {
|
|
ScrNestedSave *s = save;
|
|
scr_loop_ctx = s->loop_ctx;
|
|
#ifdef SCR_ASAN_FIBERS
|
|
scr_main_fake_stack = s->main_fake;
|
|
#endif
|
|
scr_current = s->current;
|
|
scr_exc_swap_cell(s->current ? &s->current->exc : NULL);
|
|
scr_als_active = s->als;
|
|
free(s);
|
|
}
|
|
#endif
|
|
|
|
static void scr_promise_settle_wake(ScrPromise *p);
|
|
|
|
/* Copies src's settlement into a still-pending dst (payload RETAINED —
|
|
* src can settle other destinations and still be awaited) and wakes dst's
|
|
* waiters. A rejection consumed this way counts as HANDLED on src (a
|
|
* combinator attached a handler, like Node's race). The Promise.race
|
|
* same-inner-type adapter and the rejection path both land here. */
|
|
static void scr_promise_settle_from(ScrPromise *dst, ScrPromise *src) {
|
|
if (dst->state == SCR_PROM_PENDING) {
|
|
dst->state = src->state;
|
|
dst->payload_kind = src->payload_kind;
|
|
dst->f64 = src->f64;
|
|
dst->b = src->b;
|
|
dst->retain_fn = src->retain_fn;
|
|
dst->release_fn = src->release_fn;
|
|
dst->trace_fn = src->trace_fn;
|
|
dst->payload = NULL;
|
|
if (src->payload_kind == SCR_EXC_STR && src->payload) {
|
|
dst->payload = scr_str_retain((ScrStr *)src->payload);
|
|
} else if ((src->payload_kind == SCR_EXC_REF || src->payload_kind == SCR_EXC_OBJ ||
|
|
src->payload_kind == SCR_EXC_PRIMITIVE_REF) &&
|
|
src->payload) {
|
|
dst->payload = src->retain_fn(src->payload);
|
|
}
|
|
scr_promise_settle_wake(dst);
|
|
}
|
|
if (src->state == SCR_PROM_REJECTED) scr_prom_observe(src);
|
|
}
|
|
|
|
/* The emitted same-type Promise.race adapter (see raceAdapterFor). */
|
|
void scr_promise_adapt_copy(ScrPromise *dst, ScrPromise *src) {
|
|
scr_promise_settle_from(dst, src);
|
|
}
|
|
|
|
/* One combinator entry's reaction job. An entry DECIDES the result when
|
|
* its settlement alone settles it — a Promise.all rejection, a
|
|
* Promise.any fulfillment — and copies raw into the result (the first job
|
|
* to run wins through the destination's own pending check; the entries
|
|
* stay HANDLED from the subscription, exactly Node's subscribe-to-
|
|
* everything behavior). Every other settlement stores through the mode's
|
|
* helper at the entry's INPUT index (input order regardless of
|
|
* settlement order: all's values, allSettled's records, any's reasons)
|
|
* and counts down; the last one finishes the result — all/allSettled
|
|
* fulfill with the array (void-element all fulfills void), any rejects
|
|
* through its fail hook. */
|
|
static bool scr_promise_all_decides(const ScrAllState *st, const ScrPromise *src) {
|
|
if (st->mode == SCR_COMBINE_ALL) return src->state == SCR_PROM_REJECTED;
|
|
if (st->mode == SCR_COMBINE_ANY) return src->state == SCR_PROM_FULFILLED;
|
|
return false;
|
|
}
|
|
|
|
static void scr_promise_all_fulfill(ScrAllState *st, ScrPromise *result) {
|
|
if (st->mode == SCR_COMBINE_ANY) {
|
|
st->fail(result, st->values);
|
|
} else if (st->values) {
|
|
scr_promise_fulfill_ref(result, scr_arr_retain(st->values), scr_arr_retain_v,
|
|
scr_arr_release_v, st->values->cyc ? scr_arr_trace_v : NULL);
|
|
} else {
|
|
scr_promise_fulfill_void(result);
|
|
}
|
|
}
|
|
|
|
static void scr_promise_all_count(ScrAllState *st, ScrPromise *result, size_t counted) {
|
|
st->remaining -= counted;
|
|
if (st->remaining == 0 && result->state == SCR_PROM_PENDING) scr_promise_all_fulfill(st, result);
|
|
}
|
|
|
|
static void scr_promise_all_settle(ScrAllState *st, ScrPromise *result, size_t idx,
|
|
ScrPromise *src) {
|
|
if (scr_promise_all_decides(st, src)) {
|
|
scr_promise_settle_from(result, src);
|
|
} else {
|
|
if (st->store) st->store(st->values, (double)idx, src);
|
|
scr_promise_all_count(st, result, 1);
|
|
}
|
|
}
|
|
|
|
static inline void scr_promise_wake_one(ScrFiber *f) {
|
|
#ifdef SCR_WORKERS
|
|
f->waiting = NULL;
|
|
#endif
|
|
scr_ready_push(f);
|
|
}
|
|
|
|
/* Runs one callback waiter against its settled source and drops the
|
|
* waiter's references (its destination and countdown state). */
|
|
static void scr_promise_cb_run(struct ScrPromiseCbWaiter *cb, ScrPromise *src) {
|
|
if (cb->all) {
|
|
scr_promise_all_settle(cb->all, cb->dst, cb->all_idx, src);
|
|
scr_promise_all_state_release(cb->all);
|
|
} else if (src->state == SCR_PROM_FULFILLED) {
|
|
cb->adapt(cb->dst, src);
|
|
#ifdef SCR_NODE_ISLANDS
|
|
} else if (cb->all_idx == SCR_CB_ANY) {
|
|
cb->adapt(cb->dst, src); /* scr_promise_on_settle_sync: rejections too */
|
|
#endif
|
|
} else {
|
|
scr_promise_settle_from(cb->dst, src);
|
|
}
|
|
scr_promise_release(cb->dst);
|
|
}
|
|
|
|
/* Queues a combinator's reaction job for a settled entry. The waiter's
|
|
* references move into the envelope, which also retains the entry. */
|
|
static void scr_promise_cb_queue(const struct ScrPromiseCbWaiter *cb, ScrPromise *src) {
|
|
ScrFiber *job = scr_mem_calloc(sizeof *job);
|
|
if (!job) scr_oom();
|
|
job->cb_job = true;
|
|
job->cb_src = scr_promise_retain(src);
|
|
job->cb = *cb;
|
|
scr_ready_push(job);
|
|
}
|
|
|
|
/* Runs and frees a reaction job envelope. */
|
|
static void scr_promise_job_run(ScrFiber *f) {
|
|
ScrPromise *src = f->cb_src;
|
|
struct ScrPromiseCbWaiter cb = f->cb;
|
|
scr_mem_free(f);
|
|
if (cb.job_at != SCR_CB_BATCH) {
|
|
scr_promise_cb_run(&cb, src);
|
|
} else {
|
|
if (src != NULL) scr_promise_settle_from(cb.dst, src);
|
|
else scr_promise_all_count(cb.all, cb.dst, cb.all_idx);
|
|
scr_promise_all_state_release(cb.all);
|
|
scr_promise_release(cb.dst);
|
|
}
|
|
scr_promise_release(src);
|
|
}
|
|
|
|
static struct ScrPromiseCbWaiter *scr_promise_cb_push(ScrPromise *in) {
|
|
if (in->ncbs == in->cbs_cap) {
|
|
in->cbs_cap = in->cbs_cap ? in->cbs_cap * 2 : 4;
|
|
in->cbs = realloc(in->cbs, in->cbs_cap * sizeof *in->cbs);
|
|
if (!in->cbs) scr_oom();
|
|
}
|
|
return &in->cbs[in->ncbs++];
|
|
}
|
|
|
|
/* A combinator subscribing to an entry: Node's `then` on the entry. The
|
|
* entry counts as handled from here on; a settled entry queues its
|
|
* reaction job now, a pending one when it settles. */
|
|
static void scr_promise_cb_subscribe(ScrPromise *in, struct ScrPromiseCbWaiter cb) {
|
|
scr_prom_observe(in);
|
|
if (in->state != SCR_PROM_PENDING) {
|
|
scr_promise_cb_queue(&cb, in);
|
|
} else {
|
|
cb.job_at = in->nwaiters;
|
|
*scr_promise_cb_push(in) = cb;
|
|
}
|
|
}
|
|
|
|
/* Settlement with callback waiters: combinator jobs enter the microtask
|
|
* queue interleaved with the waking fibers in subscription order, then
|
|
* the conversions run in place (they settle their destinations now, so
|
|
* those destinations' waiters queue behind this promise's). */
|
|
static void scr_promise_settle_wake_cbs(ScrPromise *p) {
|
|
size_t w = 0;
|
|
for (size_t i = 0; i < p->ncbs; i++) {
|
|
const struct ScrPromiseCbWaiter *cb = &p->cbs[i];
|
|
if (cb->job_at == SCR_CB_SYNC) continue;
|
|
for (; w < cb->job_at && w < p->nwaiters; w++) scr_promise_wake_one(p->waiters[w]);
|
|
scr_promise_cb_queue(cb, p);
|
|
}
|
|
for (; w < p->nwaiters; w++) scr_promise_wake_one(p->waiters[w]);
|
|
p->nwaiters = 0;
|
|
for (size_t i = 0; i < p->ncbs; i++) {
|
|
if (p->cbs[i].job_at == SCR_CB_SYNC) scr_promise_cb_run(&p->cbs[i], p);
|
|
}
|
|
p->ncbs = 0;
|
|
}
|
|
|
|
/* Settle helpers wake waiters into the microtask queue and dispatch the
|
|
* callback waiters. */
|
|
static void scr_promise_settle_wake(ScrPromise *p) {
|
|
if (p->ncbs != 0) {
|
|
scr_promise_settle_wake_cbs(p);
|
|
} else {
|
|
for (size_t i = 0; i < p->nwaiters; i++) scr_promise_wake_one(p->waiters[i]);
|
|
p->nwaiters = 0;
|
|
}
|
|
if (p->state == SCR_PROM_REJECTED) scr_track_rejection(p);
|
|
}
|
|
|
|
/* ── Promise.race ─────────────────────────────────────────────────────
|
|
* The compiler emits: a fresh result promise, then one race_join per
|
|
* entry. Each entry settles the result from its reaction job (first job
|
|
* wins), one microtask turn after the entry settles or, for an entry
|
|
* already settled, after the join. Losing entries keep their own
|
|
* settlements (payloads are retained, not moved).
|
|
*
|
|
* race_add is the same callback without the job: the destination is a
|
|
* view of the entry at another inner type (dyn boxes, typed reads of
|
|
* dyn promises), so it settles inside the entry's own settlement. */
|
|
|
|
void scr_promise_race_join(ScrPromise *race, ScrPromise *in,
|
|
void (*adapt)(ScrPromise *dst, ScrPromise *src)) {
|
|
scr_promise_cb_subscribe(
|
|
in, (struct ScrPromiseCbWaiter){ adapt, scr_promise_retain(race), NULL, 0, 0 });
|
|
}
|
|
|
|
#ifdef SCR_NODE_ISLANDS
|
|
/* Node islands (NI-5): runs `cb(in, in)` inside `in`'s settlement, for
|
|
* fulfillments and rejections alike (a native promise's V8 representative
|
|
* settles at the same moment, so its reactions queue where a JavaScript
|
|
* promise's would). `in` must be pending. */
|
|
void scr_promise_on_settle_sync(ScrPromise *in, void (*cb)(ScrPromise *dst, ScrPromise *src)) {
|
|
*scr_promise_cb_push(in) =
|
|
(struct ScrPromiseCbWaiter){ cb, scr_promise_retain(in), NULL, SCR_CB_ANY, SCR_CB_SYNC };
|
|
}
|
|
|
|
/* Node islands (NI-8): a settled promise's dyn payload when the promise
|
|
* owns a reference to it that its trace does not visit (settled without a
|
|
* trace function), else NULL. The cross-heap analysis counts that edge. */
|
|
void *scr_promise_untraced_dyn_payload(const ScrPromise *p) {
|
|
if ((p->payload_kind == SCR_EXC_REF || p->payload_kind == SCR_EXC_OBJ ||
|
|
p->payload_kind == SCR_EXC_PRIMITIVE_REF) &&
|
|
p->payload != NULL && p->trace_fn == NULL && p->release_fn == scr_dyn_release_v)
|
|
return p->payload;
|
|
return NULL;
|
|
}
|
|
#endif
|
|
|
|
void scr_promise_race_add(ScrPromise *race, ScrPromise *in,
|
|
void (*adapt)(ScrPromise *dst, ScrPromise *src)) {
|
|
if (in->state != SCR_PROM_PENDING) {
|
|
if (in->state == SCR_PROM_FULFILLED) adapt(race, in);
|
|
else scr_promise_settle_from(race, in);
|
|
return;
|
|
}
|
|
*scr_promise_cb_push(in) =
|
|
(struct ScrPromiseCbWaiter){ adapt, scr_promise_retain(race), NULL, 0, SCR_CB_SYNC };
|
|
}
|
|
|
|
/* ── Promise.all / allSettled / any ───────────────────────────────────
|
|
* The builder takes the entries in input order. Every entry subscribes a
|
|
* countdown reaction job (Node's `then` on each entry): it runs one
|
|
* microtask turn after the entry settles, or after its add for an entry
|
|
* already settled, and every entry is marked handled at the subscription.
|
|
* The jobs of entries already settled would run back to back with nothing
|
|
* in between, so they run as one batch job: non-deciding settlements
|
|
* store now (the array is unobservable until the result settles) and
|
|
* count down in the job; the batch's first deciding entry in input order
|
|
* settles the result there. A caller that queues other jobs between
|
|
* entries flushes the batch first, so the queue keeps Node's order.
|
|
* `values` arrives EMPTY with cap >= n (the emitted construction passes
|
|
* the length) and is pre-sized here: every slot's zero bits are the
|
|
* per-kind placeholder (0.0 / false / NULL), legal to overwrite through
|
|
* the store helpers and to release. The empty input finishes immediately
|
|
* (awaiters take the settled-await microtask hop, like Node's one-tick
|
|
* resolve). */
|
|
__attribute__((always_inline)) static inline void scr_combine_begin(ScrCombine *c, int mode,
|
|
size_t n, ScrArr *values,
|
|
ScrAllStore store,
|
|
ScrAnyFail fail) {
|
|
c->result = scr_promise_new();
|
|
if (values && n > 0) {
|
|
/* Pre-size as holes. Promise settlement writes the present slots through
|
|
* the normal setters; this also remains safe when n is sparse-sized and
|
|
* the array intentionally has no dense allocation. */
|
|
scr_arr_set_len(values, (double)n);
|
|
}
|
|
ScrAllState *st = scr_mem_alloc(sizeof *st);
|
|
if (!st) scr_oom();
|
|
st->rc = 1; /* the builder's reference, dropped at the end */
|
|
st->remaining = n;
|
|
st->values = values ? scr_arr_retain(values) : NULL;
|
|
st->store = store;
|
|
st->mode = mode;
|
|
st->fail = fail;
|
|
c->st = st;
|
|
c->next = 0;
|
|
c->batched = 0;
|
|
c->decided = NULL;
|
|
c->open = false;
|
|
}
|
|
|
|
__attribute__((always_inline)) static inline void scr_combine_add(ScrCombine *c,
|
|
ScrPromise *in) {
|
|
ScrAllState *st = c->st;
|
|
size_t i = c->next++;
|
|
if (in->state == SCR_PROM_PENDING) {
|
|
st->rc++;
|
|
scr_promise_cb_subscribe(
|
|
in, (struct ScrPromiseCbWaiter){ NULL, scr_promise_retain(c->result), st, i, 0 });
|
|
return;
|
|
}
|
|
scr_prom_observe(in);
|
|
c->open = true;
|
|
if (scr_promise_all_decides(st, in)) {
|
|
if (c->decided == NULL) c->decided = scr_promise_retain(in);
|
|
} else {
|
|
if (st->store) st->store(st->values, (double)i, in);
|
|
c->batched++;
|
|
}
|
|
}
|
|
|
|
__attribute__((always_inline)) static inline void scr_combine_flush(ScrCombine *c) {
|
|
if (!c->open) return;
|
|
ScrFiber *job = scr_mem_calloc(sizeof *job);
|
|
if (!job) scr_oom();
|
|
c->st->rc++;
|
|
job->cb_job = true;
|
|
job->cb_src = c->decided; /* the reference moves in */
|
|
job->cb = (struct ScrPromiseCbWaiter){ NULL, scr_promise_retain(c->result), c->st, c->batched,
|
|
SCR_CB_BATCH };
|
|
scr_ready_push(job);
|
|
c->decided = NULL;
|
|
c->batched = 0;
|
|
c->open = false;
|
|
}
|
|
|
|
__attribute__((always_inline)) static inline ScrPromise *scr_combine_end(ScrCombine *c) {
|
|
scr_combine_flush(c);
|
|
/* The empty input. */
|
|
if (c->next == 0) scr_promise_all_fulfill(c->st, c->result);
|
|
scr_promise_all_state_release(c->st);
|
|
return c->result;
|
|
}
|
|
|
|
/* The builder for callers in other units (the dyn combinators). */
|
|
void scr_promise_combine_begin(ScrCombine *c, int mode, size_t n, ScrArr *values,
|
|
ScrAllStore store, ScrAnyFail fail) {
|
|
scr_combine_begin(c, mode, n, values, store, fail);
|
|
}
|
|
void scr_promise_combine_add(ScrCombine *c, ScrPromise *in) { scr_combine_add(c, in); }
|
|
void scr_promise_combine_flush(ScrCombine *c) { scr_combine_flush(c); }
|
|
ScrPromise *scr_promise_combine_end(ScrCombine *c) { return scr_combine_end(c); }
|
|
|
|
/* One specialization per mode for the compiled Promise<T>[] combinators. */
|
|
__attribute__((always_inline)) static inline ScrPromise *
|
|
scr_promise_combine_array(int mode, ScrArr *ps, ScrArr *values, ScrAllStore store,
|
|
ScrAnyFail fail) {
|
|
size_t n = ps->len;
|
|
ScrCombine c;
|
|
scr_combine_begin(&c, mode, n, values, store, fail);
|
|
for (size_t i = 0; i < n; i++) {
|
|
ScrPromise *in = (ScrPromise *)scr_arr_get_ref(ps, (double)i); /* +1 */
|
|
scr_combine_add(&c, in);
|
|
scr_promise_release(in);
|
|
}
|
|
return scr_combine_end(&c);
|
|
}
|
|
|
|
/* Promise.all: BORROWS the entries array `ps` and the pre-capacity values
|
|
* array `values` (NULL for void elements), retains what it keeps, and
|
|
* returns the result promise +1. */
|
|
ScrPromise *scr_promise_all(ScrArr *ps, ScrArr *values,
|
|
void (*store)(ScrArr *a, double i, ScrPromise *src)) {
|
|
return scr_promise_combine_array(SCR_COMBINE_ALL, ps, values, store, NULL);
|
|
}
|
|
|
|
ScrPromise *scr_promise_all_settled(ScrArr *ps, ScrArr *values, ScrAllStore store) {
|
|
return scr_promise_combine_array(SCR_COMBINE_ALL_SETTLED, ps, values, store, NULL);
|
|
}
|
|
|
|
ScrPromise *scr_promise_any(ScrArr *ps, ScrArr *errors, ScrAllStore store, ScrAnyFail fail) {
|
|
return scr_promise_combine_array(SCR_COMBINE_ANY, ps, errors, store, fail);
|
|
}
|
|
|
|
/* Per-element-kind store helpers for the emitted Promise.all: write the
|
|
* entry's fulfillment payload into the values array at its input index.
|
|
* The payload accessors return retained/by-value (losing a reference is
|
|
* impossible: set_* takes ownership and releases the zero placeholder). */
|
|
void scr_promise_all_store_f64(ScrArr *a, double i, ScrPromise *src) {
|
|
scr_arr_set_f64(a, i, scr_promise_payload_f64(src));
|
|
}
|
|
void scr_promise_all_store_bool(ScrArr *a, double i, ScrPromise *src) {
|
|
scr_arr_set_bool(a, i, scr_promise_payload_bool(src));
|
|
}
|
|
void scr_promise_all_store_str(ScrArr *a, double i, ScrPromise *src) {
|
|
scr_arr_set_ref(a, i, scr_promise_payload_str(src));
|
|
}
|
|
void scr_promise_all_store_ref(ScrArr *a, double i, ScrPromise *src) {
|
|
scr_arr_set_ref(a, i, scr_promise_payload_ref(src));
|
|
}
|
|
|
|
/* Fulfillment payload accessors for the emitted race adapters — all
|
|
* retained/by-value (the source keeps its settlement). */
|
|
double scr_promise_payload_f64(ScrPromise *p) { return p->f64; }
|
|
bool scr_promise_payload_bool(ScrPromise *p) { return p->b; }
|
|
ScrStr *scr_promise_payload_str(ScrPromise *p) {
|
|
return scr_str_retain((ScrStr *)p->payload);
|
|
}
|
|
/* Thin views for the gated dyn-async TU (scr_async_dyn.c): the payload
|
|
* KIND, whether a REF payload is a dyn value (the dyn adapters), and the
|
|
* settled-await primitive (park/hop + rejection re-throw; true =
|
|
* fulfilled). */
|
|
static bool scr_await_settled(ScrPromise *p); /* defined with the await family */
|
|
int scr_promise_payload_kind(const ScrPromise *p) { return (int)p->payload_kind; }
|
|
bool scr_promise_is_rejected(const ScrPromise *p) { return p->state == SCR_PROM_REJECTED; }
|
|
bool scr_promise_is_pending(const ScrPromise *p) { return p->state == SCR_PROM_PENDING; }
|
|
bool scr_promise_payload_is_dyn(const ScrPromise *p) { return p->retain_fn == scr_dyn_retain_v; }
|
|
double scr_promise_payload_num(const ScrPromise *p) { return p->f64; }
|
|
bool scr_promise_payload_flag(const ScrPromise *p) { return p->b; }
|
|
bool scr_promise_await_settled(ScrPromise *p) { return scr_await_settled(p); }
|
|
void scr_promise_payload_release(const ScrPromise *p, void *v) { p->release_fn(v); }
|
|
|
|
void *scr_promise_payload_ref(ScrPromise *p) {
|
|
return p->payload ? p->retain_fn(p->payload) : NULL;
|
|
}
|
|
|
|
/* MOVES a pending exception out of `cell` into `p` as its rejection payload
|
|
* (the cell resets to NONE). Shared by fiber completion and the new-Promise
|
|
* executor runners; callers wake waiters themselves. */
|
|
static void scr_promise_reject_from_cell(ScrPromise *p, ScrExcCell *cell) {
|
|
p->state = SCR_PROM_REJECTED;
|
|
p->payload_kind = cell->kind;
|
|
p->f64 = cell->f64;
|
|
p->b = cell->b;
|
|
p->payload = cell->payload;
|
|
p->retain_fn = cell->retain_fn;
|
|
p->release_fn = cell->release_fn;
|
|
p->trace_fn = cell->trace_fn;
|
|
cell->kind = SCR_EXC_NONE;
|
|
cell->payload = NULL;
|
|
cell->trace_fn = NULL;
|
|
scr_exc_cell_changed();
|
|
}
|
|
|
|
static void scr_fiber_finish(ScrFiber *self) {
|
|
if (self->gen != NULL) {
|
|
/* Generator completion: the emitted trampoline already stored the
|
|
* completion value (or consumed the GENRET sentinel); a real body
|
|
* exception stays pending in this fiber's cell — the consumer-side
|
|
* resume moves it into the resumer's cell. No promise to settle. */
|
|
self->done = true;
|
|
return;
|
|
}
|
|
ScrPromise *p = self->promise;
|
|
#ifdef SCR_WORKERS
|
|
/* Checkpoint, not just test: the body may have unwound for a stop
|
|
* request that its cell does not show yet. */
|
|
if (scr_context_checkpoint()) {
|
|
scr_promise_reject_from_cell(p, &self->exc);
|
|
p->rejection_observed = true;
|
|
self->done = true;
|
|
return;
|
|
}
|
|
#endif
|
|
if (scr_exc_pending()) {
|
|
/* The body's exception escaped: the promise rejects with the payload
|
|
* (moved from the fiber's cell into the promise). */
|
|
scr_promise_reject_from_cell(p, &self->exc);
|
|
}
|
|
/* A returned checked promise may still be resolving after its function
|
|
* body finishes. Its resolver owns settlement and must not wake awaiters
|
|
* until adoption completes (WASI continuations cannot park a C stack). */
|
|
if (p->state != SCR_PROM_PENDING) scr_promise_settle_wake(p);
|
|
self->done = true;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
static void CALLBACK scr_trampoline(void *param) {
|
|
(void)param;
|
|
#else
|
|
static void scr_trampoline(void) {
|
|
#endif
|
|
#ifdef SCR_ASAN_FIBERS
|
|
const void *ob;
|
|
size_t os;
|
|
__sanitizer_finish_switch_fiber(scr_current->fake_stack, &ob, &os);
|
|
#endif
|
|
ScrFiber *self = scr_current;
|
|
self->entry(self, self->argpack);
|
|
scr_fiber_finish(self);
|
|
scr_als_prefix_end(self);
|
|
/* The loop reaps the fiber that comes back to it finished, which need
|
|
* not be the one it resumed: a fiber that continued in place for the
|
|
* awaiters it parked (scr_await_yield) returns to the loop too. */
|
|
if (self->gen == NULL && self->return_to == &scr_loop_ctx) scr_loop_reap = self;
|
|
/* Dead fiber: hop back to whoever resumed us. The loop frees us. */
|
|
scr_switch(&self->ctx, self->return_to, NULL);
|
|
/* unreachable */
|
|
}
|
|
|
|
#ifdef SCR_FAST_CTX
|
|
/* Seeds a fresh fiber's register file so its first switch-in enters the
|
|
* trampoline with an ABI-conforming stack. The stack itself is only
|
|
* written at its top word, so the mapping stays lazily committed. */
|
|
static void scr_fiber_context_start(ScrFiber *f, void (*start)(void)) {
|
|
f->stack = scr_fiber_stack_new();
|
|
uintptr_t top = ((uintptr_t)f->stack + SCR_FIBER_STACK) & ~(uintptr_t)15;
|
|
memset(&f->ctx, 0, sizeof f->ctx);
|
|
#if defined(__x86_64__)
|
|
/* The trampoline starts like a called function: rsp ≡ 8 (mod 16) with a
|
|
* NULL return address at [rsp]; rbp = 0 ends frame-pointer unwinding. */
|
|
void **sp = (void **)top;
|
|
*--sp = NULL;
|
|
f->ctx.gpr[0] = sp;
|
|
f->ctx.gpr[7] = (void *)start;
|
|
f->ctx.mxcsr = __builtin_ia32_stmxcsr();
|
|
__asm__ volatile("fnstcw %0" : "=m"(f->ctx.fpcw));
|
|
#else
|
|
f->ctx.reg[0] = (uint64_t)(uintptr_t)start; /* x19, for scr_ctx_boot */
|
|
f->ctx.reg[11] = (uint64_t)(uintptr_t)scr_ctx_boot; /* x30: resume address */
|
|
f->ctx.reg[12] = (uint64_t)top; /* sp */
|
|
#endif
|
|
}
|
|
#elif !defined(_WIN32) && !defined(__wasi__)
|
|
/* Apple Silicon's makecontext clears all of uc_stack before installing the
|
|
* initial registers. Passing the whole mapping eagerly commits all of it
|
|
* (8 MiB under ASan) per call, defeating the lazy allocation above. Its
|
|
* zero-argument startup frame only needs the top of the downward-growing
|
|
* stack. Initialize that window, then publish the FULL stack bounds before
|
|
* any execution or sanitizer switch. The stack top and saved SP are the
|
|
* same as when initializing the full mapping; ordinary calls can still
|
|
* use all of SCR_FIBER_STACK.
|
|
* See libplatform/src/ucontext/generic/makecontext.c's arm64 implementation. */
|
|
static void scr_fiber_context_start(ScrFiber *f, void (*start)(void)) {
|
|
f->stack = scr_fiber_stack_new();
|
|
if (getcontext(&f->ctx) != 0) scr_trap("scriptc: getcontext failed\n");
|
|
f->ctx.uc_stack.ss_sp = f->stack;
|
|
f->ctx.uc_stack.ss_size = SCR_FIBER_STACK;
|
|
f->ctx.uc_stack.ss_flags = 0;
|
|
f->ctx.uc_link = NULL;
|
|
#if defined(__APPLE__) && defined(__aarch64__)
|
|
const size_t bootstrap = 16 * 1024;
|
|
f->ctx.uc_stack.ss_sp = f->stack + SCR_FIBER_STACK - bootstrap;
|
|
f->ctx.uc_stack.ss_size = bootstrap;
|
|
#endif
|
|
makecontext(&f->ctx, start, 0);
|
|
f->ctx.uc_stack.ss_sp = f->stack;
|
|
f->ctx.uc_stack.ss_size = SCR_FIBER_STACK;
|
|
}
|
|
#endif
|
|
#if !defined(_WIN32) && !defined(__wasi__)
|
|
static void scr_fiber_context_init(ScrFiber *f) { scr_fiber_context_start(f, scr_trampoline); }
|
|
#endif
|
|
|
|
/* Frees a finished fiber's execution resources (the promise release and
|
|
* bookkeeping stay at the call sites). Windows: DeleteFiber tears down the
|
|
* fiber object and its stack — legal here because a finished fiber has
|
|
* switched away and can never be current again. */
|
|
static void scr_fiber_destroy(ScrFiber *f) {
|
|
#ifdef SCR_WORKERS
|
|
if (f->context_previous) {
|
|
*f->context_previous = f->context_next;
|
|
if (f->context_next) f->context_next->context_previous = f->context_previous;
|
|
}
|
|
#endif
|
|
#ifdef SCR_ASAN_FIBERS
|
|
if (f->stackless_previous) {
|
|
*f->stackless_previous = f->stackless_next;
|
|
if (f->stackless_next) f->stackless_next->stackless_previous = f->stackless_previous;
|
|
}
|
|
#endif
|
|
#ifdef __wasi__
|
|
if (f->coro != NULL) scr_wasi_coro_destroy(f->coro);
|
|
#else
|
|
if (f->coro != NULL) scr_coro_destroy(f->coro);
|
|
#ifdef _WIN32
|
|
if (!f->stackless) DeleteFiber(f->ctx);
|
|
#endif
|
|
#endif
|
|
scr_als_ctx_release(f->als);
|
|
if (f->region != NULL) atomic_load_explicit(&scr_region_fiber_hooks, memory_order_relaxed)->drop(f->region);
|
|
#if !defined(_WIN32) && !defined(__wasi__)
|
|
scr_fiber_stack_free(f->stack);
|
|
#else
|
|
free(f->stack);
|
|
#endif
|
|
scr_mem_free(f);
|
|
}
|
|
|
|
#if !defined(_WIN32) && !defined(__wasi__) && !defined(SCR_WORKERS)
|
|
#define SCR_SPAWN_AWAIT 1
|
|
#endif
|
|
|
|
/* Spawns and EAGERLY runs an async body until its first suspension (JS's
|
|
* synchronous-prefix rule). Returns the fiber's promise, +1. A non-NULL
|
|
* `awaiter` (the current fiber, SCR_SPAWN_AWAIT only) awaits that promise
|
|
* immediately: the child saves into and returns to the awaiter's own
|
|
* context, so a child suspension can park the awaiter and switch past
|
|
* it (scr_fiber_park_awaiters). The promise is retained before the child
|
|
* runs, as the child may then finish and be reaped by the loop before the
|
|
* awaiter resumes. */
|
|
static ScrPromise *scr_async_spawn_impl(void (*entry)(ScrFiber *, void *), void *argpack,
|
|
ScrFiber *awaiter) {
|
|
ScrFiber *f = scr_mem_calloc(sizeof *f);
|
|
if (!f) scr_oom();
|
|
scr_fiber_track(f);
|
|
f->promise = scr_promise_new();
|
|
f->entry = entry;
|
|
f->argpack = argpack;
|
|
/* AsyncLocalStorage: the child runs in the SPAWNER's context (Node's
|
|
* init-time capture); snapshots are immutable, so a retain suffices. */
|
|
f->als = scr_als_ctx_retain(*SCR_ALS_SLOT());
|
|
f->prefix_slot = SCR_ALS_SLOT();
|
|
f->region = scr_fiber_region_capture();
|
|
scr_fibers_live++;
|
|
|
|
#ifdef _WIN32
|
|
/* Commit one page initially; Windows commits further pages as the stack
|
|
* grows. Keep the executable's stack reserve, as with CreateFiber,
|
|
* without committing SCR_FIBER_STACK for every idle fiber. */
|
|
ScrCtx here = scr_win_self();
|
|
f->ctx = CreateFiberEx(4096, 0, 0, scr_trampoline, NULL);
|
|
if (f->ctx == NULL) scr_oom();
|
|
#elif defined(__wasi__)
|
|
(void)awaiter;
|
|
ScrFiber *spawner = scr_current;
|
|
ScrAlsCtx **spawner_slot = scr_als_active;
|
|
scr_current = f;
|
|
scr_exc_swap_cell(&f->exc);
|
|
scr_als_active = &f->als;
|
|
entry(f, argpack); /* eager prefix; returns at suspend/final suspend */
|
|
scr_als_prefix_end(f);
|
|
scr_current = spawner;
|
|
scr_exc_swap_cell(spawner ? &spawner->exc : NULL);
|
|
scr_als_active = spawner_slot;
|
|
ScrPromise *result = scr_promise_retain(f->promise);
|
|
if (f->done) {
|
|
scr_promise_release(f->promise);
|
|
scr_fiber_destroy(f);
|
|
scr_fibers_live--;
|
|
}
|
|
return result;
|
|
#else
|
|
scr_fiber_context_init(f);
|
|
|
|
ScrCtx here;
|
|
#endif
|
|
#ifndef __wasi__
|
|
ScrCtx *home = &here;
|
|
ScrPromise *awaited = NULL;
|
|
#ifdef SCR_SPAWN_AWAIT
|
|
if (awaiter != NULL) {
|
|
f->awaiter = awaiter;
|
|
awaiter->await_parked = false;
|
|
home = &awaiter->ctx;
|
|
awaited = scr_promise_retain(f->promise);
|
|
}
|
|
#else
|
|
(void)awaiter;
|
|
#endif
|
|
f->return_to = home;
|
|
ScrFiber *spawner = scr_current;
|
|
ScrAlsCtx **spawner_slot = scr_als_active;
|
|
scr_switch(home, &f->ctx, f);
|
|
/* back: the fiber suspended or finished. The switch back targeted NULL
|
|
* (the child doesn't know who spawned it), which pointed both the current
|
|
* fiber AND the exception machinery at main — restore the spawner's cell
|
|
* too, or a fiber that eagerly spawned another keeps throwing/catching
|
|
* against main's cell and completes with its own cell empty (a phantom
|
|
* `undefined` rejection while the real payload leaks). */
|
|
scr_current = spawner;
|
|
scr_exc_swap_cell(spawner ? &spawner->exc : NULL);
|
|
scr_als_active = spawner_slot;
|
|
if (awaited != NULL) {
|
|
/* Parked by the child: the loop owns (and may have reaped) it. */
|
|
if (awaiter->await_parked) return awaited;
|
|
f->awaiter = NULL;
|
|
}
|
|
ScrPromise *result = awaited != NULL ? awaited : scr_promise_retain(f->promise);
|
|
if (f->done) {
|
|
scr_promise_release(f->promise);
|
|
scr_fiber_destroy(f);
|
|
scr_fibers_live--;
|
|
}
|
|
return result;
|
|
#endif
|
|
}
|
|
|
|
ScrPromise *scr_async_spawn(void (*entry)(ScrFiber *, void *), void *argpack) {
|
|
return scr_async_spawn_impl(entry, argpack, NULL);
|
|
}
|
|
|
|
#ifndef __wasi__
|
|
/* ── stackless async functions ────────────────────────────────────────
|
|
* The compiler lowers an async body that suspends only at its own awaits
|
|
* to an LLVM switched coroutine (scr_async.h). Its fiber is a scheduler
|
|
* envelope without a stack or saved context: the eager prefix runs on the
|
|
* spawner's stack (the WASI spawn's shape), and every later step runs on
|
|
* the loop's (scr_resume_fiber). The exception cell, AsyncLocalStorage
|
|
* context, and Error.stack chain swap exactly as a fiber switch swaps them,
|
|
* and the promise settles and wakes its waiters through the same paths. */
|
|
#ifndef _WIN32
|
|
/* The stack a nest of eager prefixes leaves above the running stack's
|
|
* guard before the next prefix moves to a fresh stack. Each fiber of the
|
|
* stackful form brought its own stack, so an async recursion as deep as
|
|
* memory allows keeps working; between moves the stack guard (with its
|
|
* reserve below the guard) still covers synchronous recursion. */
|
|
#define SCR_STACKLESS_PREFIX_ROOM ((uintptr_t)64 << 10)
|
|
|
|
static SCR_TL struct {
|
|
ScrFiber *body;
|
|
void (*entry)(ScrFiber *, void *);
|
|
void *argpack;
|
|
} scr_prefix_call;
|
|
|
|
/* Runs on the carrier's fresh stack: the body's prefix as the body's own
|
|
* fiber, then back to the spawner, which frees the stack. */
|
|
static void scr_prefix_trampoline(void) {
|
|
#ifdef SCR_ASAN_FIBERS
|
|
const void *ob;
|
|
size_t os;
|
|
__sanitizer_finish_switch_fiber(scr_current->fake_stack, &ob, &os);
|
|
#endif
|
|
ScrFiber *carrier = scr_current;
|
|
ScrFiber *f = scr_prefix_call.body;
|
|
void (*entry)(ScrFiber *, void *) = scr_prefix_call.entry;
|
|
void *argpack = scr_prefix_call.argpack;
|
|
scr_current = f;
|
|
scr_exc_swap_cell(&f->exc);
|
|
scr_als_active = &f->als;
|
|
entry(f, argpack);
|
|
scr_current = carrier;
|
|
scr_switch(&carrier->ctx, carrier->return_to, NULL);
|
|
/* unreachable */
|
|
}
|
|
|
|
static void scr_prefix_on_fresh_stack(ScrFiber *f, void (*entry)(ScrFiber *, void *),
|
|
void *argpack) {
|
|
ScrFiber carrier;
|
|
memset(&carrier, 0, sizeof carrier);
|
|
scr_fiber_context_start(&carrier, scr_prefix_trampoline);
|
|
ScrCtx here;
|
|
carrier.return_to = &here;
|
|
scr_prefix_call.body = f;
|
|
scr_prefix_call.entry = entry;
|
|
scr_prefix_call.argpack = argpack;
|
|
scr_switch(&here, &carrier.ctx, &carrier);
|
|
scr_fiber_stack_free(carrier.stack);
|
|
}
|
|
#endif
|
|
|
|
/* A directly awaited body's fiber record and its promise, one block. */
|
|
typedef struct {
|
|
ScrFiber fiber;
|
|
ScrPromise promise;
|
|
} ScrDirectFiber;
|
|
|
|
/* Creates a stackless body's fiber and runs its eager prefix, which
|
|
* returns at the first suspension or the end. A direct fiber's promise is
|
|
* embedded and immortal (retains and releases skip it). */
|
|
static ScrFiber *scr_spawn_stackless_fiber(void (*entry)(ScrFiber *, void *), void *argpack,
|
|
bool direct) {
|
|
ScrFiber *f;
|
|
if (direct) {
|
|
ScrDirectFiber *block = scr_mem_calloc(sizeof *block);
|
|
if (!block) scr_oom();
|
|
f = &block->fiber;
|
|
f->promise = &block->promise;
|
|
f->promise->rc = SIZE_MAX;
|
|
f->direct = true;
|
|
} else {
|
|
f = scr_mem_calloc(sizeof *f);
|
|
if (!f) scr_oom();
|
|
f->promise = scr_promise_new();
|
|
}
|
|
scr_fiber_track(f);
|
|
f->stackless = true;
|
|
f->als = scr_als_ctx_retain(*SCR_ALS_SLOT());
|
|
f->prefix_slot = SCR_ALS_SLOT();
|
|
/* The lifetime region the eager prefix runs in, which every later step
|
|
* resumes in (scr_resume_fiber), as a fiber's body does. */
|
|
f->region = scr_fiber_region_capture();
|
|
scr_fibers_live++;
|
|
#ifdef SCR_ASAN_FIBERS
|
|
f->stackless_next = scr_stackless_fibers;
|
|
f->stackless_previous = &scr_stackless_fibers;
|
|
if (scr_stackless_fibers) scr_stackless_fibers->stackless_previous = &f->stackless_next;
|
|
scr_stackless_fibers = f;
|
|
#endif
|
|
ScrFiber *spawner = scr_current;
|
|
ScrAlsCtx **spawner_slot = scr_als_active;
|
|
#ifndef _WIN32
|
|
uintptr_t guard = scr_stack_guard;
|
|
if (guard != 0 && (uintptr_t)__builtin_frame_address(0) < guard + SCR_STACKLESS_PREFIX_ROOM) {
|
|
scr_prefix_on_fresh_stack(f, entry, argpack);
|
|
} else
|
|
#endif
|
|
{
|
|
scr_current = f;
|
|
scr_exc_swap_cell(&f->exc);
|
|
scr_als_active = &f->als;
|
|
entry(f, argpack); /* the eager prefix: returns at a suspension or the end */
|
|
}
|
|
scr_als_prefix_end(f);
|
|
scr_current = spawner;
|
|
scr_exc_swap_cell(spawner ? &spawner->exc : NULL);
|
|
scr_als_active = spawner_slot;
|
|
return f;
|
|
}
|
|
|
|
ScrPromise *scr_async_spawn_stackless(void (*entry)(ScrFiber *, void *), void *argpack) {
|
|
ScrFiber *f = scr_spawn_stackless_fiber(entry, argpack, false);
|
|
ScrPromise *result = scr_promise_retain(f->promise);
|
|
if (f->done) {
|
|
scr_promise_release(f->promise);
|
|
scr_fiber_destroy(f);
|
|
scr_fibers_live--;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/* A stackless body that a stackless coroutine awaits right away (the
|
|
* compiler's direct await): its promise never escapes, so it is embedded
|
|
* in the fiber record, which the awaiter takes the result from
|
|
* (scr_coro_direct_*) and then destroys. */
|
|
ScrFiber *scr_async_spawn_direct(void (*entry)(ScrFiber *, void *), void *argpack) {
|
|
return scr_spawn_stackless_fiber(entry, argpack, true);
|
|
}
|
|
#endif
|
|
|
|
static void scr_await_park(ScrPromise *p);
|
|
static void scr_promise_rethrow(ScrPromise *p);
|
|
|
|
/* `await f(...)` of a fiber-backed async f in one call (the compiler's
|
|
* await-call entry): spawn, then await the promise, with the same turns
|
|
* as the two steps. When the child suspends in its eager prefix, the
|
|
* awaiter is already parked on the pending promise, so the settle wake
|
|
* resumes it with the result and no further hop; a child that finished
|
|
* eagerly leaves a settled promise, which the ordinary await hops on.
|
|
* Returns the promise +1; `ok` false means the rejection was re-thrown. */
|
|
|
|
static ScrPromise *scr_async_spawn_await(void (*entry)(ScrFiber *, void *), void *argpack,
|
|
bool *ok) {
|
|
#ifdef SCR_SPAWN_AWAIT
|
|
ScrFiber *self = scr_current;
|
|
if (self != NULL && self->gen == NULL && self->inline_depth == 0 && !self->stackless) {
|
|
ScrPromise *p = scr_async_spawn_impl(entry, argpack, self);
|
|
if (self->await_parked) {
|
|
self->await_parked = false;
|
|
while (p->state == SCR_PROM_PENDING) scr_await_park(p);
|
|
scr_prom_observe(p);
|
|
if (p->state == SCR_PROM_REJECTED) {
|
|
scr_promise_rethrow(p);
|
|
*ok = false;
|
|
} else {
|
|
*ok = true;
|
|
}
|
|
return p;
|
|
}
|
|
*ok = scr_await_settled(p);
|
|
return p;
|
|
}
|
|
#endif
|
|
ScrPromise *p = scr_async_spawn(entry, argpack);
|
|
*ok = scr_await_settled(p);
|
|
return p;
|
|
}
|
|
|
|
void scr_async_spawn_await_void(void (*entry)(ScrFiber *, void *), void *argpack) {
|
|
bool ok;
|
|
scr_promise_release(scr_async_spawn_await(entry, argpack, &ok));
|
|
}
|
|
double scr_async_spawn_await_f64(void (*entry)(ScrFiber *, void *), void *argpack) {
|
|
bool ok;
|
|
ScrPromise *p = scr_async_spawn_await(entry, argpack, &ok);
|
|
double v = ok ? p->f64 : 0;
|
|
scr_promise_release(p);
|
|
return v;
|
|
}
|
|
bool scr_async_spawn_await_bool(void (*entry)(ScrFiber *, void *), void *argpack) {
|
|
bool ok;
|
|
ScrPromise *p = scr_async_spawn_await(entry, argpack, &ok);
|
|
bool v = ok ? p->b : false;
|
|
scr_promise_release(p);
|
|
return v;
|
|
}
|
|
ScrStr *scr_async_spawn_await_str(void (*entry)(ScrFiber *, void *), void *argpack) {
|
|
bool ok;
|
|
ScrPromise *p = scr_async_spawn_await(entry, argpack, &ok);
|
|
ScrStr *v = ok && p->payload ? scr_str_retain((ScrStr *)p->payload) : NULL;
|
|
scr_promise_release(p);
|
|
return v;
|
|
}
|
|
void *scr_async_spawn_await_ref(void (*entry)(ScrFiber *, void *), void *argpack) {
|
|
bool ok;
|
|
ScrPromise *p = scr_async_spawn_await(entry, argpack, &ok);
|
|
void *v = ok && p->payload ? p->retain_fn(p->payload) : NULL;
|
|
scr_promise_release(p);
|
|
return v;
|
|
}
|
|
|
|
/* ── fiberless async calls ─────────────────────────────────────────────
|
|
* An async function whose body the compiler proved can never park its
|
|
* execution context (no await, no other suspending runtime call, directly
|
|
* or through the functions it calls) completes synchronously on every
|
|
* path, exactly like a fiber that never suspends: JS runs an async body
|
|
* eagerly to its first await, and the promise settles before the call
|
|
* returns. Running it on the caller's stack skips the fiber, its stack,
|
|
* and two context switches while keeping every observable effect: the
|
|
* settled promise is created and settled in the same order, rejections
|
|
* enter the unhandled ledger through the same settle path, and the frame
|
|
* isolates what a fresh fiber would — the Error.stack frame chain
|
|
* (cell->stack). The AsyncLocalStorage context is the caller's throughout:
|
|
* the whole body is the synchronous prefix Node runs in the caller's
|
|
* execution, so a store it enters stays entered after the call
|
|
* (scr_als_prefix_end). The caller's exception cell is clean at any call
|
|
* (try/finally stashes in-flight exceptions), so a pending exception
|
|
* after the body is the body's own throw. */
|
|
static void scr_inline_park_trap(void) {
|
|
scr_trap("scriptc: internal error: fiberless async body suspended\n");
|
|
}
|
|
|
|
/* A stackless body has no stack to park: only its own emitted awaits may
|
|
* suspend it (scr_coro_await_prepare). */
|
|
static void scr_stackless_park_trap(void) {
|
|
scr_trap("scriptc: internal error: stackless async body parked its stack\n");
|
|
}
|
|
|
|
void scr_async_inline_enter(ScrAsyncInline *frame) {
|
|
ScrExcCell *cell = scr_exc_current_cell();
|
|
frame->stack = cell->stack;
|
|
cell->stack = NULL;
|
|
if (scr_current != NULL) scr_current->inline_depth++;
|
|
}
|
|
|
|
void scr_async_inline_exit(ScrAsyncInline *frame) {
|
|
if (scr_current != NULL) scr_current->inline_depth--;
|
|
scr_exc_current_cell()->stack = (ScrStackFrame *)frame->stack;
|
|
}
|
|
|
|
ScrPromise *scr_async_inline_leave(ScrAsyncInline *frame) {
|
|
scr_async_inline_exit(frame);
|
|
return scr_promise_new();
|
|
}
|
|
|
|
/* A runtime-authored C continuation whose first operation awaits one known
|
|
* dependency. Native fibers can park normally. wasm32 has no resumable C
|
|
* stack, so queue the fiber only after the dependency settles; the entry's
|
|
* await then extracts the result without attempting a stack switch. The
|
|
* READY queue supplies the settled-await microtask hop in both cases. */
|
|
ScrPromise *scr_async_spawn_after(ScrPromise *dependency,
|
|
void (*entry)(ScrFiber *, void *),
|
|
void *argpack) {
|
|
#ifndef __wasi__
|
|
(void)dependency;
|
|
return scr_async_spawn(entry, argpack);
|
|
#else
|
|
ScrFiber *f = scr_mem_calloc(sizeof *f);
|
|
if (!f) scr_oom();
|
|
f->promise = scr_promise_new();
|
|
f->entry = entry;
|
|
f->argpack = argpack;
|
|
f->als = scr_als_ctx_retain(*SCR_ALS_SLOT());
|
|
f->region = scr_fiber_region_capture();
|
|
scr_fibers_live++;
|
|
|
|
if (dependency->state == SCR_PROM_PENDING) {
|
|
scr_promise_add_waiter(dependency, f);
|
|
} else {
|
|
scr_ready_push(f);
|
|
}
|
|
return scr_promise_retain(f->promise);
|
|
#endif
|
|
}
|
|
|
|
/* Where a suspending async fiber switches to. During the eager prefix
|
|
* under an await-call entry (awaiter set), control would return to the
|
|
* awaiter only for it to park on this fiber's still-pending promise and
|
|
* switch to its own resumer: park it here, for each such awaiter up the
|
|
* chain, and switch there directly. Nothing runs in between either way. */
|
|
static ScrCtx *scr_fiber_park_awaiters(ScrFiber *self) {
|
|
scr_als_prefix_end(self);
|
|
ScrCtx *to = self->return_to;
|
|
#ifdef SCR_SPAWN_AWAIT
|
|
for (ScrFiber *cur = self; cur->awaiter != NULL;) {
|
|
ScrFiber *aw = cur->awaiter;
|
|
cur->awaiter = NULL;
|
|
/* The awaiter's await is its own suspension: its prefix ends here. */
|
|
scr_als_prefix_end(aw);
|
|
scr_promise_add_waiter(cur->promise, aw);
|
|
aw->await_parked = true;
|
|
to = aw->return_to;
|
|
cur = aw;
|
|
}
|
|
/* Whoever resumes this fiber next (the loop) becomes its return target;
|
|
* also right when the caller continues in place without switching. */
|
|
self->return_to = to;
|
|
#endif
|
|
return to;
|
|
}
|
|
|
|
/* Parks the current fiber on `p` until it settles. Only fibers await. */
|
|
static void scr_await_park(ScrPromise *p) {
|
|
ScrFiber *self = scr_current;
|
|
if (!self) {
|
|
fputs("scriptc: internal error: await outside an async function\n", stderr);
|
|
abort();
|
|
}
|
|
if (self->inline_depth != 0) scr_inline_park_trap();
|
|
if (self->stackless) scr_stackless_park_trap();
|
|
scr_promise_add_waiter(p, self);
|
|
#ifdef SCR_WORKERS
|
|
self->waiting = p;
|
|
#endif
|
|
scr_switch(&self->ctx, scr_fiber_park_awaiters(self), NULL);
|
|
#ifdef SCR_WORKERS
|
|
/* Shutdown resumes suspended stacks without settling their dependencies.
|
|
* Remove the borrowed waiter before the stack releases its promise. */
|
|
if (self->waiting) {
|
|
for (size_t i = 0; i < p->nwaiters; i++) {
|
|
if (p->waiters[i] != self) continue;
|
|
memmove(p->waiters + i, p->waiters + i + 1, (--p->nwaiters - i) * sizeof(*p->waiters));
|
|
break;
|
|
}
|
|
self->waiting = NULL;
|
|
}
|
|
#endif
|
|
/* Resumed by the loop: return_to must now point at the loop's context. */
|
|
}
|
|
|
|
#ifdef __wasi__
|
|
/* The emitted coroutine suspends immediately after these preparation
|
|
* calls. Settled promises still queue one ready turn; pending promises own
|
|
* the continuation through their waiter list. */
|
|
void scr_wasi_await_prepare(ScrFiber *self, ScrPromise *p) {
|
|
if (p->state == SCR_PROM_PENDING) {
|
|
scr_promise_add_waiter(p, self);
|
|
} else {
|
|
scr_ready_push(self);
|
|
}
|
|
}
|
|
|
|
void scr_wasi_await_hop_prepare(ScrFiber *self) { scr_ready_push(self); }
|
|
|
|
bool scr_wasi_module_await_prepare(ScrFiber *self, ScrPromise *p) {
|
|
if (p->state != SCR_PROM_PENDING) return false;
|
|
scr_wasi_await_prepare(self, p);
|
|
return true;
|
|
}
|
|
|
|
void scr_wasi_async_finish(ScrFiber *self) { scr_fiber_finish(self); }
|
|
|
|
void scr_wasi_gen_finish(ScrFiber *self) {
|
|
if (scr_exc_genret_pending()) {
|
|
scr_exc_clear();
|
|
scr_gen_ret_to_out(self->gen);
|
|
}
|
|
scr_fiber_finish(self);
|
|
}
|
|
#else
|
|
/* A stackless generator body's final block: the native trampoline
|
|
* epilogue (consume a GENRET unwind, promoting the parked .return value)
|
|
* plus the fiber's completion mark. */
|
|
void scr_gen_coro_finish(ScrFiber *self) {
|
|
if (scr_exc_genret_pending()) {
|
|
scr_exc_clear();
|
|
scr_gen_ret_to_out(self->gen);
|
|
}
|
|
scr_fiber_finish(self);
|
|
}
|
|
|
|
/* A stackless async body's await. A pending promise owns the continuation
|
|
* through its waiter list. A settled one (or a non-promise operand) takes
|
|
* one microtask turn on the READY queue, except where scr_await_yield
|
|
* continues a fiber in place: resumed by the loop's drain with nothing
|
|
* else queued, this body is the very next job either way. */
|
|
static bool scr_coro_hop_needed(ScrFiber *self) {
|
|
if (self->return_to == &scr_loop_ctx && SCR_READY_IDLE()) return false;
|
|
scr_ready_push(self);
|
|
return true;
|
|
}
|
|
|
|
bool scr_coro_await_prepare(ScrFiber *self, ScrPromise *p) {
|
|
if (p->state == SCR_PROM_PENDING) {
|
|
scr_promise_add_waiter(p, self);
|
|
return true;
|
|
}
|
|
return scr_coro_hop_needed(self);
|
|
}
|
|
|
|
bool scr_coro_hop_prepare(ScrFiber *self) { return scr_coro_hop_needed(self); }
|
|
|
|
/* A directly awaited body's end: its awaiter is the promise's only waiter,
|
|
* and it observes a rejection when it takes the result, so the body wakes
|
|
* it where the settlement would, without the unhandled-rejection ledger. */
|
|
static void scr_direct_finish(ScrFiber *self) {
|
|
ScrPromise *p = self->promise;
|
|
bool stopped = false;
|
|
#ifdef SCR_WORKERS
|
|
/* A stop request rejects like the fiber epilogue does (scr_fiber_finish). */
|
|
stopped = scr_context_checkpoint();
|
|
#endif
|
|
if (stopped || scr_exc_pending()) scr_promise_reject_from_cell(p, &self->exc);
|
|
if (stopped) p->rejection_observed = true;
|
|
if (p->state == SCR_PROM_PENDING)
|
|
scr_trap("scriptc: internal error: directly awaited body finished unsettled\n");
|
|
self->done = true;
|
|
if (self->direct_awaiter != NULL) scr_promise_wake_one(self->direct_awaiter);
|
|
}
|
|
|
|
void scr_coro_async_finish(ScrFiber *self) {
|
|
if (self->direct) scr_direct_finish(self);
|
|
else scr_fiber_finish(self);
|
|
}
|
|
|
|
/* Extraction after the await's suspension: the promise has settled. */
|
|
static bool scr_coro_await_settled(ScrPromise *p) {
|
|
if (p->state == SCR_PROM_PENDING)
|
|
scr_trap("scriptc: internal error: resumed before awaited promise settled\n");
|
|
scr_prom_observe(p);
|
|
if (p->state == SCR_PROM_REJECTED) {
|
|
scr_promise_rethrow(p);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void scr_coro_await_void(ScrPromise *p) { scr_coro_await_settled(p); }
|
|
double scr_coro_await_f64(ScrPromise *p) { return scr_coro_await_settled(p) ? p->f64 : 0; }
|
|
bool scr_coro_await_bool(ScrPromise *p) { return scr_coro_await_settled(p) ? p->b : false; }
|
|
ScrStr *scr_coro_await_str(ScrPromise *p) {
|
|
return scr_coro_await_settled(p) && p->payload ? scr_str_retain((ScrStr *)p->payload) : NULL;
|
|
}
|
|
void *scr_coro_await_ref(ScrPromise *p) {
|
|
return scr_coro_await_settled(p) && p->payload ? p->retain_fn(p->payload) : NULL;
|
|
}
|
|
|
|
/* A direct await of a stackless body (scr_async_spawn_direct): a body
|
|
* still running gets the coroutine as its only awaiter, which its finish
|
|
* wakes as a settlement wakes a promise's waiter; a finished body is a
|
|
* settled promise, which takes one microtask turn. */
|
|
bool scr_coro_direct_prepare(ScrFiber *self, ScrFiber *callee) {
|
|
if (!callee->done) {
|
|
callee->direct_awaiter = self;
|
|
return true;
|
|
}
|
|
return scr_coro_hop_needed(self);
|
|
}
|
|
|
|
/* The result of a finished direct body: its promise's settlement, read as
|
|
* scr_coro_await_* reads it (a rejection re-throws), then the record goes. */
|
|
static inline void scr_direct_destroy(ScrFiber *f) {
|
|
/* Only a rejection reason is left: values moved out, and nothing waits
|
|
* on or shares the promise. */
|
|
if (f->promise->payload_kind != SCR_EXC_NONE) scr_promise_release_payload(f->promise);
|
|
scr_fiber_destroy(f);
|
|
scr_fibers_live--;
|
|
}
|
|
|
|
void scr_coro_direct_void(ScrFiber *f) {
|
|
scr_coro_await_settled(f->promise);
|
|
scr_direct_destroy(f);
|
|
}
|
|
double scr_coro_direct_f64(ScrFiber *f) {
|
|
double v = scr_coro_await_settled(f->promise) ? f->promise->f64 : 0;
|
|
scr_direct_destroy(f);
|
|
return v;
|
|
}
|
|
bool scr_coro_direct_bool(ScrFiber *f) {
|
|
bool v = scr_coro_await_settled(f->promise) ? f->promise->b : false;
|
|
scr_direct_destroy(f);
|
|
return v;
|
|
}
|
|
/* String and reference results move out of the record (+1). */
|
|
ScrStr *scr_coro_direct_str(ScrFiber *f) {
|
|
ScrPromise *p = f->promise;
|
|
ScrStr *v = NULL;
|
|
if (scr_coro_await_settled(p)) {
|
|
v = (ScrStr *)p->payload;
|
|
p->payload = NULL;
|
|
p->payload_kind = SCR_EXC_NONE;
|
|
}
|
|
scr_direct_destroy(f);
|
|
return v;
|
|
}
|
|
void *scr_coro_direct_ref(ScrFiber *f) {
|
|
ScrPromise *p = f->promise;
|
|
void *v = NULL;
|
|
if (scr_coro_await_settled(p)) {
|
|
v = p->payload;
|
|
p->payload = NULL;
|
|
p->payload_kind = SCR_EXC_NONE;
|
|
}
|
|
scr_direct_destroy(f);
|
|
return v;
|
|
}
|
|
#endif
|
|
|
|
/* One microtask hop: park the current fiber on the READY queue itself and
|
|
* yield. JS's `await` ALWAYS suspends — even on an already-settled promise
|
|
* the continuation runs as a microtask — so without this hop an awaiter of
|
|
* a settled promise would continue synchronously inside the spawner,
|
|
* observably earlier than Node (corpus 1428 pins the ordering). */
|
|
static void scr_await_yield(void) {
|
|
ScrFiber *self = scr_current;
|
|
if (!self) {
|
|
fputs("scriptc: internal error: await outside an async function\n", stderr);
|
|
abort();
|
|
}
|
|
if (self->inline_depth != 0) scr_inline_park_trap();
|
|
if (self->stackless) scr_stackless_park_trap();
|
|
if (self->gen != NULL) {
|
|
scr_ready_push(self);
|
|
scr_switch(&self->ctx, self->return_to, NULL);
|
|
return;
|
|
}
|
|
/* The hop would return control to the loop's microtask drain with
|
|
* nothing else queued (this async fiber, or the awaiters parked for it,
|
|
* were resumed by the drain), so this fiber would be the very next job:
|
|
* the drain runs to exhaustion with no checkpoint in between. Continuing
|
|
* in place is the same turn order without the two switches. A fiber
|
|
* resumed by its spawner (the eager prefix) must still hop: the spawner
|
|
* has code to run first. */
|
|
ScrCtx *to = scr_fiber_park_awaiters(self);
|
|
if (to == &scr_loop_ctx && SCR_READY_IDLE()) return;
|
|
scr_ready_push(self);
|
|
scr_switch(&self->ctx, to, NULL);
|
|
}
|
|
|
|
/* The emitted promise-or-absent await's unit arm (`await u` where u holds
|
|
* undefined/null): JS awaits non-thenables through exactly one microtask
|
|
* turn — the same hop a settled promise takes. */
|
|
void scr_await_hop(void) { scr_await_yield(); }
|
|
|
|
/* Copies a rejection into the active execution context's exception cell.
|
|
* The payload is RETAINED, not moved — a promise can be awaited more than
|
|
* once, and the executable's top-level completion probe consumes the same
|
|
* settlement from the main stack after the loop drains. */
|
|
static void scr_promise_rethrow(ScrPromise *p) {
|
|
switch (p->payload_kind) {
|
|
case SCR_EXC_F64: scr_throw_f64(p->f64); break;
|
|
case SCR_EXC_BOOL: scr_throw_bool(p->b); break;
|
|
case SCR_EXC_STR: scr_throw_str(scr_str_retain((ScrStr *)p->payload)); break;
|
|
case SCR_EXC_REF: scr_throw_ref(p->retain_fn(p->payload), p->retain_fn, p->release_fn, p->trace_fn); break;
|
|
case SCR_EXC_PRIMITIVE_REF: scr_throw_primitive_ref(p->retain_fn(p->payload), p->retain_fn, p->release_fn, p->trace_fn); break;
|
|
case SCR_EXC_OBJ: scr_throw_obj(p->retain_fn(p->payload), p->retain_fn, p->release_fn, p->trace_fn); break;
|
|
case SCR_EXC_NONE:
|
|
case SCR_EXC_TERMINATE:
|
|
case SCR_EXC_GENRET: /* unreachable: the sentinel never settles a promise */
|
|
scr_throw_str(scr_str_new("undefined", 9));
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Await result extraction. Rejection re-throws into the awaiter. */
|
|
static bool scr_await_settled(ScrPromise *p) {
|
|
#ifdef SCR_WORKERS
|
|
if (scr_context_checkpoint()) return false;
|
|
#endif
|
|
#ifdef __wasi__
|
|
if (p->state == SCR_PROM_PENDING) {
|
|
fputs("scriptc: internal error: resumed before awaited promise settled\n", stderr);
|
|
abort();
|
|
}
|
|
#else
|
|
if (p->state != SCR_PROM_PENDING) scr_await_yield();
|
|
while (p->state == SCR_PROM_PENDING) {
|
|
#ifdef SCR_WORKERS
|
|
if (scr_context_checkpoint()) return false;
|
|
#endif
|
|
scr_await_park(p);
|
|
}
|
|
#ifdef SCR_WORKERS
|
|
if (scr_context_checkpoint()) return false;
|
|
#endif
|
|
#endif
|
|
scr_prom_observe(p);
|
|
if (p->state == SCR_PROM_REJECTED) {
|
|
scr_promise_rethrow(p);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool scr_promise_settled_now(ScrPromise *p) {
|
|
if (p->state == SCR_PROM_PENDING) return false;
|
|
scr_prom_observe(p);
|
|
if (p->state == SCR_PROM_REJECTED) scr_promise_rethrow(p);
|
|
return true;
|
|
}
|
|
|
|
bool scr_promise_payload_truthy(const ScrPromise *p) {
|
|
switch (p->payload_kind) {
|
|
case SCR_EXC_NONE: return false;
|
|
case SCR_EXC_F64: return p->f64 != 0 && p->f64 == p->f64;
|
|
case SCR_EXC_BOOL: return p->b;
|
|
case SCR_EXC_STR: return p->payload && ((const ScrStr *)p->payload)->len > 0;
|
|
default:
|
|
if (p->retain_fn == scr_dyn_retain_v) return p->payload && scr_dyn_truthy(p->payload);
|
|
return p->payload != NULL;
|
|
}
|
|
}
|
|
|
|
double scr_await_f64(ScrPromise *p) {
|
|
return scr_await_settled(p) ? p->f64 : 0;
|
|
}
|
|
bool scr_await_bool(ScrPromise *p) {
|
|
return scr_await_settled(p) ? p->b : false;
|
|
}
|
|
void *scr_await_ref(ScrPromise *p) {
|
|
return scr_await_settled(p) && p->payload ? p->retain_fn(p->payload) : NULL;
|
|
}
|
|
void scr_await_void(ScrPromise *p) { scr_await_settled(p); }
|
|
|
|
/* ECMAScript's INTERNAL module-dependency wait. Unlike a user-authored
|
|
* await, an already-completed dependency does not introduce a promise-job
|
|
* hop: the evaluator continues synchronously into the importer. A pending
|
|
* dependency still parks this module fiber, and rejection propagates into
|
|
* it exactly like an ordinary await. */
|
|
void scr_module_await(ScrPromise *p) {
|
|
#ifndef __wasi__
|
|
while (p->state == SCR_PROM_PENDING) scr_await_park(p);
|
|
#endif
|
|
scr_prom_observe(p);
|
|
if (p->state == SCR_PROM_REJECTED) scr_promise_rethrow(p);
|
|
}
|
|
|
|
int scr_promise_finish_top_level(ScrPromise *p) {
|
|
if (p->state == SCR_PROM_PENDING) {
|
|
/* ECMAScript module evaluation is still pending, but Node's ref'd
|
|
* event-loop work is exhausted. Node exits with its dedicated
|
|
* unsettled-top-level-await status unless the program set a nonzero
|
|
* process.exitCode (`process.exitCode || 13`). Uncaught errors and
|
|
* reported rejections leave through other exits, so a nonzero hint
|
|
* here is the program's own. */
|
|
if (scr_exit_code_hint_get() == 0) scr_exit_code_note(13);
|
|
return 13;
|
|
}
|
|
scr_prom_observe(p);
|
|
return p->state == SCR_PROM_REJECTED && !p->top_level_handled ? 1 : 0;
|
|
}
|
|
|
|
void scr_promise_rethrow_top_level(ScrPromise *p) {
|
|
if (p->state == SCR_PROM_REJECTED) scr_promise_rethrow(p);
|
|
}
|
|
|
|
/* Fulfillment storers, called by the compiled trampolines / resolve thunks. */
|
|
void scr_promise_fulfill_f64(ScrPromise *p, double v) {
|
|
if (p->state != SCR_PROM_PENDING) return; /* first settle wins */
|
|
p->state = SCR_PROM_FULFILLED;
|
|
p->payload_kind = SCR_EXC_F64;
|
|
p->f64 = v;
|
|
scr_promise_settle_wake(p);
|
|
}
|
|
void scr_promise_fulfill_bool(ScrPromise *p, bool v) {
|
|
if (p->state != SCR_PROM_PENDING) return;
|
|
p->state = SCR_PROM_FULFILLED;
|
|
p->payload_kind = SCR_EXC_BOOL;
|
|
p->b = v;
|
|
scr_promise_settle_wake(p);
|
|
}
|
|
void scr_promise_fulfill_ref(ScrPromise *p, void *v, void *(*retain)(void *), void (*release)(void *), ScrTraceFn trace) {
|
|
if (p->state != SCR_PROM_PENDING) {
|
|
if (release) release(v); /* value was +1 for us; drop it */
|
|
return;
|
|
}
|
|
p->state = SCR_PROM_FULFILLED;
|
|
p->payload_kind = v ? SCR_EXC_REF : SCR_EXC_NONE;
|
|
p->payload = v;
|
|
p->retain_fn = retain;
|
|
p->release_fn = release;
|
|
p->trace_fn = trace;
|
|
scr_promise_settle_wake(p);
|
|
}
|
|
void scr_promise_fulfill_like(ScrPromise *p, const ScrPromise *src) {
|
|
if (p->state != SCR_PROM_PENDING) return;
|
|
p->state = SCR_PROM_FULFILLED;
|
|
p->payload_kind = src->payload_kind;
|
|
p->f64 = src->f64;
|
|
p->b = src->b;
|
|
p->payload = src->payload && src->retain_fn ? src->retain_fn(src->payload) : src->payload;
|
|
p->retain_fn = src->retain_fn;
|
|
p->release_fn = src->release_fn;
|
|
p->trace_fn = src->trace_fn;
|
|
scr_promise_settle_wake(p);
|
|
}
|
|
void scr_promise_fulfill_void(ScrPromise *p) {
|
|
if (p->state != SCR_PROM_PENDING) return;
|
|
p->state = SCR_PROM_FULFILLED;
|
|
p->payload_kind = SCR_EXC_NONE;
|
|
scr_promise_settle_wake(p);
|
|
}
|
|
|
|
/* String payloads ride the STR kind so awaits type them precisely. */
|
|
void scr_promise_fulfill_str(ScrPromise *p, ScrStr *v) {
|
|
if (p->state != SCR_PROM_PENDING) {
|
|
scr_str_release(v);
|
|
return;
|
|
}
|
|
p->state = SCR_PROM_FULFILLED;
|
|
p->payload_kind = SCR_EXC_STR;
|
|
p->payload = v; /* ownership moves in */
|
|
p->retain_fn = (void *(*)(void *))scr_str_retain;
|
|
p->release_fn = (void (*)(void *))scr_str_release;
|
|
scr_promise_settle_wake(p);
|
|
}
|
|
ScrStr *scr_await_str(ScrPromise *p) {
|
|
return scr_await_settled(p) && p->payload ? scr_str_retain((ScrStr *)p->payload) : NULL;
|
|
}
|
|
|
|
void scr_promise_reject_pending(ScrPromise *p) {
|
|
if (!scr_exc_pending()) return;
|
|
if (p->state == SCR_PROM_PENDING) {
|
|
scr_promise_reject_from_cell(p, scr_exc_current_cell());
|
|
scr_promise_settle_wake(p);
|
|
} else {
|
|
scr_exc_clear();
|
|
}
|
|
}
|
|
|
|
/* ── settled-promise minting (the fs/promises bridge) ─────────────────
|
|
* The promise-returning stdlib functions run their syscall SYNCHRONOUSLY
|
|
* (the documented non-interleaving divergence, SEMANTICS.md) and wrap the
|
|
* outcome here: a pending exception in the active cell becomes the
|
|
* promise's REJECTION (moved out of the cell — the caller's pending check
|
|
* then sees a clean cell, exactly like Node where fs/promises failures
|
|
* reject instead of throwing), anything else fulfills with the payload.
|
|
* The rejected case still enters the unhandled-rejection ledger, so a
|
|
* never-awaited failure reports at exit like any other rejection. */
|
|
|
|
static ScrPromise *scr_promise_settled_common(void) {
|
|
if (!scr_exc_pending()) return NULL;
|
|
ScrPromise *p = scr_promise_new();
|
|
scr_promise_reject_from_cell(p, scr_exc_current_cell());
|
|
scr_promise_settle_wake(p); /* no waiters yet; enters the ledger */
|
|
return p;
|
|
}
|
|
|
|
ScrPromise *scr_promise_settled_str(ScrStr *v) {
|
|
ScrPromise *rejected = scr_promise_settled_common();
|
|
if (rejected) {
|
|
scr_str_release(v); /* NULL-tolerant; the op returned a dummy */
|
|
return rejected;
|
|
}
|
|
ScrPromise *p = scr_promise_new();
|
|
scr_promise_fulfill_str(p, v); /* moves in */
|
|
return p;
|
|
}
|
|
|
|
ScrPromise *scr_promise_settled_void(void) {
|
|
ScrPromise *rejected = scr_promise_settled_common();
|
|
if (rejected) return rejected;
|
|
ScrPromise *p = scr_promise_new();
|
|
scr_promise_fulfill_void(p);
|
|
return p;
|
|
}
|
|
|
|
ScrPromise *scr_promise_settled_ref(void *v, void *(*retain)(void *), void (*release)(void *),
|
|
ScrTraceFn trace) {
|
|
ScrPromise *rejected = scr_promise_settled_common();
|
|
if (rejected) {
|
|
if (v && release) release(v);
|
|
return rejected;
|
|
}
|
|
ScrPromise *p = scr_promise_new();
|
|
scr_promise_fulfill_ref(p, v, retain, release, trace); /* moves in */
|
|
return p;
|
|
}
|
|
|
|
/* ── fs/promises ─────────────────────────────────────────────────────
|
|
* The promise forms run the SAME sync operations and mint an already-
|
|
* settled promise (scr_promise_settled_*): success fulfills, a pending
|
|
* exception moves in as the rejection — catchable at the await, like
|
|
* Node. DOCUMENTED DIVERGENCE (SEMANTICS.md): the syscall blocks the
|
|
* event loop, so I/O never interleaves with timers or other fibers —
|
|
* observable only in concurrent code, not in the sequential await
|
|
* chains CLIs are made of. */
|
|
|
|
ScrPromise *scr_fsp_read_file(ScrStr *path) {
|
|
return scr_promise_settled_str(scr_fs_read_file(path));
|
|
}
|
|
|
|
ScrPromise *scr_fsp_write_file(ScrStr *path, ScrStr *data) {
|
|
scr_fs_write_file(path, data);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_write_file_mode(ScrStr *path, ScrStr *data, double mode) {
|
|
scr_fs_write_file_mode(path, data, mode);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_mkdir(ScrStr *path) {
|
|
scr_fs_mkdir(path);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_mkdir_mode(ScrStr *path, double mode) {
|
|
scr_fs_mkdir_mode(path, mode);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_mkdir_recursive(ScrStr *path) {
|
|
scr_fs_mkdir_recursive(path);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_mkdir_recursive_mode(ScrStr *path, double mode) {
|
|
scr_fs_mkdir_recursive_mode(path, mode);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_unlink(ScrStr *path) {
|
|
scr_fs_unlink(path);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_chmod(ScrStr *path, double mode) {
|
|
scr_fs_chmod(path, mode);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_chown(ScrStr *path, double uid, double gid) {
|
|
scr_fs_chown(path, uid, gid);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_lchown(ScrStr *path, double uid, double gid) {
|
|
scr_fs_lchown(path, uid, gid);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_access(ScrStr *path, double mode) {
|
|
scr_fs_access(path, mode);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_append_file(ScrStr *path, ScrStr *data) {
|
|
scr_fs_append_file(path, data);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_append_file_bytes(ScrStr *path, ScrBytes *data) {
|
|
scr_fs_append_file_bytes(path, data);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_copyfile(ScrStr *src, ScrStr *dest) {
|
|
scr_fs_copyfile(src, dest);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
/* readFile(path, encoding | { encoding }): the encoding is checked first,
|
|
* so an unknown name rejects before the read. */
|
|
ScrPromise *scr_fsp_read_file_encoded(ScrStr *path, ScrStr *encoding) {
|
|
return scr_promise_settled_str(scr_fs_read_file_encoded(path, encoding));
|
|
}
|
|
|
|
ScrPromise *scr_fsp_mkdtemp(ScrStr *prefix) {
|
|
return scr_promise_settled_str(scr_fs_mkdtemp_async(prefix));
|
|
}
|
|
|
|
ScrPromise *scr_fsp_rmdir(ScrStr *path) {
|
|
scr_fs_rmdir(path);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_truncate(ScrStr *path, double length) {
|
|
scr_fs_truncate(path, length);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_utimes(const ScrDyn *path, const ScrDyn *atime, const ScrDyn *mtime) {
|
|
scr_fs_utimes_checked(path, atime, mtime);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_link(const ScrDyn *existing, const ScrDyn *destination) {
|
|
scr_fs_link_checked(existing, destination);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_symlink(const ScrDyn *target, const ScrDyn *destination, const ScrDyn *type) {
|
|
scr_fs_symlink_promise_checked(target, destination, type);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_readlink_dyn(const ScrDyn *path, const ScrDyn *options) {
|
|
ScrDyn *result = scr_fs_readlink_checked(path, options, true);
|
|
return scr_promise_settled_ref(result, scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
|
|
}
|
|
|
|
ScrPromise *scr_fsp_statfs(const ScrDyn *path, const ScrDyn *options) {
|
|
ScrDyn *result = scr_fs_statfs_checked(path, options);
|
|
return scr_promise_settled_ref(result, scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
|
|
}
|
|
|
|
ScrPromise *scr_fsp_readlink_str(const ScrDyn *path, const ScrDyn *options) {
|
|
ScrDyn *result = scr_fs_readlink_checked(path, options, true);
|
|
ScrStr *text = result && result->kind == SCR_DYN_STR ? scr_str_retain(result->v.str) : NULL;
|
|
scr_dyn_release(result);
|
|
return scr_promise_settled_str(text);
|
|
}
|
|
|
|
ScrPromise *scr_fsp_readlink_buffer(const ScrDyn *path, const ScrDyn *options) {
|
|
ScrDyn *result = scr_fs_readlink_checked(path, options, true);
|
|
ScrBytes *bytes = result ? scr_dyn_bytes_unbox(result) : NULL;
|
|
scr_dyn_release(result);
|
|
return scr_promise_settled_ref(bytes, scr_bytes_retain_v, scr_bytes_release_v, NULL);
|
|
}
|
|
|
|
ScrPromise *scr_fsp_lutimes(const ScrDyn *path, const ScrDyn *atime, const ScrDyn *mtime) {
|
|
scr_fs_lutimes_checked(path, atime, mtime);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_readdir(ScrStr *path) {
|
|
ScrArr *names = scr_fs_readdir(path);
|
|
return scr_promise_settled_ref(names, &scr_arr_retain_v, &scr_arr_release_v, NULL);
|
|
}
|
|
|
|
ScrPromise *scr_fsp_rm(ScrStr *path) {
|
|
scr_fs_rm(path);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_rm_opts(ScrStr *path, bool recursive, bool force) {
|
|
scr_fs_rm_opts(path, recursive, force);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
/* fs.promises.rm validates the path before its options, unlike rmSync. */
|
|
ScrPromise *scr_fsp_rm_opts_retry(ScrStr *path, bool recursive, bool force, double max_retries,
|
|
double retry_delay) {
|
|
if (scr_fs_path_ok(path, "path") && scr_fs_rm_retry_options_ok(max_retries, retry_delay))
|
|
scr_fs_rm_opts_retry(path, recursive, force, max_retries, retry_delay);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_stat(ScrStr *path) {
|
|
ScrStats *st = scr_fs_stat(path);
|
|
return scr_promise_settled_ref(st, &scr_stats_retain_v, &scr_stats_release_v, NULL);
|
|
}
|
|
|
|
ScrPromise *scr_fsp_realpath(ScrStr *path) {
|
|
return scr_promise_settled_str(scr_fs_realpath_promise(path));
|
|
}
|
|
|
|
ScrPromise *scr_fsp_lstat(ScrStr *path) {
|
|
ScrStats *st = scr_fs_lstat(path);
|
|
return scr_promise_settled_ref(st, &scr_stats_retain_v, &scr_stats_release_v, NULL);
|
|
}
|
|
|
|
ScrPromise *scr_fsp_opendir(ScrStr *path, double buffer_size) {
|
|
ScrDir *d = scr_fs_opendir(path, buffer_size);
|
|
return scr_promise_settled_ref(d, &scr_dir_retain_v, &scr_dir_release_v, NULL);
|
|
}
|
|
|
|
/* dir.close(): a closed Dir rejects with ERR_DIR_CLOSED. */
|
|
ScrPromise *scr_dir_close_promise(ScrDir *d) {
|
|
scr_dir_close(d);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
ScrPromise *scr_fsp_rename(ScrStr *oldpath, ScrStr *newpath) {
|
|
scr_fs_rename(oldpath, newpath);
|
|
return scr_promise_settled_void();
|
|
}
|
|
|
|
/* Shared native work queue, initially introduced by fs.rename and also used
|
|
* by node:zlib's callback codecs. Work starts immediately on a native worker,
|
|
* matching libuv's key contract while JS/main is synchronously occupied.
|
|
* Error/Buffer construction, callback invocation, and every RC mutation stay
|
|
* on the main runtime thread when a later loop turn observes completion.
|
|
*
|
|
* A compact operation queue is also the liveness handle: an outstanding
|
|
* callback-style request keeps the loop alive. Four persistent workers mirror
|
|
* libuv's default pool size without creating one OS thread per request. A
|
|
* platform mutex publishes each result before the loop removes it from the
|
|
* completion queue. */
|
|
#if !defined(SCR_LIB) && !defined(__wasi__)
|
|
/* The completion wake: a worker signals it once per published completion,
|
|
* so the loop's sleep ends for the completion — a pipe the poll(2) sleep
|
|
* watches on POSIX, an auto-reset event the Windows sleep waits on. */
|
|
typedef struct ScrFsRenameWake {
|
|
#ifdef _WIN32
|
|
HANDLE event; /* NULL until created */
|
|
#else
|
|
int fds[2]; /* -1 until created */
|
|
#endif
|
|
} ScrFsRenameWake;
|
|
#ifdef _WIN32
|
|
#define SCR_FS_RENAME_WAKE_NONE {NULL}
|
|
#else
|
|
#define SCR_FS_RENAME_WAKE_NONE {{-1, -1}}
|
|
#endif
|
|
#ifdef SCR_WORKERS
|
|
typedef struct ScrFsRenameOwner ScrFsRenameOwner;
|
|
#endif
|
|
typedef struct ScrFsRenameOp {
|
|
bool generic;
|
|
bool ran;
|
|
void *payload;
|
|
ScrWorkFn work;
|
|
ScrWorkFn after;
|
|
ScrWorkFn destroy;
|
|
ScrStr *oldpath;
|
|
ScrStr *newpath;
|
|
ScrClosure *cb;
|
|
ScrFsRenameFn fn;
|
|
int error;
|
|
struct ScrFsRenameOp *next;
|
|
#ifdef SCR_WORKERS
|
|
ScrFsRenameOwner *owner;
|
|
#endif
|
|
} ScrFsRenameOp;
|
|
|
|
static ScrFsRenameOp *scr_fs_rename_work = NULL;
|
|
static ScrFsRenameOp **scr_fs_rename_work_tail = &scr_fs_rename_work;
|
|
#ifdef SCR_WORKERS
|
|
struct ScrFsRenameOwner {
|
|
ScrFsRenameOp *done;
|
|
ScrFsRenameOp **tail;
|
|
size_t pending;
|
|
ScrFsRenameWake wake;
|
|
};
|
|
static SCR_TL ScrFsRenameOwner *scr_fs_rename_owner;
|
|
#define scr_fs_rename_done (scr_fs_rename_owner->done)
|
|
#define scr_fs_rename_done_tail (scr_fs_rename_owner->tail)
|
|
#define scr_fs_rename_pending_count (scr_fs_rename_owner->pending)
|
|
#else
|
|
static ScrFsRenameOp *scr_fs_rename_done = NULL;
|
|
static ScrFsRenameOp **scr_fs_rename_done_tail = &scr_fs_rename_done;
|
|
static size_t scr_fs_rename_pending_count = 0;
|
|
static ScrFsRenameWake scr_fs_rename_wake_global = SCR_FS_RENAME_WAKE_NONE;
|
|
#endif
|
|
static size_t scr_fs_rename_worker_count = 0;
|
|
static bool scr_fs_rename_stopping = false;
|
|
static bool scr_fs_rename_shutdown_registered = false;
|
|
|
|
#ifdef _WIN32
|
|
static HANDLE scr_fs_rename_workers[4];
|
|
static SRWLOCK scr_fs_rename_lock = SRWLOCK_INIT;
|
|
static CONDITION_VARIABLE scr_fs_rename_cond = CONDITION_VARIABLE_INIT;
|
|
static void scr_fs_rename_lock_enter(void) { AcquireSRWLockExclusive(&scr_fs_rename_lock); }
|
|
static void scr_fs_rename_lock_leave(void) { ReleaseSRWLockExclusive(&scr_fs_rename_lock); }
|
|
static void scr_fs_rename_wait(void) {
|
|
(void)SleepConditionVariableSRW(&scr_fs_rename_cond, &scr_fs_rename_lock, INFINITE, 0);
|
|
}
|
|
static void scr_fs_rename_signal(void) { WakeConditionVariable(&scr_fs_rename_cond); }
|
|
static void scr_fs_rename_broadcast(void) { WakeAllConditionVariable(&scr_fs_rename_cond); }
|
|
#else
|
|
static pthread_t scr_fs_rename_workers[4];
|
|
static pthread_mutex_t scr_fs_rename_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
static pthread_cond_t scr_fs_rename_cond = PTHREAD_COND_INITIALIZER;
|
|
static void scr_fs_rename_lock_enter(void) { (void)pthread_mutex_lock(&scr_fs_rename_lock); }
|
|
static void scr_fs_rename_lock_leave(void) { (void)pthread_mutex_unlock(&scr_fs_rename_lock); }
|
|
static void scr_fs_rename_wait(void) {
|
|
(void)pthread_cond_wait(&scr_fs_rename_cond, &scr_fs_rename_lock);
|
|
}
|
|
static void scr_fs_rename_signal(void) { (void)pthread_cond_signal(&scr_fs_rename_cond); }
|
|
static void scr_fs_rename_broadcast(void) { (void)pthread_cond_broadcast(&scr_fs_rename_cond); }
|
|
#endif
|
|
|
|
#ifdef SCR_WORKERS
|
|
#define SCR_FS_RENAME_WAKE(owner) (&(owner)->wake)
|
|
#else
|
|
#define SCR_FS_RENAME_WAKE(owner) (&scr_fs_rename_wake_global)
|
|
#endif
|
|
|
|
/* Creation failure leaves the wake unset; the loop then bounds its sleep
|
|
* at the child-reap cap, as it did before the wake existed. */
|
|
static void scr_fs_rename_wake_open(ScrFsRenameWake *wake) {
|
|
#ifdef _WIN32
|
|
if (wake->event == NULL) wake->event = CreateEventW(NULL, FALSE, FALSE, NULL);
|
|
#else
|
|
if (wake->fds[0] >= 0) return;
|
|
int fds[2];
|
|
if (pipe(fds) != 0) return;
|
|
for (int i = 0; i < 2; i++) {
|
|
(void)fcntl(fds[i], F_SETFD, FD_CLOEXEC);
|
|
(void)fcntl(fds[i], F_SETFL, fcntl(fds[i], F_GETFL) | O_NONBLOCK);
|
|
}
|
|
wake->fds[0] = fds[0];
|
|
wake->fds[1] = fds[1];
|
|
#endif
|
|
}
|
|
|
|
static void scr_fs_rename_wake_post(ScrFsRenameWake *wake) {
|
|
#ifdef _WIN32
|
|
if (wake->event != NULL) (void)SetEvent(wake->event);
|
|
#else
|
|
if (wake->fds[1] >= 0) {
|
|
char byte = 1;
|
|
ssize_t n = write(wake->fds[1], &byte, 1); /* a full pipe already wakes the loop */
|
|
(void)n;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/* POSIX: empty the pipe before taking the queue, so a completion published
|
|
* afterwards leaves its byte for the next poll. The Windows event resets
|
|
* when a wait consumes it. */
|
|
static void scr_fs_rename_wake_drain(ScrFsRenameWake *wake) {
|
|
#ifdef _WIN32
|
|
(void)wake;
|
|
#else
|
|
if (wake->fds[0] < 0) return;
|
|
char buf[64];
|
|
while (read(wake->fds[0], buf, sizeof buf) > 0) {
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#ifdef SCR_WORKERS
|
|
static void scr_fs_rename_wake_close(ScrFsRenameWake *wake) {
|
|
#ifdef _WIN32
|
|
if (wake->event != NULL) CloseHandle(wake->event);
|
|
wake->event = NULL;
|
|
#else
|
|
for (int i = 0; i < 2; i++) {
|
|
if (wake->fds[i] >= 0) close(wake->fds[i]);
|
|
wake->fds[i] = -1;
|
|
}
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
static void scr_fs_rename_op_release(ScrFsRenameOp *op) {
|
|
if (op->generic) {
|
|
op->destroy(op->payload);
|
|
free(op);
|
|
return;
|
|
}
|
|
scr_str_release(op->oldpath);
|
|
scr_str_release(op->newpath);
|
|
scr_closure_release(op->cb);
|
|
free(op);
|
|
}
|
|
|
|
#ifdef SCR_WORKERS
|
|
static void scr_fs_rename_context_cleanup(void) {
|
|
ScrFsRenameOwner *owner = scr_fs_rename_owner;
|
|
/* Outstanding native work may still borrow payload storage. Wait for its
|
|
* publication before reclaiming it, without invoking script callbacks
|
|
* during context teardown. Other script contexts keep using the pool. */
|
|
while (owner->pending) {
|
|
scr_fs_rename_lock_enter();
|
|
while (!owner->done) scr_fs_rename_wait();
|
|
ScrFsRenameOp *op = owner->done;
|
|
owner->done = op->next;
|
|
if (!owner->done) owner->tail = &owner->done;
|
|
scr_fs_rename_lock_leave();
|
|
owner->pending--;
|
|
scr_fs_rename_op_release(op);
|
|
}
|
|
scr_fs_rename_owner = NULL;
|
|
scr_fs_rename_wake_close(&owner->wake);
|
|
free(owner);
|
|
}
|
|
|
|
static ScrFsRenameOwner *scr_fs_rename_context(void) {
|
|
if (!scr_fs_rename_owner) {
|
|
ScrFsRenameOwner *owner = calloc(1, sizeof(*owner));
|
|
if (!owner) scr_oom();
|
|
owner->tail = &owner->done;
|
|
owner->wake = (ScrFsRenameWake)SCR_FS_RENAME_WAKE_NONE;
|
|
if (scr_atexit(scr_fs_rename_context_cleanup) != 0) {
|
|
free(owner);
|
|
scr_trap("scriptc: could not register work context cleanup\n");
|
|
}
|
|
scr_fs_rename_owner = owner;
|
|
}
|
|
return scr_fs_rename_owner;
|
|
}
|
|
#endif
|
|
|
|
static void scr_fs_rename_worker_loop(void) {
|
|
for (;;) {
|
|
scr_fs_rename_lock_enter();
|
|
while (scr_fs_rename_work == NULL && !scr_fs_rename_stopping) scr_fs_rename_wait();
|
|
if (scr_fs_rename_stopping) {
|
|
scr_fs_rename_lock_leave();
|
|
return;
|
|
}
|
|
ScrFsRenameOp *op = scr_fs_rename_work;
|
|
scr_fs_rename_work = op->next;
|
|
if (scr_fs_rename_work == NULL) scr_fs_rename_work_tail = &scr_fs_rename_work;
|
|
scr_fs_rename_lock_leave();
|
|
|
|
if (op->generic) op->work(op->payload);
|
|
else op->error = scr_fs_rename_raw(op->oldpath, op->newpath);
|
|
op->ran = true;
|
|
|
|
scr_fs_rename_lock_enter();
|
|
op->next = NULL;
|
|
#ifdef SCR_WORKERS
|
|
*op->owner->tail = op;
|
|
op->owner->tail = &op->next;
|
|
scr_fs_rename_broadcast();
|
|
scr_fs_rename_wake_post(&op->owner->wake);
|
|
#else
|
|
*scr_fs_rename_done_tail = op;
|
|
scr_fs_rename_done_tail = &op->next;
|
|
scr_fs_rename_wake_post(&scr_fs_rename_wake_global);
|
|
#endif
|
|
scr_fs_rename_lock_leave();
|
|
}
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
static DWORD WINAPI scr_fs_rename_worker(LPVOID payload) {
|
|
(void)payload;
|
|
scr_fs_rename_worker_loop();
|
|
return 0;
|
|
}
|
|
#else
|
|
static void *scr_fs_rename_worker(void *payload) {
|
|
(void)payload;
|
|
scr_fs_rename_worker_loop();
|
|
return NULL;
|
|
}
|
|
#endif
|
|
|
|
/* Runs before the ordinary runtime atexit cleanup/audit (it is registered
|
|
* lazily by the first rename request, after scr_init/scr_lib_init). Fatal
|
|
* top-level or callback exceptions can stop the loop with requests still in
|
|
* flight; join the workers before releasing their retained path/callback
|
|
* payloads so sanitized builds keep the program's real exit verdict. */
|
|
static void scr_fs_renames_shutdown(void) {
|
|
scr_fs_rename_lock_enter();
|
|
scr_fs_rename_stopping = true;
|
|
scr_fs_rename_broadcast();
|
|
scr_fs_rename_lock_leave();
|
|
|
|
for (size_t i = 0; i < scr_fs_rename_worker_count; i++) {
|
|
#ifdef _WIN32
|
|
(void)WaitForSingleObject(scr_fs_rename_workers[i], INFINITE);
|
|
CloseHandle(scr_fs_rename_workers[i]);
|
|
#else
|
|
(void)pthread_join(scr_fs_rename_workers[i], NULL);
|
|
#endif
|
|
}
|
|
|
|
scr_fs_rename_lock_enter();
|
|
#ifdef SCR_WORKERS
|
|
/* Every context drains its in-flight work before it exits. This final
|
|
* process hook owns only the pool threads and cannot touch runtime heaps. */
|
|
if (scr_fs_rename_work) {
|
|
scr_fs_rename_lock_leave();
|
|
scr_trap("scriptc: work queue survived context teardown\n");
|
|
}
|
|
scr_fs_rename_lock_leave();
|
|
#else
|
|
ScrFsRenameOp *work = scr_fs_rename_work;
|
|
ScrFsRenameOp *done = scr_fs_rename_done;
|
|
scr_fs_rename_work = NULL;
|
|
scr_fs_rename_work_tail = &scr_fs_rename_work;
|
|
scr_fs_rename_done = NULL;
|
|
scr_fs_rename_done_tail = &scr_fs_rename_done;
|
|
scr_fs_rename_pending_count = 0;
|
|
scr_fs_rename_lock_leave();
|
|
while (work != NULL) {
|
|
ScrFsRenameOp *next = work->next;
|
|
scr_fs_rename_op_release(work);
|
|
work = next;
|
|
}
|
|
while (done != NULL) {
|
|
ScrFsRenameOp *next = done->next;
|
|
scr_fs_rename_op_release(done);
|
|
done = next;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static bool scr_fs_renames_pending(void) {
|
|
#ifdef SCR_WORKERS
|
|
if (!scr_fs_rename_owner) return false;
|
|
#endif
|
|
return scr_fs_rename_pending_count != 0;
|
|
}
|
|
|
|
/* POSIX: the loop polls this fd while completions are pending (-1: none). */
|
|
static int scr_fs_renames_pollfd(void) {
|
|
#if defined(_WIN32)
|
|
return -1;
|
|
#else
|
|
#ifdef SCR_WORKERS
|
|
if (!scr_fs_rename_owner) return -1;
|
|
#endif
|
|
return SCR_FS_RENAME_WAKE(scr_fs_rename_owner)->fds[0];
|
|
#endif
|
|
}
|
|
|
|
/* Windows: the loop's sleep waits on the event (false: no event). */
|
|
static bool scr_fs_renames_can_wait(void) {
|
|
#if defined(_WIN32)
|
|
#ifdef SCR_WORKERS
|
|
if (!scr_fs_rename_owner) return false;
|
|
#endif
|
|
return SCR_FS_RENAME_WAKE(scr_fs_rename_owner)->event != NULL;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
static void scr_fs_renames_wait(double ms) {
|
|
DWORD timeout = ms >= 4294967294.0 ? 4294967294u : (DWORD)(ms + 0.999);
|
|
(void)WaitForSingleObject(SCR_FS_RENAME_WAKE(scr_fs_rename_owner)->event, timeout);
|
|
}
|
|
#endif
|
|
|
|
static bool scr_fs_renames_dispatch(void) {
|
|
#ifdef SCR_WORKERS
|
|
if (!scr_fs_rename_owner) return false;
|
|
#endif
|
|
scr_fs_rename_wake_drain(SCR_FS_RENAME_WAKE(scr_fs_rename_owner));
|
|
scr_fs_rename_lock_enter();
|
|
ScrFsRenameOp *op = scr_fs_rename_done;
|
|
if (op != NULL) {
|
|
scr_fs_rename_done = op->next;
|
|
if (scr_fs_rename_done == NULL) scr_fs_rename_done_tail = &scr_fs_rename_done;
|
|
}
|
|
scr_fs_rename_lock_leave();
|
|
if (op == NULL) return false;
|
|
scr_fs_rename_pending_count--;
|
|
if (op->generic) {
|
|
if (!op->ran) op->work(op->payload);
|
|
op->after(op->payload);
|
|
scr_fs_rename_op_release(op);
|
|
return true;
|
|
}
|
|
ScrCaught *caught = NULL;
|
|
ScrError *err = NULL;
|
|
if (op->error != 0) {
|
|
scr_fs_rename_error(op->error, op->oldpath, op->newpath);
|
|
caught = scr_exc_take();
|
|
if (caught && caught->kind == SCR_EXC_OBJ && scr_error_is(caught->payload)) {
|
|
err = (ScrError *)caught->payload;
|
|
}
|
|
}
|
|
op->fn(op->cb, err);
|
|
scr_caught_release(caught);
|
|
scr_fs_rename_op_release(op);
|
|
return true;
|
|
}
|
|
|
|
static void scr_work_submit_op(ScrFsRenameOp *op) {
|
|
#ifdef SCR_WORKERS
|
|
op->owner = scr_fs_rename_context();
|
|
scr_fs_rename_wake_open(&op->owner->wake);
|
|
scr_fs_rename_pending_count++;
|
|
scr_fs_rename_lock_enter();
|
|
#else
|
|
scr_fs_rename_wake_open(&scr_fs_rename_wake_global);
|
|
#endif
|
|
if (!scr_fs_rename_shutdown_registered) {
|
|
if (atexit(scr_fs_renames_shutdown) != 0) {
|
|
scr_trap("scriptc: could not register worker cleanup\n");
|
|
}
|
|
scr_fs_rename_shutdown_registered = true;
|
|
}
|
|
#ifndef SCR_WORKERS
|
|
scr_fs_rename_pending_count++;
|
|
scr_fs_rename_lock_enter();
|
|
#endif
|
|
*scr_fs_rename_work_tail = op;
|
|
scr_fs_rename_work_tail = &op->next;
|
|
int create_error = 0;
|
|
if (scr_fs_rename_worker_count < 4) {
|
|
#ifdef _WIN32
|
|
HANDLE worker = CreateThread(NULL, 0, scr_fs_rename_worker, NULL, 0, NULL);
|
|
if (worker != NULL) {
|
|
scr_fs_rename_workers[scr_fs_rename_worker_count] = worker;
|
|
scr_fs_rename_worker_count++;
|
|
} else {
|
|
create_error = EAGAIN;
|
|
}
|
|
#else
|
|
pthread_t worker;
|
|
create_error = pthread_create(&worker, NULL, scr_fs_rename_worker, NULL);
|
|
if (create_error == 0) {
|
|
scr_fs_rename_workers[scr_fs_rename_worker_count] = worker;
|
|
scr_fs_rename_worker_count++;
|
|
}
|
|
#endif
|
|
}
|
|
if (create_error != 0 && scr_fs_rename_worker_count == 0) {
|
|
/* Submission failure remains asynchronous. Rename preserves its
|
|
* resource error; generic work falls back to the main loop turn. */
|
|
while (scr_fs_rename_work != NULL) {
|
|
ScrFsRenameOp *failed = scr_fs_rename_work;
|
|
scr_fs_rename_work = failed->next;
|
|
failed->error = create_error;
|
|
failed->ran = !failed->generic;
|
|
failed->next = NULL;
|
|
#ifdef SCR_WORKERS
|
|
*failed->owner->tail = failed;
|
|
failed->owner->tail = &failed->next;
|
|
scr_fs_rename_broadcast();
|
|
scr_fs_rename_wake_post(&failed->owner->wake);
|
|
#else
|
|
*scr_fs_rename_done_tail = failed;
|
|
scr_fs_rename_done_tail = &failed->next;
|
|
scr_fs_rename_wake_post(&scr_fs_rename_wake_global);
|
|
#endif
|
|
}
|
|
scr_fs_rename_work_tail = &scr_fs_rename_work;
|
|
} else {
|
|
scr_fs_rename_signal();
|
|
}
|
|
scr_fs_rename_lock_leave();
|
|
}
|
|
#elif defined(__wasi__)
|
|
/* WASI Preview 1 has no threads, but rename itself is available. Queue the
|
|
* request and perform it on the next loop turn so callback timing remains
|
|
* asynchronous and the request remains a liveness handle. */
|
|
typedef struct ScrFsRenameOp {
|
|
bool generic;
|
|
void *payload;
|
|
ScrWorkFn work;
|
|
ScrWorkFn after;
|
|
ScrWorkFn destroy;
|
|
ScrStr *oldpath;
|
|
ScrStr *newpath;
|
|
ScrClosure *cb;
|
|
ScrFsRenameFn fn;
|
|
struct ScrFsRenameOp *next;
|
|
} ScrFsRenameOp;
|
|
|
|
static ScrFsRenameOp *scr_fs_rename_done = NULL;
|
|
static ScrFsRenameOp **scr_fs_rename_done_tail = &scr_fs_rename_done;
|
|
static size_t scr_fs_rename_pending_count = 0;
|
|
|
|
static bool scr_fs_renames_pending(void) { return scr_fs_rename_pending_count != 0; }
|
|
static int scr_fs_renames_pollfd(void) { return -1; }
|
|
static bool scr_fs_renames_can_wait(void) { return false; }
|
|
|
|
static bool scr_fs_renames_dispatch(void) {
|
|
ScrFsRenameOp *op = scr_fs_rename_done;
|
|
if (op == NULL) return false;
|
|
scr_fs_rename_done = op->next;
|
|
if (scr_fs_rename_done == NULL) scr_fs_rename_done_tail = &scr_fs_rename_done;
|
|
scr_fs_rename_pending_count--;
|
|
|
|
if (op->generic) {
|
|
op->work(op->payload);
|
|
op->after(op->payload);
|
|
op->destroy(op->payload);
|
|
free(op);
|
|
return true;
|
|
}
|
|
|
|
int error = scr_fs_rename_raw(op->oldpath, op->newpath);
|
|
ScrCaught *caught = NULL;
|
|
ScrError *err = NULL;
|
|
if (error != 0) {
|
|
scr_fs_rename_error(error, op->oldpath, op->newpath);
|
|
caught = scr_exc_take();
|
|
if (caught && caught->kind == SCR_EXC_OBJ && scr_error_is(caught->payload)) {
|
|
err = (ScrError *)caught->payload;
|
|
}
|
|
}
|
|
op->fn(op->cb, err);
|
|
scr_caught_release(caught);
|
|
scr_str_release(op->oldpath);
|
|
scr_str_release(op->newpath);
|
|
scr_closure_release(op->cb);
|
|
free(op);
|
|
return true;
|
|
}
|
|
#else
|
|
static bool scr_fs_renames_pending(void) { return false; }
|
|
static bool scr_fs_renames_dispatch(void) { return false; }
|
|
static int scr_fs_renames_pollfd(void) { return -1; }
|
|
static bool scr_fs_renames_can_wait(void) { return false; }
|
|
#endif
|
|
|
|
void scr_fs_rename_async(ScrStr *oldpath, ScrStr *newpath,
|
|
ScrClosure *cb, ScrFsRenameFn fn) {
|
|
/* Node validates both paths before queueing the rename: a null byte
|
|
* throws here and the callback never runs. */
|
|
if (!scr_fs_path_ok(oldpath, "oldPath") || !scr_fs_path_ok(newpath, "newPath")) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
#if defined(SCR_LIB)
|
|
(void)oldpath; (void)newpath; (void)fn;
|
|
scr_closure_release(cb);
|
|
scr_trap("scriptc: callback-style fs.rename is not supported by this target\n");
|
|
#elif defined(__wasi__)
|
|
ScrFsRenameOp *op = calloc(1, sizeof *op);
|
|
if (!op) scr_trap("scriptc: out of memory\n");
|
|
op->oldpath = scr_str_retain(oldpath);
|
|
op->newpath = scr_str_retain(newpath);
|
|
op->cb = cb;
|
|
op->fn = fn;
|
|
*scr_fs_rename_done_tail = op;
|
|
scr_fs_rename_done_tail = &op->next;
|
|
scr_fs_rename_pending_count++;
|
|
#else
|
|
ScrFsRenameOp *op = calloc(1, sizeof *op);
|
|
if (!op) scr_trap("scriptc: out of memory\n");
|
|
op->oldpath = scr_str_retain(oldpath);
|
|
op->newpath = scr_str_retain(newpath);
|
|
op->cb = cb;
|
|
op->fn = fn;
|
|
scr_work_submit_op(op);
|
|
#endif
|
|
}
|
|
|
|
void scr_work_submit(void *payload, ScrWorkFn work, ScrWorkFn after,
|
|
ScrWorkFn destroy) {
|
|
#if defined(SCR_LIB)
|
|
(void)work; (void)after;
|
|
destroy(payload);
|
|
scr_trap("scriptc: native worker jobs are not supported by this target\n");
|
|
#elif defined(__wasi__)
|
|
ScrFsRenameOp *op = calloc(1, sizeof *op);
|
|
if (!op) scr_trap("scriptc: out of memory\n");
|
|
op->generic = true;
|
|
op->payload = payload;
|
|
op->work = work;
|
|
op->after = after;
|
|
op->destroy = destroy;
|
|
*scr_fs_rename_done_tail = op;
|
|
scr_fs_rename_done_tail = &op->next;
|
|
scr_fs_rename_pending_count++;
|
|
#else
|
|
ScrFsRenameOp *op = calloc(1, sizeof *op);
|
|
if (!op) scr_trap("scriptc: out of memory\n");
|
|
op->generic = true;
|
|
op->payload = payload;
|
|
op->work = work;
|
|
op->after = after;
|
|
op->destroy = destroy;
|
|
scr_work_submit_op(op);
|
|
#endif
|
|
}
|
|
|
|
void scr_fs_rename_thunk0(ScrClosure *cb, ScrError *err) {
|
|
(void)err;
|
|
((void (*)(ScrClosure *))cb->fn)(cb);
|
|
}
|
|
|
|
/* Callback-style crypto completions. The operation validates and computes
|
|
* before enqueueing; delivery is a later macrotask, never synchronous with
|
|
* the API call. The queue owns both references and transfers the Buffer to
|
|
* the emitted callback adapter. */
|
|
typedef struct ScrCryptoBytesOp {
|
|
ScrBytes *value; /* a ScrDyn when value_is_dyn */
|
|
bool value_is_dyn;
|
|
ScrError *error;
|
|
ScrClosure *cb;
|
|
ScrCryptoBytesFn fn;
|
|
struct ScrCryptoBytesOp *next;
|
|
} ScrCryptoBytesOp;
|
|
|
|
static SCR_TL ScrCryptoBytesOp *scr_crypto_bytes_head = NULL;
|
|
#ifdef SCR_WORKERS
|
|
static SCR_TL ScrCryptoBytesOp **scr_crypto_bytes_tail;
|
|
#else
|
|
static SCR_TL ScrCryptoBytesOp **scr_crypto_bytes_tail = &scr_crypto_bytes_head;
|
|
#endif
|
|
static SCR_TL size_t scr_crypto_bytes_pending_count = 0;
|
|
static SCR_TL ScrCryptoBytesOp *scr_crypto_bytes_last = NULL; /* the newest entry */
|
|
static SCR_TL bool scr_crypto_bytes_cleanup_registered = false;
|
|
|
|
static void scr_crypto_bytes_shutdown(void) {
|
|
while (scr_crypto_bytes_head != NULL) {
|
|
ScrCryptoBytesOp *op = scr_crypto_bytes_head;
|
|
scr_crypto_bytes_head = op->next;
|
|
if (op->value_is_dyn) {
|
|
if (op->value) scr_dyn_release((ScrDyn *)op->value);
|
|
} else {
|
|
scr_bytes_release(op->value);
|
|
}
|
|
scr_error_release(op->error);
|
|
scr_closure_release(op->cb);
|
|
free(op);
|
|
}
|
|
scr_crypto_bytes_tail = &scr_crypto_bytes_head;
|
|
scr_crypto_bytes_pending_count = 0;
|
|
}
|
|
|
|
static bool scr_crypto_bytes_pending(void) {
|
|
return scr_crypto_bytes_pending_count != 0;
|
|
}
|
|
|
|
static bool scr_crypto_bytes_dispatch(void) {
|
|
ScrCryptoBytesOp *op = scr_crypto_bytes_head;
|
|
if (op == NULL) return false;
|
|
scr_crypto_bytes_head = op->next;
|
|
if (scr_crypto_bytes_head == NULL) scr_crypto_bytes_tail = &scr_crypto_bytes_head;
|
|
scr_crypto_bytes_pending_count--;
|
|
op->fn(op->cb, op->error, op->value); /* adapter consumes value */
|
|
scr_error_release(op->error);
|
|
scr_closure_release(op->cb);
|
|
free(op);
|
|
return true;
|
|
}
|
|
|
|
void scr_crypto_defer_bytes(ScrBytes *value, ScrError *error, ScrClosure *cb, ScrCryptoBytesFn fn) {
|
|
if (!scr_crypto_bytes_cleanup_registered) {
|
|
if (scr_atexit(scr_crypto_bytes_shutdown) != 0) {
|
|
scr_bytes_release(value);
|
|
scr_error_release(error);
|
|
scr_closure_release(cb);
|
|
scr_trap("scriptc: could not register crypto callback cleanup\n");
|
|
}
|
|
scr_crypto_bytes_cleanup_registered = true;
|
|
scr_crypto_bytes_tail = &scr_crypto_bytes_head;
|
|
}
|
|
ScrCryptoBytesOp *op = malloc(sizeof *op);
|
|
if (!op) {
|
|
scr_bytes_release(value);
|
|
scr_error_release(error);
|
|
scr_closure_release(cb);
|
|
scr_trap("scriptc: out of memory\n");
|
|
}
|
|
op->value = value;
|
|
op->value_is_dyn = false;
|
|
op->error = error;
|
|
op->cb = cb;
|
|
op->fn = fn;
|
|
op->next = NULL;
|
|
*scr_crypto_bytes_tail = op;
|
|
scr_crypto_bytes_tail = &op->next;
|
|
scr_crypto_bytes_last = op;
|
|
scr_crypto_bytes_pending_count++;
|
|
}
|
|
|
|
/* The same queue with a dyn value, which the callback adapter receives in
|
|
* the value slot unchanged (randomFill over an ArrayBuffer passes back the
|
|
* ArrayBuffer itself). */
|
|
void scr_crypto_defer_dyn(ScrDyn *value, ScrError *error, ScrClosure *cb, ScrCryptoBytesFn fn) {
|
|
scr_crypto_defer_bytes(NULL, error, cb, fn);
|
|
scr_crypto_bytes_last->value = (ScrBytes *)value;
|
|
scr_crypto_bytes_last->value_is_dyn = true;
|
|
}
|
|
|
|
/* Deferred native callbacks: the fs error-first callbacks (and fs.exists)
|
|
* do their work synchronously and deliver the callback on a later loop
|
|
* turn. The queue is FIFO and its callbacks are macrotasks, one per
|
|
* checkpoint like the worker-pool completions, so nextTicks and microtasks
|
|
* drain between them. Before this queue they rode zero-delay timers,
|
|
* whose 1 ms floor made every callback wait for a timer turn. */
|
|
typedef struct ScrLoopDeferred {
|
|
ScrClosure *cb;
|
|
struct ScrLoopDeferred *next;
|
|
} ScrLoopDeferred;
|
|
|
|
static SCR_TL ScrLoopDeferred *scr_loop_deferred_head = NULL;
|
|
static SCR_TL ScrLoopDeferred **scr_loop_deferred_tail = NULL;
|
|
static SCR_TL size_t scr_loop_deferred_count = 0;
|
|
/* Callbacks this turn may deliver: the queue as it stood when the turn's
|
|
* timers ran (libuv's poll phase runs the callbacks that were ready when
|
|
* it began), so a callback that queues another cannot starve timers and
|
|
* immediates. */
|
|
static SCR_TL size_t scr_loop_deferred_ready = 0;
|
|
static SCR_TL bool scr_loop_deferred_cleanup_registered = false;
|
|
|
|
static void scr_loop_deferred_shutdown(void) {
|
|
while (scr_loop_deferred_head != NULL) {
|
|
ScrLoopDeferred *d = scr_loop_deferred_head;
|
|
scr_loop_deferred_head = d->next;
|
|
scr_closure_release(d->cb);
|
|
free(d);
|
|
}
|
|
scr_loop_deferred_tail = &scr_loop_deferred_head;
|
|
scr_loop_deferred_count = 0;
|
|
scr_loop_deferred_ready = 0;
|
|
}
|
|
|
|
static bool scr_loop_deferred_pending(void) { return scr_loop_deferred_count != 0; }
|
|
|
|
static bool scr_loop_deferred_dispatch(void) {
|
|
ScrLoopDeferred *d = scr_loop_deferred_head;
|
|
if (d == NULL || scr_loop_deferred_ready == 0) return false;
|
|
scr_loop_deferred_ready--;
|
|
scr_loop_deferred_head = d->next;
|
|
if (scr_loop_deferred_head == NULL) scr_loop_deferred_tail = &scr_loop_deferred_head;
|
|
scr_loop_deferred_count--;
|
|
ScrClosure *cb = d->cb;
|
|
free(d);
|
|
((void (*)(ScrClosure *))cb->fn)(cb);
|
|
scr_closure_release(cb);
|
|
return true;
|
|
}
|
|
|
|
void scr_loop_defer(ScrClosure *cb) {
|
|
if (!scr_loop_deferred_cleanup_registered) {
|
|
if (scr_atexit(scr_loop_deferred_shutdown) != 0) {
|
|
scr_closure_release(cb);
|
|
scr_trap("scriptc: could not register deferred callback cleanup\n");
|
|
}
|
|
scr_loop_deferred_cleanup_registered = true;
|
|
scr_loop_deferred_tail = &scr_loop_deferred_head;
|
|
}
|
|
ScrLoopDeferred *d = malloc(sizeof *d);
|
|
if (!d) {
|
|
scr_closure_release(cb);
|
|
scr_trap("scriptc: out of memory\n");
|
|
}
|
|
d->cb = cb;
|
|
d->next = NULL;
|
|
*scr_loop_deferred_tail = d;
|
|
scr_loop_deferred_tail = &d->next;
|
|
scr_loop_deferred_count++;
|
|
}
|
|
|
|
/* ── node:timers/promises ────────────────────────────────────────────
|
|
* The promisified pair: a PENDING void promise a one-shot heap timer
|
|
* (setTimeout) or the immediate queue (setImmediate) fulfills — the
|
|
* resolve closure is scr_make_resolve's void thunk, so settlement wakes
|
|
* awaiting fibers exactly like every other promise, and the armed
|
|
* timer/immediate keeps the loop alive until it fires (Node: an awaited
|
|
* timers/promises sleep holds the process open). Neither throws; the
|
|
* delay rides scr_set_timeout's own Node-exact coercion (clamp to 1,
|
|
* truncate). */
|
|
|
|
ScrPromise *scr_tp_set_timeout(double ms) {
|
|
ScrPromise *p = scr_promise_new();
|
|
scr_set_timeout(scr_make_resolve(p, 3 /* void */), ms);
|
|
return p;
|
|
}
|
|
|
|
ScrPromise *scr_tp_set_immediate(void) {
|
|
ScrPromise *p = scr_promise_new();
|
|
scr_set_immediate(scr_make_resolve(p, 3 /* void */));
|
|
return p;
|
|
}
|
|
|
|
/* setTimeout(delay, value[, { ref }]) and setImmediate(value[, { ref }]):
|
|
* the value arrives as an already-settled promise of its own type (the
|
|
* Promise.resolve lowering, borrowed), whose settlement the result copies
|
|
* when the timer or immediate fires. `ref: false` unrefs it, so it no
|
|
* longer keeps the loop alive, as in Node. */
|
|
static void scr_tp_settle_thunk(ScrClosure *self) {
|
|
ScrPromise *dst = (ScrPromise *)scr_box_get_ref(self->caps[0]);
|
|
ScrPromise *src = (ScrPromise *)scr_box_get_ref(self->caps[1]);
|
|
scr_promise_settle_from(dst, src);
|
|
scr_promise_release(dst);
|
|
scr_promise_release(src);
|
|
}
|
|
|
|
static ScrClosure *scr_tp_settle_closure(ScrPromise *dst, ScrPromise *src) {
|
|
ScrClosure *c = scr_closure_new((void *)&scr_tp_settle_thunk, 2);
|
|
for (int i = 0; i < 2; i++) {
|
|
ScrBox *b = scr_box_new_obj(scr_promise_retain_v, scr_promise_release_v, scr_promise_trace_v);
|
|
scr_box_set_ref(b, scr_promise_retain(i == 0 ? dst : src));
|
|
c->caps[i] = b;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
ScrPromise *scr_tp_set_timeout_value(double ms, ScrPromise *value, bool ref) {
|
|
ScrPromise *p = scr_promise_new();
|
|
double handle = scr_set_timeout_handle(scr_tp_settle_closure(p, value), ms);
|
|
if (!ref) scr_timer_unref(handle);
|
|
return p;
|
|
}
|
|
|
|
ScrPromise *scr_tp_set_immediate_value(ScrPromise *value, bool ref) {
|
|
ScrPromise *p = scr_promise_new();
|
|
double handle = scr_set_immediate(scr_tp_settle_closure(p, value));
|
|
if (!ref) scr_immediate_unref(handle);
|
|
return p;
|
|
}
|
|
|
|
/* ── setImmediate as a first-class dyn value ─────────────────────────
|
|
* The global passed AS A FUNCTION VALUE into untyped code (the Node-suite
|
|
* traceCallback shape: `channel.traceCallback(setImmediate, 0, ctx, null,
|
|
* cb)`). The minted dyn callable schedules its own immediate that calls
|
|
* args[0] with the remaining arguments (Node's setImmediate(cb, ...args)
|
|
* contract) and answers undefined (the Immediate handle object has no dyn
|
|
* story — clearImmediate over this value is not modeled). A non-function
|
|
* first argument throws Node's ERR_INVALID_ARG_TYPE synchronously. */
|
|
|
|
static void scr_imm_dyn_fire(ScrClosure *c) {
|
|
ScrDyn *fn = (ScrDyn *)scr_box_get_ref(c->caps[0]);
|
|
ScrDyn *args = (ScrDyn *)scr_box_get_ref(c->caps[1]);
|
|
ScrDyn *r = scr_dyn_call(fn, args->v.arr.items, args->v.arr.len, "immediate callback");
|
|
if (r != NULL) scr_dyn_release(r);
|
|
scr_dyn_release(fn);
|
|
scr_dyn_release(args);
|
|
}
|
|
|
|
static ScrDyn *scr_imm_dyn_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
|
|
(void)clo;
|
|
if (argc == 0 || args[0]->kind != SCR_DYN_FUNC) {
|
|
static const char msg[] = "The \"callback\" argument must be of type function.";
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_TYPE");
|
|
return NULL;
|
|
}
|
|
ScrDyn *rest = scr_dyn_new_arr();
|
|
for (size_t i = 1; i < argc; i++) scr_dyn_arr_push(rest, scr_dyn_retain(args[i]));
|
|
ScrClosure *c = scr_closure_new((void *)scr_imm_dyn_fire, 2);
|
|
c->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL);
|
|
scr_box_set_ref(c->caps[0], scr_dyn_retain(args[0]));
|
|
c->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL);
|
|
scr_box_set_ref(c->caps[1], rest); /* moves */
|
|
scr_set_immediate(c);
|
|
return scr_dyn_retain(scr_dyn_undefined());
|
|
}
|
|
|
|
ScrDyn *scr_set_immediate_dyn_value(void) {
|
|
return scr_dyn_new_func(scr_closure_new((void *)scr_imm_dyn_thunk, 0), scr_imm_dyn_thunk,
|
|
1, "(cb,...)", "setImmediate");
|
|
}
|
|
|
|
/* ── queueMicrotask ──────────────────────────────────────────────────
|
|
* The READY queue's FIFO is the microtask queue (promise continuations
|
|
* ride it as parked fibers), so a queueMicrotask callback enters the SAME
|
|
* queue as a stackless envelope — one order across both producers, like
|
|
* V8's single microtask queue. scr_resume_fiber runs the closure on the
|
|
* main stack; a throw is an uncaught exception (Node's queueMicrotask),
|
|
* never a rejection. */
|
|
void scr_queue_microtask(ScrClosure *cb) {
|
|
ScrFiber *f = scr_mem_calloc(sizeof *f);
|
|
if (!f) scr_oom();
|
|
f->micro_cb = cb; /* ownership moves in */
|
|
scr_ready_push(f);
|
|
}
|
|
|
|
/* ── scheduling in an AsyncLocalStorage context ───────────────────────
|
|
* Programs that enter AsyncLocalStorage stores schedule their timer,
|
|
* interval, setImmediate, process.nextTick and queueMicrotask callbacks
|
|
* through these entry points (the emitter picks them when the program
|
|
* uses the store API), so each callback runs with the stores that were
|
|
* active when it was scheduled, as Node's async resources capture them.
|
|
* With a snapshot active, the callback is wrapped in a closure that
|
|
* installs it around the call; other programs keep the plain entry
|
|
* points. */
|
|
static void *scr_als_ctx_retain_v(void *c) { return scr_als_ctx_retain(c); }
|
|
static void scr_als_ctx_release_v(void *c) { scr_als_ctx_release(c); }
|
|
|
|
static void scr_als_bound_fire(ScrClosure *self) {
|
|
ScrClosure *cb = scr_box_get_ref(self->caps[0]);
|
|
ScrAlsCtx **slot = SCR_ALS_SLOT();
|
|
ScrAlsCtx *prev = *slot;
|
|
*slot = scr_box_get_ref(self->caps[1]); /* the slot owns this reference */
|
|
((void (*)(ScrClosure *))cb->fn)(cb);
|
|
/* An enterWith inside the callback replaced the slot's snapshot; either
|
|
* way the slot's reference is released here. */
|
|
slot = SCR_ALS_SLOT();
|
|
scr_als_ctx_release(*slot);
|
|
*slot = prev;
|
|
scr_closure_release(cb);
|
|
}
|
|
|
|
static ScrClosure *scr_als_bind(ScrClosure *cb /*moves*/) {
|
|
ScrAlsCtx *active = *SCR_ALS_SLOT();
|
|
if (active == NULL) return cb;
|
|
ScrClosure *bound = scr_closure_new((void *)scr_als_bound_fire, 2);
|
|
bound->caps[0] = scr_box_new(SCR_BOX_FUNC);
|
|
scr_box_set_ref(bound->caps[0], cb);
|
|
bound->caps[1] = scr_box_new_obj(scr_als_ctx_retain_v, scr_als_ctx_release_v, NULL);
|
|
scr_box_set_ref(bound->caps[1], scr_als_ctx_retain(active));
|
|
return bound;
|
|
}
|
|
|
|
void scr_set_timeout_als(ScrClosure *cb, double ms) { scr_set_timeout(scr_als_bind(cb), ms); }
|
|
double scr_set_interval_als(ScrClosure *cb, double ms) {
|
|
return scr_set_interval(scr_als_bind(cb), ms);
|
|
}
|
|
double scr_set_timeout_handle_als(ScrClosure *cb, double ms) {
|
|
return scr_set_timeout_handle(scr_als_bind(cb), ms);
|
|
}
|
|
double scr_set_immediate_als(ScrClosure *cb) { return scr_set_immediate(scr_als_bind(cb)); }
|
|
void scr_next_tick_als(ScrClosure *cb) { scr_next_tick(scr_als_bind(cb)); }
|
|
void scr_queue_microtask_als(ScrClosure *cb) { scr_queue_microtask(scr_als_bind(cb)); }
|
|
|
|
/* The checked-dynamic argument form (JS files — common.mustCall wrappers
|
|
* and the suite's invalid-input probes): a non-function throws Node's
|
|
* ERR_INVALID_ARG_TYPE synchronously; a function value is called with
|
|
* zero arguments (Node passes none — extra queueMicrotask arguments are
|
|
* ignored). */
|
|
static void scr_micro_dyn_fire(ScrClosure *c) {
|
|
ScrDyn *fn = (ScrDyn *)scr_box_get_ref(c->caps[0]);
|
|
ScrDyn *r = scr_dyn_call(fn, NULL, 0, "queueMicrotask callback");
|
|
if (r != NULL) scr_dyn_release(r);
|
|
scr_dyn_release(fn);
|
|
}
|
|
|
|
/* The closure that calls a dyn function value, or NULL with Node's
|
|
* ERR_INVALID_ARG_TYPE pending for a non-function. */
|
|
static ScrClosure *scr_micro_dyn_closure(const ScrDyn *cb) {
|
|
if (cb->kind != SCR_DYN_FUNC) {
|
|
static const char msg[] = "The \"callback\" argument must be of type function.";
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_INVALID_ARG_TYPE");
|
|
return NULL;
|
|
}
|
|
ScrClosure *c = scr_closure_new((void *)scr_micro_dyn_fire, 1);
|
|
c->caps[0] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, NULL);
|
|
scr_box_set_ref(c->caps[0], scr_dyn_retain((ScrDyn *)cb));
|
|
return c;
|
|
}
|
|
|
|
void scr_queue_microtask_dyn(const ScrDyn *cb) {
|
|
ScrClosure *c = scr_micro_dyn_closure(cb);
|
|
if (c) scr_queue_microtask(c);
|
|
}
|
|
|
|
void scr_queue_microtask_dyn_als(const ScrDyn *cb) {
|
|
ScrClosure *c = scr_micro_dyn_closure(cb);
|
|
if (c) scr_queue_microtask_als(c);
|
|
}
|
|
|
|
/* ── the event hooks (scr_events.c) ──────────────────────────────────
|
|
* Process signal/exit events and the piped-stdin surface live in
|
|
* scr_events.c, which links ONLY into binaries whose IR uses those
|
|
* surfaces (the io-hook precedent, gated like scr_regex/scr_fetch):
|
|
* scr_events_install() fills these slots before %main. Event-free
|
|
* builds keep every slot NULL and the loop is byte-identical in
|
|
* behavior. The scr_lib.c hooks live in scr_lib.c and the abnormal-exit
|
|
* code hint in scr_exception.c, so each unit stays self-contained (the
|
|
* runtime C tests link them separately). */
|
|
|
|
static SCR_TL bool (*scr_events_pending_fn)(void) = NULL; /* keeps the loop alive */
|
|
static SCR_TL bool (*scr_events_watching_fn)(void) = NULL; /* wants the poll sleep */
|
|
static SCR_TL void (*scr_events_dispatch_fn)(void) = NULL; /* fire due listeners */
|
|
static SCR_TL int (*scr_events_pollfds_fn)(int out[2]) = NULL;
|
|
|
|
void scr_loop_set_events(bool (*pending)(void), bool (*watching)(void),
|
|
void (*dispatch)(void), int (*pollfds)(int out[2])) {
|
|
scr_events_pending_fn = pending;
|
|
scr_events_watching_fn = watching;
|
|
scr_events_dispatch_fn = dispatch;
|
|
scr_events_pollfds_fn = pollfds;
|
|
}
|
|
|
|
/* ── the event loop ───────────────────────────────────────────────────── */
|
|
|
|
static void scr_resume_fiber(ScrFiber *f) {
|
|
if (f->gen_job != NULL) {
|
|
ScrGen *g = f->gen_job;
|
|
scr_mem_free(f);
|
|
scr_async_gen_job(g);
|
|
scr_gen_release(g);
|
|
return;
|
|
}
|
|
/* A queueMicrotask envelope: run the closure on the main stack, no
|
|
* context switch. A throw leaves the exception cell pending — every
|
|
* drain site returns to main's uncaught report, Node's queueMicrotask
|
|
* semantics (uncaughtException, never an unhandled rejection). */
|
|
if (f->micro_cb != NULL) {
|
|
ScrClosure *cb = f->micro_cb;
|
|
scr_mem_free(f);
|
|
((void (*)(ScrClosure *))cb->fn)(cb);
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
/* A Promise.all / Promise.race reaction job, also on the main stack. */
|
|
if (f->cb_job) {
|
|
scr_promise_job_run(f);
|
|
return;
|
|
}
|
|
if (f->gen != NULL) {
|
|
scr_async_gen_resume_ready(f->gen);
|
|
return;
|
|
}
|
|
#ifdef _WIN32
|
|
/* The loop runs on the main stack; make sure scr_loop_ctx names its
|
|
* fiber handle (a no-op after the first conversion). */
|
|
(void)scr_win_self();
|
|
#endif
|
|
f->return_to = &scr_loop_ctx;
|
|
/* The body continues in the lifetime region it was spawned in. */
|
|
const ScrRgnFiberHooks *regions = atomic_load_explicit(&scr_region_fiber_hooks, memory_order_relaxed);
|
|
void *region_before = regions != NULL ? regions->resume(f->region) : NULL;
|
|
#ifdef __wasi__
|
|
scr_current = f;
|
|
scr_exc_swap_cell(&f->exc);
|
|
scr_als_active = &f->als;
|
|
if (f->coro != NULL) {
|
|
scr_wasi_coro_resume(f->coro);
|
|
} else {
|
|
/* scr_async_spawn_after's runtime C continuation. Its dependency is
|
|
* settled, so the entry can extract through scr_await_* without a
|
|
* stack switch and then completes in this turn. */
|
|
f->entry(f, f->argpack);
|
|
scr_fiber_finish(f);
|
|
}
|
|
scr_current = NULL;
|
|
scr_exc_swap_cell(NULL);
|
|
scr_als_active = &scr_als_main_slot;
|
|
if (regions != NULL) regions->suspend(region_before);
|
|
#else
|
|
if (f->stackless) {
|
|
/* A stackless async body continues on the loop's stack. */
|
|
scr_current = f;
|
|
scr_exc_swap_cell(&f->exc);
|
|
scr_als_active = &f->als;
|
|
scr_coro_resume(f->coro);
|
|
scr_current = NULL;
|
|
scr_exc_swap_cell(NULL);
|
|
scr_als_active = &scr_als_main_slot;
|
|
if (regions != NULL) regions->suspend(region_before);
|
|
} else {
|
|
scr_switch(&scr_loop_ctx, &f->ctx, f);
|
|
if (regions != NULL) regions->suspend(region_before);
|
|
f = scr_loop_reap;
|
|
if (f == NULL) return;
|
|
scr_loop_reap = NULL;
|
|
}
|
|
#endif
|
|
if (f->done && !f->direct) {
|
|
scr_promise_release(f->promise);
|
|
scr_fiber_destroy(f);
|
|
scr_fibers_live--;
|
|
}
|
|
}
|
|
|
|
/* Reap granularity while children are pending on the POLLING fallback:
|
|
* the loop polls waitpid(WNOHANG) at least this often instead of sleeping
|
|
* to the next timer deadline. The primary path has no cap — the sleep
|
|
* waits on the child watch (scr_children_wait — kqueue NOTE_EXIT on BSD,
|
|
* pidfd epoll on Linux, the timer deadline riding the wait timeout) and a
|
|
* child's exit wakes it immediately. The fallback covers platforms with
|
|
* neither backend, children whose exit watch could not be armed, and
|
|
* turns where the io poll takes the sleep (curl's fds can't join it). */
|
|
#define SCR_CHILD_POLL_MS 1.0
|
|
|
|
/* Sleep cap while only external io is pending (no timer models its
|
|
* wakeup): the io poll sleeps on REAL fds inside this window (a fetch
|
|
* transfer's sockets wake it early), so a generous cap costs nothing. */
|
|
#define SCR_IO_POLL_MS 1000.0
|
|
|
|
/* Sleep cap while signal handlers or stdin consumers are pending AND the
|
|
* io poll owns the sleep (curl's fd wait retries on EINTR and can't watch
|
|
* the wake pipe): bounded signal/stdin latency during a fetch at 20
|
|
* wakeups/second. The dedicated poll(2) sleep needs no cap — it waits on
|
|
* the wake pipe and fd 0 directly. */
|
|
#define SCR_SIGNAL_POLL_MS 50.0
|
|
|
|
/* The net hook (scr_net.c, when linked — the events-hook shape):
|
|
* `pending` keeps the loop alive while servers/sockets are live,
|
|
* `dispatch` fires at every turn top, and `pollfd` is the unit's poller
|
|
* fd for the idle poll(2) sleep (readable while events are pending —
|
|
* kqueue and epoll fds both behave this way).
|
|
* Net-free builds keep every slot NULL and the loop is byte-identical. */
|
|
static SCR_TL bool (*scr_net_pending_fn)(void) = NULL;
|
|
static SCR_TL void (*scr_net_dispatch_fn)(void) = NULL;
|
|
static SCR_TL int (*scr_net_pollfd_fn)(void) = NULL;
|
|
|
|
/* Net io without liveness: unref'd sockets and servers still deliver
|
|
* events while the loop runs for other work, but do not keep it alive. */
|
|
static SCR_TL bool (*scr_net_io_fn)(void) = NULL;
|
|
|
|
void scr_loop_set_net_io(bool (*io)(void)) { scr_net_io_fn = io; }
|
|
|
|
void scr_loop_set_net(bool (*pending)(void), void (*dispatch)(void), int (*pollfd)(void)) {
|
|
scr_net_pending_fn = pending;
|
|
scr_net_dispatch_fn = dispatch;
|
|
scr_net_pollfd_fn = pollfd;
|
|
}
|
|
|
|
/* The dgram hook (scr_dgram.c, when linked) — the net hook's exact shape:
|
|
* one more set of nullable slots, byte-identical loop behavior when
|
|
* unset. */
|
|
static SCR_TL bool (*scr_dgram_pending_fn)(void) = NULL;
|
|
static SCR_TL void (*scr_dgram_dispatch_fn)(void) = NULL;
|
|
static SCR_TL int (*scr_dgram_pollfd_fn)(void) = NULL;
|
|
|
|
void scr_loop_set_dgram(bool (*pending)(void), void (*dispatch)(void), int (*pollfd)(void)) {
|
|
scr_dgram_pending_fn = pending;
|
|
scr_dgram_dispatch_fn = dispatch;
|
|
scr_dgram_pollfd_fn = pollfd;
|
|
}
|
|
|
|
/* The fs.watch hook (scr_watch.c, when linked) — the net hook's exact
|
|
* shape: one more set of nullable slots, byte-identical loop behavior
|
|
* when unset. */
|
|
static SCR_TL bool (*scr_watch_pending_fn)(void) = NULL;
|
|
static SCR_TL void (*scr_watch_dispatch_fn)(void) = NULL;
|
|
static SCR_TL int (*scr_watch_pollfd_fn)(void) = NULL;
|
|
|
|
void scr_loop_set_watch(bool (*pending)(void), void (*dispatch)(void), int (*pollfd)(void)) {
|
|
scr_watch_pending_fn = pending;
|
|
scr_watch_dispatch_fn = dispatch;
|
|
scr_watch_pollfd_fn = pollfd;
|
|
}
|
|
|
|
|
|
/* The child-process hook (scr_child.c, installed with the first spawned
|
|
* child or IPC channel). Programs that never create one keep it NULL and
|
|
* do not link the child unit; WASI never links it. */
|
|
static SCR_TL const ScrLoopChildren *scr_loop_children = NULL;
|
|
|
|
void scr_loop_set_children(const ScrLoopChildren *hooks) { scr_loop_children = hooks; }
|
|
|
|
/* The foreign-FFI queue hook (scr_ffi.c when a format-5 descriptor exists).
|
|
* Its pending count includes live registrations (reffed by default) and
|
|
* queued deliveries; dispatch runs exactly one callback per loop turn. */
|
|
static SCR_TL bool (*scr_ffi_pending_fn)(void) = NULL;
|
|
static SCR_TL bool (*scr_ffi_dispatch_fn)(void) = NULL;
|
|
static SCR_TL int (*scr_ffi_pollfd_fn)(void) = NULL;
|
|
static SCR_TL void (*scr_ffi_stop_fn)(void) = NULL;
|
|
|
|
void scr_loop_set_ffi(bool (*pending)(void), bool (*dispatch)(void),
|
|
int (*pollfd)(void), void (*stop)(void)) {
|
|
scr_ffi_pending_fn = pending;
|
|
scr_ffi_dispatch_fn = dispatch;
|
|
scr_ffi_pollfd_fn = pollfd;
|
|
scr_ffi_stop_fn = stop;
|
|
}
|
|
|
|
/* The stream hook (scr_stream.c, when linked): `pending` keeps the loop
|
|
* alive while deferred stream ticks exist, `dispatch` drains them at the
|
|
* TOP of every turn — before the events/net stations, the closest
|
|
* placement to Node's process.nextTick (whose stream emissions these
|
|
* are). No poller fd: ticks are pure CPU work, always ready. */
|
|
static SCR_TL bool (*scr_stream_pending_fn)(void) = NULL;
|
|
static SCR_TL void (*scr_stream_dispatch_fn)(void) = NULL;
|
|
|
|
void scr_loop_set_stream(bool (*pending)(void), void (*dispatch)(void)) {
|
|
scr_stream_pending_fn = pending;
|
|
scr_stream_dispatch_fn = dispatch;
|
|
}
|
|
|
|
#ifdef SCR_WORKERS
|
|
static SCR_TL bool (*scr_workers_pending_fn)(void);
|
|
static SCR_TL void (*scr_workers_dispatch_fn)(void);
|
|
static SCR_TL bool (*scr_workers_more_fn)(void);
|
|
static SCR_TL int (*scr_workers_pollfd_fn)(void);
|
|
static SCR_TL bool (*scr_workers_ready_fn)(void);
|
|
static SCR_TL void (*scr_workers_wait_fn)(double);
|
|
|
|
void scr_loop_set_workers(bool (*pending)(void), bool (*ready)(void), void (*dispatch)(void),
|
|
int (*pollfd)(void), void (*wait)(double)) {
|
|
scr_workers_pending_fn = pending;
|
|
scr_workers_dispatch_fn = dispatch;
|
|
scr_workers_pollfd_fn = pollfd;
|
|
scr_workers_ready_fn = ready;
|
|
scr_workers_wait_fn = wait;
|
|
}
|
|
|
|
void scr_loop_set_workers_more(bool (*more)(void)) { scr_workers_more_fn = more; }
|
|
#endif
|
|
|
|
static bool scr_workers_pending(void) {
|
|
#ifdef SCR_WORKERS
|
|
return scr_workers_pending_fn && scr_workers_pending_fn();
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
static void scr_workers_dispatch(void) {
|
|
#ifdef SCR_WORKERS
|
|
if (scr_workers_dispatch_fn) scr_workers_dispatch_fn();
|
|
#endif
|
|
}
|
|
|
|
static int scr_workers_pollfd(void) {
|
|
#ifdef SCR_WORKERS
|
|
return scr_workers_pollfd_fn ? scr_workers_pollfd_fn() : -1;
|
|
#else
|
|
return -1;
|
|
#endif
|
|
}
|
|
|
|
/* Dispatch hooks yield between event batches when process.nextTick
|
|
* callbacks or fibers are already queued, so ticks and promise jobs
|
|
* interleave with event delivery (Node's checkpoint after each
|
|
* macrotask). */
|
|
bool scr_loop_has_ready(void) {
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) return scr_hosted_queued > 0;
|
|
#endif
|
|
return scr_ready_len > 0 || scr_nt_head != NULL;
|
|
}
|
|
|
|
/* The poll phase runs once per loop iteration, like libuv's: the net unit
|
|
* drains the poller at most once between check phases. I/O that becomes
|
|
* ready while this iteration's callbacks run (a loopback connect started
|
|
* by one of them) waits for the next iteration, so immediates queued in
|
|
* the meantime fire first — Node's poll → check order. A full batch polls
|
|
* again without waiting, at most SCR_LOOP_IO_BATCHES times per phase (the
|
|
* count libuv's uv__io_poll uses); later events wait for the next
|
|
* iteration, so timers and immediates still run while sockets stay busy. */
|
|
enum { SCR_LOOP_IO_BATCHES = 48 };
|
|
static SCR_TL bool scr_loop_io_open = true;
|
|
static SCR_TL int scr_loop_io_batches = 0;
|
|
bool scr_loop_io_take(void) {
|
|
bool open = scr_loop_io_open;
|
|
scr_loop_io_open = false;
|
|
return open;
|
|
}
|
|
bool scr_loop_io_more(void) { return ++scr_loop_io_batches < SCR_LOOP_IO_BATCHES; }
|
|
void scr_loop_io_reopen(void) { scr_loop_io_open = true; }
|
|
|
|
#ifdef SCR_NODE_ISLANDS
|
|
/* ── the hosted loop (scope-B NI-3) ───────────────────────────────────────
|
|
* Once Node is loaded into the process there is one event loop, Node's,
|
|
* and one set of queues. scr_loop_host (called by scr_node_island.c right
|
|
* after Node starts) migrates the runtime's pending work onto Node's
|
|
* queues in order and points every scheduling primitive at the bridge:
|
|
*
|
|
* - promise jobs and queueMicrotask envelopes (the READY queue) become V8
|
|
* microtasks, ticks become process.nextTick callbacks, so the jobs of
|
|
* both sides interleave in one FIFO each, exactly as in Node;
|
|
* - timers become Node timers, created in (deadline, sequence) order with
|
|
* their remaining delay measured from the moment Node began loading (the
|
|
* load itself is invisible to timer order, as it is to a program that
|
|
* runs on Node from the start); immediates become Node immediates;
|
|
* - the runtime's I/O hooks (the scr_loop_set_* slots) are watched by the
|
|
* bridge: their fds as uv_poll handles, their liveness as the ref of one
|
|
* handle, always-ready work as an idle handle; scr_loop_hosted_io runs
|
|
* the dispatch stations with Node's checkpoint after each.
|
|
*
|
|
* Every runtime job runs through scr_loop_hosted_run on the stack of
|
|
* Node's own dispatch, so Node drains ticks and microtasks after it as
|
|
* after any callback. A throw that escapes a job goes through the
|
|
* runtime's uncaught machinery (process 'uncaughtException' listeners)
|
|
* and, unhandled, ends the process as the runtime's main would. */
|
|
|
|
typedef enum {
|
|
SCR_HJ_FREE = 0,
|
|
SCR_HJ_MICRO = SCR_NODE_JOB_MICROTASK,
|
|
SCR_HJ_TICK = SCR_NODE_JOB_TICK,
|
|
SCR_HJ_TIMER = SCR_NODE_JOB_TIMER,
|
|
SCR_HJ_IMMEDIATE = SCR_NODE_JOB_IMMEDIATE,
|
|
} ScrHJobKind;
|
|
|
|
/* One scheduled runtime job; its token (index + 1) travels through the
|
|
* bridge. Entries move when the table grows: never hold a pointer across
|
|
* a callback. */
|
|
typedef struct {
|
|
uint8_t kind;
|
|
bool reffed;
|
|
bool firing; /* a timer whose callback is running */
|
|
bool cleared; /* ... and cleared from inside it */
|
|
bool refreshed; /* ... and refreshed from inside it */
|
|
bool due; /* runs before Node's loop starts (handle 0 until then) */
|
|
uint32_t handle; /* the bridge's timer or immediate */
|
|
uint32_t next_free;
|
|
unsigned long id; /* the program's handle; 0 for plain setTimeout */
|
|
double repeat_ms;
|
|
double delay_ms; /* the original delay: refresh() of a due timer re-arms with it */
|
|
void *p; /* ScrFiber *, ScrNtick *, or the owned ScrClosure * */
|
|
} ScrHJob;
|
|
|
|
|
|
static SCR_TL ScrHJob *scr_hjobs = NULL;
|
|
static SCR_TL uint32_t scr_hjobs_len = 0, scr_hjobs_cap = 0, scr_hjobs_free = 0;
|
|
|
|
static uint32_t scr_hjob_new(uint8_t kind, void *p) {
|
|
uint32_t token;
|
|
if (scr_hjobs_free != 0) {
|
|
token = scr_hjobs_free;
|
|
scr_hjobs_free = scr_hjobs[token - 1].next_free;
|
|
} else {
|
|
if (scr_hjobs_len == scr_hjobs_cap) {
|
|
scr_hjobs_cap = scr_hjobs_cap ? scr_hjobs_cap * 2 : 64;
|
|
scr_hjobs = realloc(scr_hjobs, scr_hjobs_cap * sizeof *scr_hjobs);
|
|
if (!scr_hjobs) scr_oom();
|
|
}
|
|
token = ++scr_hjobs_len;
|
|
}
|
|
ScrHJob *j = &scr_hjobs[token - 1];
|
|
memset(j, 0, sizeof *j);
|
|
j->kind = kind;
|
|
j->p = p;
|
|
return token;
|
|
}
|
|
|
|
static ScrHJob *scr_hjob(uint32_t token, uint8_t kind) {
|
|
if (token == 0 || token > scr_hjobs_len) return NULL;
|
|
ScrHJob *j = &scr_hjobs[token - 1];
|
|
return j->kind == kind ? j : NULL;
|
|
}
|
|
|
|
static void scr_hjob_free(uint32_t token) {
|
|
ScrHJob *j = &scr_hjobs[token - 1];
|
|
j->kind = SCR_HJ_FREE;
|
|
j->due = false;
|
|
j->p = NULL;
|
|
j->next_free = scr_hjobs_free;
|
|
scr_hjobs_free = token;
|
|
}
|
|
|
|
/* Work the loop pass was about to run when Node loaded: immediates left in
|
|
* the check phase being run, and timers already due. They run, in that
|
|
* order, before Node's loop starts (scr_loop_run_hosted), as they would
|
|
* have in the phase the switch interrupted; Node would never have started
|
|
* a new turn first. */
|
|
static SCR_TL uint32_t *scr_hosted_due = NULL;
|
|
static SCR_TL size_t scr_hosted_due_len = 0, scr_hosted_due_cap = 0;
|
|
|
|
static void scr_hosted_due_push(uint32_t token) {
|
|
if (scr_hosted_due_len == scr_hosted_due_cap) {
|
|
scr_hosted_due_cap = scr_hosted_due_cap ? scr_hosted_due_cap * 2 : 16;
|
|
scr_hosted_due = realloc(scr_hosted_due, scr_hosted_due_cap * sizeof *scr_hosted_due);
|
|
if (!scr_hosted_due) scr_oom();
|
|
}
|
|
scr_hosted_due[scr_hosted_due_len++] = token;
|
|
scr_hjobs[token - 1].due = true;
|
|
}
|
|
|
|
/* Program handle → token, for timers (key id*2) and immediates (id*2+1).
|
|
* Open addressing with linear probing and backward-shift deletion. */
|
|
typedef struct {
|
|
unsigned long key; /* 0 = empty */
|
|
uint32_t token;
|
|
} ScrHSlot;
|
|
static SCR_TL ScrHSlot *scr_hmap = NULL;
|
|
static SCR_TL size_t scr_hmap_cap = 0, scr_hmap_len = 0;
|
|
|
|
static size_t scr_hmap_home(unsigned long key) {
|
|
uint64_t h = (uint64_t)key * UINT64_C(0x9E3779B97F4A7C15);
|
|
return (size_t)(h >> 32) & (scr_hmap_cap - 1);
|
|
}
|
|
|
|
static void scr_hmap_put(unsigned long key, uint32_t token);
|
|
|
|
static void scr_hmap_grow(void) {
|
|
ScrHSlot *old = scr_hmap;
|
|
size_t old_cap = scr_hmap_cap;
|
|
scr_hmap_cap = old_cap ? old_cap * 2 : 64;
|
|
scr_hmap = calloc(scr_hmap_cap, sizeof *scr_hmap);
|
|
if (!scr_hmap) scr_oom();
|
|
scr_hmap_len = 0;
|
|
for (size_t i = 0; i < old_cap; i++)
|
|
if (old[i].key != 0) scr_hmap_put(old[i].key, old[i].token);
|
|
free(old);
|
|
}
|
|
|
|
static void scr_hmap_put(unsigned long key, uint32_t token) {
|
|
if ((scr_hmap_len + 1) * 2 > scr_hmap_cap) scr_hmap_grow();
|
|
size_t mask = scr_hmap_cap - 1, i = scr_hmap_home(key);
|
|
while (scr_hmap[i].key != 0 && scr_hmap[i].key != key) i = (i + 1) & mask;
|
|
if (scr_hmap[i].key == 0) scr_hmap_len++;
|
|
scr_hmap[i].key = key;
|
|
scr_hmap[i].token = token;
|
|
}
|
|
|
|
static size_t scr_hmap_find(unsigned long key) {
|
|
if (scr_hmap_cap == 0) return SIZE_MAX;
|
|
size_t mask = scr_hmap_cap - 1;
|
|
for (size_t i = scr_hmap_home(key); scr_hmap[i].key != 0; i = (i + 1) & mask)
|
|
if (scr_hmap[i].key == key) return i;
|
|
return SIZE_MAX;
|
|
}
|
|
|
|
static void scr_hmap_delete(unsigned long key) {
|
|
size_t i = scr_hmap_find(key);
|
|
if (i == SIZE_MAX) return;
|
|
size_t mask = scr_hmap_cap - 1;
|
|
for (size_t j = (i + 1) & mask; scr_hmap[j].key != 0; j = (j + 1) & mask) {
|
|
size_t home = scr_hmap_home(scr_hmap[j].key);
|
|
bool stays = i <= j ? (i < home && home <= j) : (i < home || home <= j);
|
|
if (stays) continue;
|
|
scr_hmap[i] = scr_hmap[j];
|
|
i = j;
|
|
}
|
|
scr_hmap[i].key = 0;
|
|
scr_hmap_len--;
|
|
}
|
|
|
|
static uint32_t scr_hmap_token(unsigned long key) {
|
|
size_t i = scr_hmap_find(key);
|
|
return i == SIZE_MAX ? 0 : scr_hmap[i].token;
|
|
}
|
|
|
|
/* ── scheduling ── */
|
|
|
|
static void scr_hosted_microtask(ScrFiber *f) {
|
|
uint32_t token = scr_hjob_new(SCR_HJ_MICRO, f);
|
|
scr_hosted_queued++;
|
|
scr_loop_hosted->microtask(token);
|
|
}
|
|
|
|
static void scr_hosted_tick(ScrNtick *t) {
|
|
uint32_t token = scr_hjob_new(SCR_HJ_TICK, t);
|
|
scr_hosted_queued++;
|
|
scr_loop_hosted->tick(token);
|
|
}
|
|
|
|
/* Node's timer delay for a remaining time: whole milliseconds, at least
|
|
* one (Node's clamp), rounded up so a timer never fires before its
|
|
* deadline. */
|
|
static double scr_hosted_delay(double remaining) {
|
|
if (!(remaining >= 1)) return 1;
|
|
if (remaining > 2147483647.0) return 2147483647.0;
|
|
double delay = (double)(long long)remaining;
|
|
return delay < remaining - 1e-6 ? delay + 1 : delay;
|
|
}
|
|
|
|
/* A runtime timer on Node's loop; a `due` one waits for scr_loop_run_hosted
|
|
* and is armed only if it runs again. */
|
|
static void scr_hosted_timer_arm(ScrClosure *cb, double delay, double repeat, double delay_ms,
|
|
unsigned long id, bool reffed, bool due) {
|
|
uint32_t token = scr_hjob_new(SCR_HJ_TIMER, cb);
|
|
ScrHJob *j = &scr_hjobs[token - 1];
|
|
j->id = id;
|
|
j->repeat_ms = repeat;
|
|
j->delay_ms = delay_ms;
|
|
j->reffed = reffed;
|
|
if (id != 0) scr_hmap_put(id * 2, token);
|
|
if (due) {
|
|
scr_hosted_due_push(token);
|
|
return;
|
|
}
|
|
uint32_t handle = scr_loop_hosted->timer(delay, repeat, reffed ? 1 : 0, token);
|
|
scr_hjobs[token - 1].handle = handle;
|
|
}
|
|
|
|
static void scr_hosted_timer_push(ScrTimer t) {
|
|
scr_hosted_timer_arm(t.cb, scr_hosted_delay(t.deadline_ms - scr_now_ms()), t.repeat_ms,
|
|
t.delay_ms, t.id, t.reffed, false);
|
|
}
|
|
|
|
static void scr_hosted_timer_drop(uint32_t token) {
|
|
ScrHJob *j = &scr_hjobs[token - 1];
|
|
ScrClosure *cb = (ScrClosure *)j->p;
|
|
if (j->id != 0) scr_hmap_delete(j->id * 2);
|
|
scr_hjob_free(token);
|
|
scr_closure_release(cb);
|
|
}
|
|
|
|
static bool scr_hosted_timer_op(unsigned long id, int op) {
|
|
uint32_t token = scr_hmap_token(id * 2);
|
|
ScrHJob *j = scr_hjob(token, SCR_HJ_TIMER);
|
|
if (j == NULL) return false;
|
|
switch (op) {
|
|
case SCR_HOSTED_CLEAR:
|
|
scr_loop_hosted->timer_clear(j->handle);
|
|
if (j->firing) {
|
|
j->cleared = true; /* dropped when its callback returns */
|
|
scr_hmap_delete(id * 2);
|
|
} else {
|
|
scr_hosted_timer_drop(token);
|
|
}
|
|
return true;
|
|
case SCR_HOSTED_REF:
|
|
case SCR_HOSTED_UNREF:
|
|
j->reffed = op == SCR_HOSTED_REF;
|
|
scr_loop_hosted->timer_ref(j->handle, j->reffed ? 1 : 0);
|
|
return true;
|
|
case SCR_HOSTED_REFRESH:
|
|
if (j->firing) j->refreshed = true;
|
|
if (j->handle != 0) scr_loop_hosted->timer_refresh(j->handle);
|
|
return true;
|
|
default:
|
|
return j->reffed;
|
|
}
|
|
}
|
|
|
|
static double scr_hosted_set_immediate(ScrClosure *cb) {
|
|
unsigned long id = ++scr_immediate_seq;
|
|
uint32_t token = scr_hjob_new(SCR_HJ_IMMEDIATE, cb);
|
|
scr_hjobs[token - 1].id = id;
|
|
scr_hjobs[token - 1].reffed = true;
|
|
scr_hmap_put(id * 2 + 1, token);
|
|
uint32_t handle = scr_loop_hosted->immediate(1, token);
|
|
scr_hjobs[token - 1].handle = handle;
|
|
return (double)id;
|
|
}
|
|
|
|
static bool scr_hosted_immediate_op(unsigned long id, int op) {
|
|
uint32_t token = scr_hmap_token(id * 2 + 1);
|
|
ScrHJob *j = scr_hjob(token, SCR_HJ_IMMEDIATE);
|
|
if (j == NULL) return false;
|
|
switch (op) {
|
|
case SCR_HOSTED_CLEAR: {
|
|
ScrClosure *cb = (ScrClosure *)j->p;
|
|
scr_loop_hosted->immediate_clear(j->handle);
|
|
scr_hmap_delete(id * 2 + 1);
|
|
scr_hjob_free(token);
|
|
scr_closure_release(cb);
|
|
return true;
|
|
}
|
|
case SCR_HOSTED_REF:
|
|
case SCR_HOSTED_UNREF:
|
|
j->reffed = op == SCR_HOSTED_REF;
|
|
scr_loop_hosted->immediate_ref(j->handle, j->reffed ? 1 : 0);
|
|
return true;
|
|
default:
|
|
return j->reffed;
|
|
}
|
|
}
|
|
|
|
/* Live runtime timers (kind 0) or immediates (kind 1). */
|
|
static size_t scr_hosted_count(int kind) {
|
|
uint8_t want = kind == 0 ? SCR_HJ_TIMER : SCR_HJ_IMMEDIATE;
|
|
size_t n = 0;
|
|
for (uint32_t i = 0; i < scr_hjobs_len; i++)
|
|
if (scr_hjobs[i].kind == want && !scr_hjobs[i].cleared) n++;
|
|
return n;
|
|
}
|
|
|
|
/* Loop exit: timers and immediates that never ran (unref'd ones, or what
|
|
* remained at process exit) release their closures, like the plain heap's
|
|
* teardown; Node keeps its objects, which can no longer fire. */
|
|
static void scr_hosted_teardown(void) {
|
|
for (uint32_t i = 0; i < scr_hjobs_len; i++) {
|
|
ScrHJob *j = &scr_hjobs[i];
|
|
if ((j->kind == SCR_HJ_TIMER || j->kind == SCR_HJ_IMMEDIATE) && !j->firing) {
|
|
ScrClosure *cb = (ScrClosure *)j->p;
|
|
j->kind = SCR_HJ_FREE;
|
|
j->p = NULL;
|
|
j->next_free = scr_hjobs_free;
|
|
scr_hjobs_free = i + 1;
|
|
scr_closure_release(cb);
|
|
}
|
|
}
|
|
free(scr_hmap);
|
|
scr_hmap = NULL;
|
|
scr_hmap_cap = scr_hmap_len = 0;
|
|
}
|
|
|
|
/* ── running jobs ── */
|
|
|
|
/* After a runtime callback: an escaped throw goes to the runtime's
|
|
* uncaught machinery; unhandled, it ends the process like main's
|
|
* uncaught path (report, exit listeners, the noted code). */
|
|
static void scr_hosted_settle_exception(void) {
|
|
if (!scr_exc_pending()) return;
|
|
if (scr_exc_handle_uncaught(false)) return;
|
|
scr_exc_print_uncaught();
|
|
scr_process_exit((double)scr_exit_code_hint_get());
|
|
}
|
|
|
|
void scr_loop_hosted_run(uint32_t kind, uint32_t token) {
|
|
switch (kind) {
|
|
case SCR_HJ_MICRO: {
|
|
ScrHJob *j = scr_hjob(token, SCR_HJ_MICRO);
|
|
if (j == NULL) return;
|
|
ScrFiber *f = (ScrFiber *)j->p;
|
|
scr_hjob_free(token);
|
|
scr_hosted_queued--;
|
|
scr_resume_fiber(f);
|
|
break;
|
|
}
|
|
case SCR_HJ_TICK: {
|
|
ScrHJob *j = scr_hjob(token, SCR_HJ_TICK);
|
|
if (j == NULL) return;
|
|
ScrNtick *t = (ScrNtick *)j->p;
|
|
scr_hjob_free(token);
|
|
scr_hosted_queued--;
|
|
ScrClosure *cb = t->cb;
|
|
ScrFsRenameFn error_cb = t->error_cb;
|
|
void (*raw)(void) = t->raw;
|
|
free(t);
|
|
if (cb) {
|
|
if (error_cb) error_cb(cb, NULL);
|
|
else ((void (*)(ScrClosure *))cb->fn)(cb);
|
|
scr_closure_release(cb);
|
|
} else {
|
|
raw();
|
|
}
|
|
break;
|
|
}
|
|
case SCR_HJ_TIMER: {
|
|
ScrHJob *j = scr_hjob(token, SCR_HJ_TIMER);
|
|
if (j == NULL || j->cleared) return;
|
|
ScrClosure *cb = (ScrClosure *)j->p;
|
|
j->firing = true;
|
|
j->refreshed = false;
|
|
((void (*)(ScrClosure *))cb->fn)(cb);
|
|
j = &scr_hjobs[token - 1];
|
|
j->firing = false;
|
|
if (j->cleared) {
|
|
j->cleared = false;
|
|
scr_hjob_free(token);
|
|
scr_closure_release(cb);
|
|
} else if (j->due) {
|
|
/* A due timer ran before Node's loop: arm it only to run again. */
|
|
j->due = false;
|
|
if (j->repeat_ms > 0 || j->refreshed) {
|
|
double delay = j->repeat_ms > 0 ? j->repeat_ms : j->delay_ms;
|
|
uint32_t handle =
|
|
scr_loop_hosted->timer(delay, j->repeat_ms, j->reffed ? 1 : 0, token);
|
|
scr_hjobs[token - 1].handle = handle;
|
|
} else {
|
|
scr_hosted_timer_drop(token);
|
|
}
|
|
} else if (j->repeat_ms <= 0 && !j->refreshed) {
|
|
/* A fired one-shot: Node drops its Timeout; the bridge forgets it. */
|
|
scr_loop_hosted->timer_clear(j->handle);
|
|
scr_hosted_timer_drop(token);
|
|
}
|
|
break;
|
|
}
|
|
case SCR_HJ_IMMEDIATE: {
|
|
ScrHJob *j = scr_hjob(token, SCR_HJ_IMMEDIATE);
|
|
if (j == NULL) return;
|
|
ScrClosure *cb = (ScrClosure *)j->p;
|
|
scr_hmap_delete(j->id * 2 + 1);
|
|
scr_hjob_free(token);
|
|
((void (*)(ScrClosure *))cb->fn)(cb);
|
|
scr_closure_release(cb);
|
|
break;
|
|
}
|
|
default:
|
|
return;
|
|
}
|
|
scr_hosted_settle_exception();
|
|
}
|
|
|
|
/* V8 finished a microtask checkpoint: the runtime's unhandled rejections
|
|
* are decided here, where Node decides its own. An unhandled one ends the
|
|
* process like main's reported-rejection path. */
|
|
void scr_loop_hosted_microtasks_done(void) {
|
|
if (scr_nunhandled == 0) return;
|
|
if (!scr_report_unhandled_rejections()) {
|
|
scr_hosted_settle_exception();
|
|
return;
|
|
}
|
|
scr_discard_unhandled_rejections();
|
|
scr_process_exit((double)scr_exit_code_hint_get());
|
|
}
|
|
|
|
/* ── the runtime's I/O on Node's loop ── */
|
|
|
|
static void scr_hosted_io_fd(ScrNodeIoState *st, int fd) {
|
|
if (fd < 0 || st->nfds >= SCR_NODE_IO_MAX_FDS) return;
|
|
st->fds[st->nfds++] = fd;
|
|
}
|
|
|
|
static void scr_hosted_io_cap(ScrNodeIoState *st, double ms) {
|
|
int cap = (int)ms;
|
|
if (st->cap_ms < 0 || cap < st->cap_ms) st->cap_ms = cap;
|
|
}
|
|
|
|
/* The loop pass's liveness and sleep decisions, as Node's loop needs them
|
|
* before each poll phase and after each check phase. */
|
|
void scr_loop_hosted_io_state(ScrNodeIoState *st) {
|
|
st->nfds = 0;
|
|
st->alive = 0;
|
|
st->ready = 0;
|
|
st->cap_ms = -1;
|
|
/* Quiescent before the poll phase: the turn's scheduled cycle collection. */
|
|
if (st->phase == SCR_NODE_IO_PREPARE) scr_cyc_collect_scheduled();
|
|
bool kids = scr_loop_children != NULL && scr_loop_children->pending();
|
|
bool kids_reffed = scr_loop_children != NULL && scr_loop_children->reffed_pending();
|
|
bool kids_failed = scr_loop_children != NULL && scr_loop_children->failed_pending();
|
|
bool events = scr_events_pending_fn != NULL && scr_events_pending_fn();
|
|
bool evw = scr_events_watching_fn != NULL && scr_events_watching_fn();
|
|
bool net = scr_net_pending_fn != NULL && scr_net_pending_fn();
|
|
bool net_io = net || (scr_net_io_fn != NULL && scr_net_io_fn());
|
|
bool dgram = scr_dgram_pending_fn != NULL && scr_dgram_pending_fn();
|
|
bool watch = scr_watch_pending_fn != NULL && scr_watch_pending_fn();
|
|
bool ffi = scr_ffi_pending_fn != NULL && scr_ffi_pending_fn();
|
|
bool stream = scr_stream_pending_fn != NULL && scr_stream_pending_fn();
|
|
bool renames = scr_fs_renames_pending();
|
|
bool crypto = scr_crypto_bytes_pending();
|
|
bool deferred = scr_loop_deferred_pending();
|
|
bool workers = scr_workers_pending();
|
|
/* Deferred native callbacks (the fs callbacks) become deliverable at the
|
|
* end of the turn that queued them and run in the next one, as the loop
|
|
* pass delivers them; Node's own fs requests take a turn or more too. */
|
|
if (st->phase == SCR_NODE_IO_CHECK) scr_loop_deferred_ready = scr_loop_deferred_count;
|
|
st->alive = kids_reffed || kids_failed || events || net || dgram || watch || ffi || stream ||
|
|
renames || crypto || deferred || workers;
|
|
/* Queued deferred callbacks keep the poll from sleeping (their work is
|
|
* done; only the delivery waits for the next turn). */
|
|
st->ready = stream || crypto || deferred || kids_failed ||
|
|
(scr_loop_children != NULL && scr_loop_children->ready());
|
|
#ifdef SCR_WORKERS
|
|
if (scr_workers_ready_fn && scr_workers_ready_fn()) st->ready = 1;
|
|
#endif
|
|
#if !defined(_WIN32) && !defined(__wasi__)
|
|
if (evw && scr_events_pollfds_fn != NULL) {
|
|
int evfds[2];
|
|
int nev = scr_events_pollfds_fn(evfds);
|
|
for (int i = 0; i < nev; i++) scr_hosted_io_fd(st, evfds[i]);
|
|
}
|
|
if (net_io) {
|
|
int fd = scr_net_pollfd_fn != NULL ? scr_net_pollfd_fn() : -1;
|
|
if (fd >= 0) scr_hosted_io_fd(st, fd);
|
|
else scr_hosted_io_cap(st, SCR_SIGNAL_POLL_MS);
|
|
}
|
|
if (dgram) {
|
|
int fd = scr_dgram_pollfd_fn != NULL ? scr_dgram_pollfd_fn() : -1;
|
|
if (fd >= 0) scr_hosted_io_fd(st, fd);
|
|
else scr_hosted_io_cap(st, SCR_SIGNAL_POLL_MS);
|
|
}
|
|
if (watch) {
|
|
int fd = scr_watch_pollfd_fn != NULL ? scr_watch_pollfd_fn() : -1;
|
|
if (fd >= 0) scr_hosted_io_fd(st, fd);
|
|
else scr_hosted_io_cap(st, SCR_SIGNAL_POLL_MS);
|
|
}
|
|
if (ffi) {
|
|
int fd = scr_ffi_pollfd_fn != NULL ? scr_ffi_pollfd_fn() : -1;
|
|
if (fd >= 0) scr_hosted_io_fd(st, fd);
|
|
else scr_hosted_io_cap(st, SCR_CHILD_POLL_MS);
|
|
}
|
|
if (kids) {
|
|
int fd = scr_loop_children->wake_fd();
|
|
if (fd >= 0) scr_hosted_io_fd(st, fd);
|
|
else scr_hosted_io_cap(st, SCR_CHILD_POLL_MS);
|
|
}
|
|
if (renames) {
|
|
int fd = scr_fs_renames_pollfd();
|
|
if (fd >= 0) scr_hosted_io_fd(st, fd);
|
|
else scr_hosted_io_cap(st, SCR_CHILD_POLL_MS);
|
|
}
|
|
if (workers) {
|
|
int fd = scr_workers_pollfd();
|
|
if (fd >= 0) scr_hosted_io_fd(st, fd);
|
|
else scr_hosted_io_cap(st, SCR_CHILD_POLL_MS);
|
|
}
|
|
#else
|
|
(void)evw;
|
|
if (evw || net_io || dgram || watch || ffi || kids || renames || workers)
|
|
scr_hosted_io_cap(st, SCR_CHILD_POLL_MS);
|
|
#endif
|
|
}
|
|
|
|
/* The end of one runtime macrotask inside a dispatch station: the throw
|
|
* it left, then Node's checkpoint (ticks and microtasks of both sides). */
|
|
static void scr_hosted_macrotask_done(void) {
|
|
scr_hosted_settle_exception();
|
|
scr_loop_hosted->checkpoint(0);
|
|
}
|
|
|
|
/* One pass over the dispatch stations, in the loop pass's order: worker
|
|
* completions and deferred callbacks one per checkpoint, then the event,
|
|
* socket, watch and child stations. */
|
|
void scr_loop_hosted_io(void) {
|
|
/* Once per loop iteration (the bridge calls this at most once per turn):
|
|
* the poll phase is open again, as after the pass's check phase. */
|
|
scr_loop_io_open = true;
|
|
scr_loop_io_batches = 0;
|
|
while (scr_fs_renames_pending() && scr_fs_renames_dispatch()) scr_hosted_macrotask_done();
|
|
while (scr_crypto_bytes_pending() && scr_crypto_bytes_dispatch()) scr_hosted_macrotask_done();
|
|
while (scr_loop_deferred_dispatch()) scr_hosted_macrotask_done();
|
|
if (scr_ffi_dispatch_fn != NULL && scr_ffi_dispatch_fn()) scr_hosted_macrotask_done();
|
|
if (scr_stream_dispatch_fn != NULL) {
|
|
scr_stream_dispatch_fn();
|
|
scr_hosted_macrotask_done();
|
|
}
|
|
scr_workers_dispatch();
|
|
scr_hosted_macrotask_done();
|
|
if (scr_events_dispatch_fn != NULL) {
|
|
scr_events_dispatch_fn();
|
|
scr_hosted_macrotask_done();
|
|
}
|
|
if (scr_net_dispatch_fn != NULL) {
|
|
scr_net_dispatch_fn();
|
|
scr_hosted_macrotask_done();
|
|
}
|
|
if (scr_dgram_dispatch_fn != NULL) {
|
|
scr_dgram_dispatch_fn();
|
|
scr_hosted_macrotask_done();
|
|
}
|
|
if (scr_watch_dispatch_fn != NULL) {
|
|
scr_watch_dispatch_fn();
|
|
scr_hosted_macrotask_done();
|
|
}
|
|
if (scr_loop_children != NULL && scr_loop_children->pending()) {
|
|
#if !defined(_WIN32) && !defined(__wasi__)
|
|
(void)scr_loop_children->wait(0);
|
|
#endif
|
|
scr_loop_children->poll();
|
|
scr_hosted_macrotask_done();
|
|
}
|
|
}
|
|
|
|
/* ── the switch ── */
|
|
|
|
void scr_loop_host(const ScrNodeBridge *bridge, double since_ms) {
|
|
if (scr_loop_hosted != NULL) return;
|
|
scr_loop_hosted = bridge;
|
|
scr_hosted_continue_turn = scr_pass_phase == SCR_PASS_IO || scr_pass_phase == SCR_PASS_TIMERS;
|
|
scr_hosted_microtasks_first = scr_in_microtask;
|
|
bridge->host_loop();
|
|
/* Promise jobs, FIFO. */
|
|
while (scr_ready_len > 0) {
|
|
ScrFiber *f = scr_ready[scr_ready_head++];
|
|
scr_ready_len--;
|
|
scr_hosted_microtask(f);
|
|
}
|
|
scr_ready_head = 0;
|
|
/* Ticks, FIFO. */
|
|
ScrNtick *t = scr_nt_head;
|
|
scr_nt_head = scr_nt_tail = NULL;
|
|
while (t != NULL) {
|
|
ScrNtick *next = t->next;
|
|
t->next = NULL;
|
|
scr_hosted_tick(t);
|
|
t = next;
|
|
}
|
|
/* Immediates in queue order, keeping their handles. Those of a check
|
|
* phase the switch interrupted finish it before Node's loop starts. */
|
|
for (size_t i = scr_immediates_head; i < scr_nimmediates; i++) {
|
|
ScrImmediate *im = &scr_immediates[i];
|
|
if (im->cb == NULL) continue;
|
|
ScrClosure *cb = im->cb;
|
|
bool reffed = im->reffed;
|
|
unsigned long id = im->id;
|
|
im->cb = NULL;
|
|
uint32_t token = scr_hjob_new(SCR_HJ_IMMEDIATE, cb);
|
|
scr_hjobs[token - 1].id = id;
|
|
scr_hjobs[token - 1].reffed = reffed;
|
|
scr_hmap_put(id * 2 + 1, token);
|
|
if (i < scr_check_end) {
|
|
scr_hosted_due_push(token);
|
|
continue;
|
|
}
|
|
uint32_t handle = bridge->immediate(reffed ? 1 : 0, token);
|
|
scr_hjobs[token - 1].handle = handle;
|
|
}
|
|
scr_check_end = 0;
|
|
/* Timers in (deadline, sequence) order; those already due when Node
|
|
* began loading run before Node's loop starts, as the interrupted pass
|
|
* would have run them. */
|
|
while (scr_ntimers > 0) {
|
|
ScrTimer tm = scr_timer_pop();
|
|
scr_hosted_timer_arm(tm.cb, scr_hosted_delay(tm.deadline_ms - since_ms), tm.repeat_ms,
|
|
tm.delay_ms, tm.id, tm.reffed, tm.deadline_ms <= since_ms);
|
|
}
|
|
scr_reffed_timers = 0;
|
|
scr_nimmediates = scr_immediates_head = 0;
|
|
scr_pending_immediates = scr_reffed_immediates = 0;
|
|
}
|
|
|
|
bool scr_loop_is_hosted(void) { return scr_loop_hosted != NULL; }
|
|
|
|
static void scr_loop_run_hosted(bool first_checkpoint) {
|
|
scr_loop_hosted->checkpoint(first_checkpoint || scr_hosted_microtasks_first ? 1 : 0);
|
|
/* The interrupted phase's work, each followed by Node's checkpoint. A
|
|
* due entry cleared meanwhile is gone (or its token reused, not due). */
|
|
for (size_t i = 0; i < scr_hosted_due_len; i++) {
|
|
uint32_t token = scr_hosted_due[i];
|
|
if (token == 0 || token > scr_hjobs_len || !scr_hjobs[token - 1].due) continue;
|
|
uint8_t kind = scr_hjobs[token - 1].kind;
|
|
if (kind == SCR_HJ_IMMEDIATE) scr_hjobs[token - 1].due = false;
|
|
scr_loop_hosted_run(kind, token);
|
|
scr_loop_hosted->checkpoint(0);
|
|
}
|
|
free(scr_hosted_due);
|
|
scr_hosted_due = NULL;
|
|
scr_hosted_due_len = scr_hosted_due_cap = 0;
|
|
/* process.exitCode is one property in Node; island code's assignment
|
|
* becomes the program's code when the loop ends. */
|
|
int code = scr_loop_hosted->run_loop(scr_hosted_continue_turn ? 1 : 0);
|
|
if (code != 0) scr_process_set_exit_code_tracked((double)code);
|
|
}
|
|
#endif /* SCR_NODE_ISLANDS */
|
|
|
|
/* The process.nextTick queue to exhaustion, ticks queued meanwhile
|
|
* included. false: a tick threw (uncaught, pending). */
|
|
static bool scr_loop_run_ticks(void) {
|
|
while (scr_nt_head != NULL) {
|
|
ScrNtick *t = scr_nt_head;
|
|
scr_nt_head = t->next;
|
|
if (scr_nt_head == NULL) scr_nt_tail = NULL;
|
|
ScrClosure *cb = t->cb;
|
|
ScrFsRenameFn error_cb = t->error_cb;
|
|
void (*raw)(void) = t->raw;
|
|
free(t);
|
|
if (cb) {
|
|
if (error_cb) error_cb(cb, NULL);
|
|
else ((void (*)(ScrClosure *))cb->fn)(cb);
|
|
scr_closure_release(cb);
|
|
} else {
|
|
raw(); /* one stream tick, FIFO with the user ticks around it */
|
|
}
|
|
if (scr_exc_pending()) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* The checkpoint after a macrotask callback (processTicksAndRejections):
|
|
* ticks to exhaustion, then promise jobs to exhaustion, and back while
|
|
* either has work. false: a tick threw (uncaught, pending). */
|
|
static bool scr_loop_run_checkpoint(void) {
|
|
while (scr_nt_head != NULL || scr_ready_len > 0) {
|
|
if (!scr_loop_run_ticks()) return false;
|
|
while (scr_ready_len > 0) {
|
|
ScrFiber *f = scr_ready[scr_ready_head++];
|
|
scr_ready_len--;
|
|
SCR_RESUME_JOB(f);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) {
|
|
/* The FIRST checkpoint after the synchronous main body runs promise
|
|
* jobs BEFORE the first tick drain: Node's main-module evaluation is
|
|
* itself awaited (the runMain continuation is a microtask queued after
|
|
* the body's own), so microtasks scheduled during the body beat ticks
|
|
* scheduled during the body exactly once, at startup — differentially
|
|
* pinned. Every later checkpoint drains ticks first. */
|
|
bool rejection_failed = false;
|
|
for (;;) {
|
|
#ifdef SCR_NODE_ISLANDS
|
|
/* Node loaded during this turn: its loop takes over (scr_loop_run). */
|
|
if (scr_loop_hosted != NULL) return rejection_failed;
|
|
#endif
|
|
if (top_level != NULL && top_level->state == SCR_PROM_REJECTED) break;
|
|
/* process.nextTick callbacks BEFORE promise jobs (Node's checkpoint
|
|
* order): the tick queue to exhaustion, then the microtask queue to
|
|
* exhaustion, and back while either has work — a microtask's
|
|
* nextTick runs before the turn proceeds, but never preempts the
|
|
* microtask queue mid-drain (V8 drains it fully). A tick's uncaught
|
|
* throw ends the loop like any listener's (main reports it). */
|
|
if (!first_checkpoint && !scr_loop_run_ticks()) return false;
|
|
/* Microtasks to exhaustion (Node: promise jobs before timers). */
|
|
while (scr_ready_len > 0) {
|
|
ScrFiber *f = scr_ready[scr_ready_head++];
|
|
scr_ready_len--;
|
|
SCR_RESUME_JOB(f);
|
|
}
|
|
first_checkpoint = false;
|
|
/* The ESM loader observes a rejected entry evaluation at a promise-job
|
|
* checkpoint and terminates before later ref'd timers or I/O can run.
|
|
* Wait until this checkpoint's ready queue is drained — the root's
|
|
* rejection handler is itself promise-job ordered — then leave through
|
|
* the normal teardown below. A fulfilled root deliberately does not
|
|
* stop the loop. */
|
|
if (top_level != NULL && top_level->state == SCR_PROM_REJECTED) break;
|
|
if (scr_nt_head != NULL) continue;
|
|
/* Node decides unhandled rejections at the END of each complete
|
|
* nextTick/microtask checkpoint, before advancing to timers or I/O.
|
|
* A rejected executable module root wins over OTHER rejections from
|
|
* this same checkpoint (the root check above); rejections from an
|
|
* earlier checkpoint have already delivered here. Handled listeners
|
|
* may enqueue more jobs or ref'd work, so return to the checkpoint
|
|
* head instead of declaring the loop exhausted underneath them. */
|
|
if (scr_report_unhandled_rejections()) {
|
|
rejection_failed = true;
|
|
break;
|
|
}
|
|
if (scr_ready_len > 0 || scr_nt_head != NULL || scr_nunhandled > 0) continue;
|
|
/* Quiescent between turns (microtasks drained, nothing running):
|
|
* collect any cycles the turn left behind. One SCHEDULED pass — almost
|
|
* always a nursery one, sized to the turn's own garbage. A full sweep
|
|
* here would walk the whole live heap every turn, which is precisely
|
|
* what the generations exist to avoid. No-op on an empty buffer. */
|
|
scr_cyc_collect_scheduled();
|
|
/* Completed callback-style worker jobs are delivered on the main
|
|
* runtime thread. A worker may have finished while synchronous user code
|
|
* occupied that thread. Deliver exactly one completion per checkpoint:
|
|
* Node drains nextTicks and microtasks after each native callback before
|
|
* invoking the next ready callback. */
|
|
SCR_PASS_PHASE(SCR_PASS_IO);
|
|
if (scr_fs_renames_pending()) {
|
|
bool dispatched = scr_fs_renames_dispatch();
|
|
if (scr_exc_pending()) return false;
|
|
if (dispatched) continue;
|
|
}
|
|
if (scr_crypto_bytes_pending()) {
|
|
bool dispatched = scr_crypto_bytes_dispatch();
|
|
if (scr_exc_pending()) return false;
|
|
if (dispatched) continue;
|
|
}
|
|
if (scr_loop_deferred_pending()) {
|
|
bool dispatched = scr_loop_deferred_dispatch();
|
|
if (scr_exc_pending()) return false;
|
|
if (dispatched) continue;
|
|
}
|
|
/* Foreign native callbacks are macrotasks. Deliver one, then restart at
|
|
* the microtask checkpoint before considering the next queued post. */
|
|
if (scr_ffi_dispatch_fn != NULL && scr_ffi_dispatch_fn()) {
|
|
if (scr_exc_pending()) return false;
|
|
if (scr_ready_len > 0 || scr_nt_head != NULL || scr_nunhandled > 0) continue;
|
|
}
|
|
/* Stream tick dispatch (scr_stream.c, when linked): the deferred
|
|
* next-tick emissions ('data' flow kicks, 'readable'/'end'/'finish'/
|
|
* 'drain'/'error'/'close') fire now, FIRST — the nextTick station.
|
|
* Listeners may enqueue microtasks or more ticks — restart the turn
|
|
* so those drain first. */
|
|
if (scr_stream_dispatch_fn != NULL) {
|
|
scr_stream_dispatch_fn();
|
|
if (scr_exc_pending()) return false; /* uncaught throw in a listener */
|
|
if (scr_ready_len > 0 || scr_nt_head != NULL) continue;
|
|
if (scr_stream_pending_fn != NULL && scr_stream_pending_fn()) continue;
|
|
}
|
|
scr_workers_dispatch();
|
|
if (scr_exc_pending()) return false;
|
|
if (scr_ready_len > 0 || scr_nt_head != NULL) continue;
|
|
#ifdef SCR_WORKERS
|
|
if (scr_workers_more_fn != NULL && scr_workers_more_fn()) continue;
|
|
#endif
|
|
/* Event dispatch (scr_events.c, when linked): watched signals
|
|
* delivered since the last turn fire their listeners now (macrotasks,
|
|
* like timers), then stdin — while a consumer exists, probe fd 0 and
|
|
* deliver one arrived chunk / the EOF events. Both may enqueue
|
|
* microtasks — restart the turn so those drain first. */
|
|
if (scr_events_dispatch_fn != NULL) {
|
|
scr_events_dispatch_fn();
|
|
if (scr_exc_pending()) return false; /* uncaught throw in a listener */
|
|
if (scr_ready_len > 0 || scr_nt_head != NULL) continue;
|
|
}
|
|
/* Net dispatch (scr_net.c, when linked): accepts, arrived data,
|
|
* connect completions, and the deferred listening/error/close emits
|
|
* fire now (macrotasks, like the events hook). Callbacks may enqueue
|
|
* microtasks — restart the turn so those drain first. */
|
|
if (scr_net_dispatch_fn != NULL) {
|
|
scr_net_dispatch_fn();
|
|
if (scr_exc_pending()) return false; /* uncaught throw in a listener */
|
|
if (scr_ready_len > 0 || scr_nt_head != NULL) continue;
|
|
}
|
|
/* Dgram dispatch (scr_dgram.c, when linked): arrived datagrams and
|
|
* the deferred listening/connect/error/close emits (plus pending
|
|
* dns.lookup callbacks) fire now — the net hook's exact station. */
|
|
if (scr_dgram_dispatch_fn != NULL) {
|
|
scr_dgram_dispatch_fn();
|
|
if (scr_exc_pending()) return false; /* uncaught throw in a listener */
|
|
if (scr_ready_len > 0 || scr_nt_head != NULL) continue;
|
|
}
|
|
/* Watch dispatch (scr_watch.c, when linked): file events queued on
|
|
* the unit's event backend fire their FSWatcher listeners now — the
|
|
* net hook's exact station. */
|
|
if (scr_watch_dispatch_fn != NULL) {
|
|
scr_watch_dispatch_fn();
|
|
if (scr_exc_pending()) return false; /* uncaught throw in a listener */
|
|
if (scr_ready_len > 0 || scr_nt_head != NULL) continue;
|
|
}
|
|
/* Reap spawned children and fire their listeners (before timers,
|
|
* like Node's nextTick-ish spawn-failure "error"). A callback may
|
|
* enqueue microtasks or spawn again — restart the turn if so.
|
|
* EXCEPT when ONLY unref'd children remain: Node's liveness check
|
|
* precedes any further poll/reap, so once nothing reffed holds the
|
|
* loop their pending exits are DROPPED at loop exit — never
|
|
* delivered. Without this gate the kill-then-exit shape (a timer's
|
|
* last act kills an unref'd child) would race machine timing: a
|
|
* death landing before this sweep fired its 'exit' where Node
|
|
* deterministically never does. The teardown below releases the
|
|
* undelivered listeners, so the RC audit stays clean. */
|
|
if (scr_loop_children != NULL && scr_loop_children->pending()) {
|
|
bool held =
|
|
scr_reffed_timers > 0 || scr_reffed_immediates > 0 ||
|
|
scr_loop_children->reffed_pending() ||
|
|
/* spawn failures settle regardless: their 'error' is a
|
|
* next-tick event, delivered even on an unref'd child */
|
|
scr_loop_children->failed_pending() ||
|
|
(scr_events_pending_fn != NULL && scr_events_pending_fn()) ||
|
|
(scr_net_pending_fn != NULL && scr_net_pending_fn()) ||
|
|
(scr_dgram_pending_fn != NULL && scr_dgram_pending_fn()) ||
|
|
(scr_watch_pending_fn != NULL && scr_watch_pending_fn()) ||
|
|
(scr_ffi_pending_fn != NULL && scr_ffi_pending_fn()) ||
|
|
scr_fs_renames_pending() || scr_crypto_bytes_pending() || scr_loop_deferred_pending() ||
|
|
scr_workers_pending()
|
|
;
|
|
if (held) {
|
|
scr_loop_children->poll();
|
|
if (scr_exc_pending()) return false; /* uncaught throw in a listener */
|
|
if (scr_ready_len > 0 || scr_nt_head != NULL) continue;
|
|
}
|
|
}
|
|
/* Deferred stream ticks are always-ready work: never sleep or exit
|
|
* while any exist (the dispatch above drains them, so reaching here
|
|
* with ticks pending means a dispatch station enqueued more). User
|
|
* nextTicks enqueued by a station listener are the same story. */
|
|
if (scr_stream_pending_fn != NULL && scr_stream_pending_fn()) continue;
|
|
if (scr_nt_head != NULL) continue;
|
|
#ifdef SCR_NODE_ISLANDS
|
|
/* Node loaded during this turn: its loop takes over (scr_loop_run). */
|
|
if (scr_loop_hosted != NULL) return rejection_failed;
|
|
#endif
|
|
bool kids = scr_loop_children != NULL && scr_loop_children->pending();
|
|
bool kids_reffed = scr_loop_children != NULL && scr_loop_children->reffed_pending();
|
|
bool events = scr_events_pending_fn != NULL && scr_events_pending_fn();
|
|
bool net = scr_net_pending_fn != NULL && scr_net_pending_fn();
|
|
/* `net` keeps the process alive; `net_io` (unref'd handles too) decides
|
|
* whether the sleep polls the net unit's fd */
|
|
bool net_io = net || (scr_net_io_fn != NULL && scr_net_io_fn());
|
|
bool dgram = scr_dgram_pending_fn != NULL && scr_dgram_pending_fn();
|
|
bool watch = scr_watch_pending_fn != NULL && scr_watch_pending_fn();
|
|
bool ffi = scr_ffi_pending_fn != NULL && scr_ffi_pending_fn();
|
|
bool renames = scr_fs_renames_pending();
|
|
bool crypto = scr_crypto_bytes_pending() || scr_loop_deferred_pending();
|
|
bool workers = scr_workers_pending();
|
|
/* Timer liveness counts only REF'd timers: an unref'd timer stays in
|
|
* the heap (and fires if the loop runs on for other reasons) but does
|
|
* not by itself keep the process alive — Node's unref semantics.
|
|
* Children follow the same rule: an unref'd child is still REAPED
|
|
* while the loop runs (kids drives the sweeps and sleeps above) but
|
|
* only reffed ones keep the process alive. */
|
|
if (scr_reffed_timers == 0 && scr_reffed_immediates == 0 && !kids_reffed && !events && !net && !dgram && !watch && !ffi && !renames && !crypto && !workers) break;
|
|
/* Sleep to the earliest deadline, then run every due timer (each may
|
|
* enqueue microtasks, which the next iteration drains first). Who
|
|
* sleeps depends on what is pending:
|
|
* - external io: the io POLL takes the sleep — it waits on real fds
|
|
* up to the deadline (socket readiness wakes it early) and makes
|
|
* progress (engine jobs, arrived response data) before timers run.
|
|
* A child's exit can't wake curl's fds, so children re-impose the
|
|
* reap-granularity cap on this path.
|
|
* - signals watched or stdin consumed: poll(2) takes the sleep — the
|
|
* wake pipe, fd 0, and the child watch's fd together, so any of
|
|
* them ends it immediately (details at the branch).
|
|
* - children only: the sleep waits on the child watch — the exit
|
|
* wakeup and the timer deadline together via the wait timeout — so a
|
|
* child's exit ends the sleep immediately and the next turn's reap
|
|
* pass fires its listeners. scr_children_wait declines when it
|
|
* can't wake for every pending child; the ~1ms polling cap is the
|
|
* fallback.
|
|
* - timers only: plain nanosleep to the deadline. */
|
|
double now = scr_now_ms();
|
|
double due = scr_ntimers > 0 ? scr_timers[0].deadline_ms : now + SCR_IO_POLL_MS;
|
|
/* The worker pool's wake ends the sleep when a job completes: its pipe
|
|
* joins the poll(2) set on POSIX, and the Windows sleeps wait on its
|
|
* event. Without one (a failed pipe or event, or a child wait that
|
|
* cannot include it) the idle wait is bounded like the portable child
|
|
* fallback so completion is noticed promptly. */
|
|
int renames_fd = renames ? scr_fs_renames_pollfd() : -1;
|
|
bool renames_wait = renames && renames_fd < 0 && scr_fs_renames_can_wait();
|
|
if (renames && renames_fd < 0 && (!renames_wait || kids) && due > now + SCR_CHILD_POLL_MS) {
|
|
due = now + SCR_CHILD_POLL_MS;
|
|
}
|
|
/* Pending immediates are always-ready work: no sleep — run due timers
|
|
* (Node's timers phase precedes check), then the check phase below. */
|
|
if (scr_pending_immediates > 0) due = now;
|
|
/* Queued native callbacks are ready work: no sleep. The next turn
|
|
* delivers the ones queued so far. */
|
|
if (scr_loop_deferred_pending()) {
|
|
due = now;
|
|
scr_loop_deferred_ready = scr_loop_deferred_count;
|
|
}
|
|
/* Child polling can drain a pipe into the IPC queue after its dispatch
|
|
* station has run. Completed sends and disconnects can also become ready
|
|
* during that turn. No fd will wake us for this userspace work: return to
|
|
* dispatch without sleeping, still allowing due timers and immediates. */
|
|
if (scr_loop_children != NULL && scr_loop_children->ready()) due = now;
|
|
#ifdef SCR_WORKERS
|
|
if (scr_workers_ready_fn && scr_workers_ready_fn()) due = now;
|
|
#endif
|
|
/* An idle moment: a full pass the death rule deferred runs now. */
|
|
if (due > now && scr_cyc_collect_idle()) now = scr_now_ms();
|
|
#if !defined(_WIN32) && !defined(__wasi__)
|
|
if (due > now) scr_fiber_stacks_clear();
|
|
#endif
|
|
#ifdef SCR_WORKERS
|
|
if (due > now) scr_sa_loop_waiting(true);
|
|
#endif
|
|
bool evw = scr_events_watching_fn != NULL && scr_events_watching_fn();
|
|
#ifdef SCR_WORKERS
|
|
if (workers && scr_workers_wait_fn && !kids && !evw && !net_io && !dgram && !watch && !ffi && !renames && !crypto) {
|
|
scr_workers_wait_fn(due > now ? due - now : 0);
|
|
now = scr_now_ms();
|
|
} else
|
|
#endif
|
|
if (evw || net_io || dgram || watch || ffi || workers || renames_fd >= 0) {
|
|
#if defined(_WIN32) || defined(__wasi__)
|
|
/* The win32 arm, and WASI hosts whose poll_oneoff adapters do not
|
|
* reliably wake for a closed inherited stdin pipe: the sleep is a capped nanosleep and
|
|
* the next turn's dispatch does the work — signal flags (set by the
|
|
* CRT handler on msvcrt's console-ctrl thread) and the stdin probe
|
|
* at SCR_SIGNAL_POLL_MS granularity (scr_events.c), socket/dgram
|
|
* readiness at the child-reap cap (scr_loop_wsapoll.c's zero-timeout
|
|
* WSAPoll drains at dispatch; ~1ms keeps loopback exchanges inside
|
|
* the granularity Node's IOCP-woken loop delivers, where 50ms could
|
|
* reorder a socket emit past a short timer). When the caps ever
|
|
* show up in a profile, the upgrade is a real waitable arm —
|
|
* WaitForMultipleObjects over WSAEVENTs, or IOCP. */
|
|
if (evw && due > now + SCR_SIGNAL_POLL_MS) due = now + SCR_SIGNAL_POLL_MS;
|
|
if ((net_io || dgram || watch) && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
|
|
if ((ffi || workers) && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
|
|
if (kids && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
|
|
if (due > now) {
|
|
double wait = due - now;
|
|
#ifdef _WIN32
|
|
if (renames_wait) {
|
|
scr_fs_renames_wait(wait);
|
|
} else
|
|
#endif
|
|
{
|
|
struct timespec ts = {(time_t)(wait / 1000.0), (long)((wait - (double)((time_t)(wait / 1000.0)) * 1000.0) * 1e6)};
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
}
|
|
now = scr_now_ms();
|
|
#else
|
|
/* poll(2) takes the sleep: the events unit's fds — the signal wake
|
|
* pipe (a signal delivered BEFORE the poll entered still has its
|
|
* byte in the pipe — no lost-wakeup race) and fd 0 while stdin has
|
|
* a consumer — plus the child watch's fd when it can represent
|
|
* every pending child (kqueue and epoll fds poll readable when
|
|
* events are pending); unrepresentable children keep the ~1ms reap cap
|
|
* instead. Dispatch happens at the next turn's top — the poll only
|
|
* decides how long to sleep. */
|
|
struct pollfd fds[10];
|
|
int nfds = 0;
|
|
int evfds[2];
|
|
int nev = evw && scr_events_pollfds_fn != NULL ? scr_events_pollfds_fn(evfds) : 0;
|
|
for (int i = 0; i < nev; i++) {
|
|
fds[nfds].fd = evfds[i];
|
|
fds[nfds].events = POLLIN;
|
|
fds[nfds++].revents = 0;
|
|
}
|
|
if (net_io) {
|
|
/* The net unit's poller fd: readable while socket events are
|
|
* pending, so arrived data/accepts end the sleep immediately.
|
|
* Dispatch (which also DRAINS the poller) happens at the next
|
|
* turn's top — the poll only decides how long to sleep. */
|
|
int nfd = scr_net_pollfd_fn != NULL ? scr_net_pollfd_fn() : -1;
|
|
if (nfd >= 0) {
|
|
fds[nfds].fd = nfd;
|
|
fds[nfds].events = POLLIN;
|
|
fds[nfds++].revents = 0;
|
|
} else if (due > now + SCR_SIGNAL_POLL_MS) {
|
|
due = now + SCR_SIGNAL_POLL_MS;
|
|
}
|
|
}
|
|
if (dgram) {
|
|
/* The dgram unit's poller fd — the net slot's exact story. */
|
|
int dfd = scr_dgram_pollfd_fn != NULL ? scr_dgram_pollfd_fn() : -1;
|
|
if (dfd >= 0) {
|
|
fds[nfds].fd = dfd;
|
|
fds[nfds].events = POLLIN;
|
|
fds[nfds++].revents = 0;
|
|
} else if (due > now + SCR_SIGNAL_POLL_MS) {
|
|
due = now + SCR_SIGNAL_POLL_MS;
|
|
}
|
|
}
|
|
if (watch) {
|
|
/* The watch unit's event fd — the net slot's exact story. */
|
|
int wfd = scr_watch_pollfd_fn != NULL ? scr_watch_pollfd_fn() : -1;
|
|
if (wfd >= 0) {
|
|
fds[nfds].fd = wfd;
|
|
fds[nfds].events = POLLIN;
|
|
fds[nfds++].revents = 0;
|
|
} else if (due > now + SCR_SIGNAL_POLL_MS) {
|
|
due = now + SCR_SIGNAL_POLL_MS;
|
|
}
|
|
}
|
|
if (ffi) {
|
|
int ffd = scr_ffi_pollfd_fn != NULL ? scr_ffi_pollfd_fn() : -1;
|
|
if (ffd >= 0) {
|
|
fds[nfds].fd = ffd;
|
|
fds[nfds].events = POLLIN;
|
|
fds[nfds++].revents = 0;
|
|
} else if (due > now + SCR_CHILD_POLL_MS) {
|
|
due = now + SCR_CHILD_POLL_MS;
|
|
}
|
|
}
|
|
if (kids) {
|
|
int cfd = scr_loop_children->wake_fd();
|
|
if (cfd >= 0) {
|
|
fds[nfds].fd = cfd;
|
|
fds[nfds].events = POLLIN;
|
|
fds[nfds++].revents = 0;
|
|
} else if (due > now + SCR_CHILD_POLL_MS) {
|
|
due = now + SCR_CHILD_POLL_MS;
|
|
}
|
|
}
|
|
if (renames_fd >= 0) {
|
|
fds[nfds].fd = renames_fd;
|
|
fds[nfds].events = POLLIN;
|
|
fds[nfds++].revents = 0;
|
|
}
|
|
if (workers) {
|
|
int worker_fd = scr_workers_pollfd();
|
|
if (worker_fd >= 0) {
|
|
fds[nfds].fd = worker_fd;
|
|
fds[nfds].events = POLLIN;
|
|
fds[nfds++].revents = 0;
|
|
} else if (due > now + SCR_CHILD_POLL_MS) {
|
|
due = now + SCR_CHILD_POLL_MS;
|
|
}
|
|
}
|
|
double wait = due > now ? due - now : 0;
|
|
if (wait > 1e9) wait = 1e9; /* poll takes int ms */
|
|
(void)poll(fds, (nfds_t)nfds, (int)(wait + 0.999));
|
|
/* Consume queued child-exit events NOW (zero-timeout drain) — an
|
|
* unconsumed event keeps the kqueue fd readable and would turn the
|
|
* next poll into a busy spin while other children still run. The
|
|
* WNOHANG sweep at the next turn's top stays the reaper. */
|
|
if (kids) (void)scr_loop_children->wait(0);
|
|
now = scr_now_ms();
|
|
#endif /* !_WIN32 && !__wasi__ */
|
|
} else if (kids && scr_loop_children->wait(due > now ? due - now : 0)) {
|
|
now = scr_now_ms(); /* woke early on an exit, or hit the deadline */
|
|
} else {
|
|
if (kids && due > now + SCR_CHILD_POLL_MS) due = now + SCR_CHILD_POLL_MS;
|
|
#ifdef _WIN32
|
|
if (renames_wait && due > now) {
|
|
scr_fs_renames_wait(due - now); /* a completion ends the sleep early */
|
|
now = scr_now_ms();
|
|
} else
|
|
#endif
|
|
if (due > now) {
|
|
double wait = due - now;
|
|
struct timespec ts = {(time_t)(wait / 1000.0), (long)((wait - (double)((time_t)(wait / 1000.0)) * 1000.0) * 1e6)};
|
|
nanosleep(&ts, NULL);
|
|
now = due;
|
|
}
|
|
}
|
|
#ifdef SCR_WORKERS
|
|
scr_sa_loop_waiting(false);
|
|
#endif
|
|
if (scr_ntimers > 0 && scr_timers[0].deadline_ms <= now) SCR_PASS_PHASE(SCR_PASS_TIMERS);
|
|
while (scr_ntimers > 0 && scr_timers[0].deadline_ms <= now) {
|
|
ScrTimer t = scr_timer_pop();
|
|
/* Timer callbacks are plain sync closures, run on the main stack.
|
|
* An interval entry is OUT of the heap while its callback runs;
|
|
* clearInterval(its own id) from inside sets the firing flag and
|
|
* the re-arm below drops it instead. A throw kills the interval
|
|
* with the program (Node: the uncaught exception ends the process
|
|
* before any rescheduled tick could run). */
|
|
if (t.id != 0) {
|
|
scr_firing_id = t.id;
|
|
scr_firing_cleared = false;
|
|
scr_firing_reffed = t.reffed; /* unref/ref during the callback lands here */
|
|
scr_firing_refresh = false;
|
|
}
|
|
((void (*)(ScrClosure *))t.cb->fn)(t.cb);
|
|
if (t.id != 0 && t.repeat_ms > 0 && !scr_firing_cleared) {
|
|
/* Re-arm relative to the post-callback clock (libuv's uv_timer
|
|
* repeat behavior: no catch-up bursts after a slow callback). The
|
|
* ref state carries across ticks (a self-unref'd interval stays
|
|
* unref'd). */
|
|
ScrTimer again = {scr_now_ms() + t.repeat_ms, scr_timer_seq++, t.cb, t.repeat_ms, t.id, scr_firing_reffed, t.delay_ms};
|
|
scr_timer_push(again);
|
|
} else if (t.id != 0 && scr_firing_refresh && !scr_firing_cleared) {
|
|
/* refresh() from inside the one-shot's own callback: re-arm to
|
|
* now + the original delay, ref state carried (Node's
|
|
* Timeout.refresh — the timer fires again). */
|
|
ScrTimer again = {scr_now_ms() + t.delay_ms, scr_timer_seq++, t.cb, 0, t.id, scr_firing_reffed, t.delay_ms};
|
|
scr_timer_push(again);
|
|
} else {
|
|
scr_closure_release(t.cb);
|
|
}
|
|
scr_firing_id = 0;
|
|
if (scr_exc_pending()) return false; /* uncaught throw in a callback: main handles it */
|
|
#ifdef SCR_NODE_ISLANDS
|
|
/* Node loaded during this turn: its loop takes over (scr_loop_run). */
|
|
if (scr_loop_hosted != NULL) return rejection_failed;
|
|
#endif
|
|
/* Drain ticks and microtasks, and report the rejections the callback
|
|
* left, before more timers (Node's runNextTicks between timers). */
|
|
if (scr_ready_len > 0 || scr_nt_head != NULL || scr_nunhandled > 0) break;
|
|
}
|
|
/* The check phase: immediates queued BEFORE this phase started run
|
|
* now, FIFO; ones a callback queues wait for the next turn (the end
|
|
* snapshot — Node's once-per-turn rule, so a setImmediate chain can't
|
|
* starve the loop). Cleared slots (cb NULL) skip. Ticks, then
|
|
* microtasks, drain after each callback, like Node's checkpoint after
|
|
* every macrotask; pending ticks or a woken fiber restart the turn
|
|
* first (the timer loop's break above), so the phase only runs on a
|
|
* drained checkpoint. */
|
|
bool check_ran = false;
|
|
if (scr_ready_len == 0 && scr_nt_head == NULL && scr_nunhandled == 0 && scr_pending_immediates > 0) {
|
|
size_t end = scr_nimmediates;
|
|
#ifdef SCR_NODE_ISLANDS
|
|
scr_check_end = end; /* a switch inside this phase finishes it first */
|
|
SCR_PASS_PHASE(SCR_PASS_CHECK);
|
|
#endif
|
|
while (scr_immediates_head < end) {
|
|
size_t i = scr_immediates_head++;
|
|
ScrClosure *cb = scr_immediates[i].cb;
|
|
if (cb == NULL) continue; /* cleared */
|
|
scr_immediates[i].cb = NULL; /* fired: clear/ref/unref on this id no-op now */
|
|
if (scr_pending_immediates > 0) scr_pending_immediates--;
|
|
if (scr_immediates[i].reffed) {
|
|
scr_immediates[i].reffed = false;
|
|
if (scr_reffed_immediates > 0) scr_reffed_immediates--;
|
|
}
|
|
((void (*)(ScrClosure *))cb->fn)(cb);
|
|
scr_closure_release(cb);
|
|
if (scr_exc_pending()) return false; /* uncaught throw: main handles it */
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) return rejection_failed;
|
|
#endif
|
|
if (!scr_loop_run_checkpoint()) return false;
|
|
/* Node's runNextTicks between immediates also reports the
|
|
* rejections the callback left before the next one runs; a
|
|
* listener's jobs drain before the next report. */
|
|
while (scr_nunhandled > 0 && (top_level == NULL || top_level->state != SCR_PROM_REJECTED)) {
|
|
if (scr_report_unhandled_rejections()) return true;
|
|
if (!scr_loop_run_checkpoint()) return false;
|
|
}
|
|
if (top_level != NULL && top_level->state == SCR_PROM_REJECTED) break;
|
|
}
|
|
#ifdef SCR_NODE_ISLANDS
|
|
scr_check_end = 0;
|
|
#endif
|
|
/* Fully drained: reset the cursor so the array reuses its slots. */
|
|
if (scr_immediates_head == scr_nimmediates) {
|
|
scr_immediates_head = 0;
|
|
scr_nimmediates = 0;
|
|
}
|
|
check_ran = true;
|
|
}
|
|
/* The next poll phase opens once this iteration's check phase ran (or
|
|
* had nothing to run). A check phase deferred for pending microtasks
|
|
* keeps it closed, so the next iteration reaches the check phase
|
|
* before polling again. */
|
|
if (check_ran || scr_pending_immediates == 0) {
|
|
scr_loop_io_open = true;
|
|
scr_loop_io_batches = 0;
|
|
}
|
|
}
|
|
return rejection_failed;
|
|
}
|
|
|
|
/* A CommonJS main module runs synchronously (Module._load), so the first
|
|
* checkpoint after it drains process.nextTick callbacks before promise
|
|
* jobs like every later one; only an ES-module main's awaited evaluation
|
|
* lets its body's promise jobs run first. Set by the emitted main before
|
|
* the loop starts. */
|
|
static SCR_TL bool scr_loop_cjs_entry = false;
|
|
void scr_loop_commonjs_entry(void) { scr_loop_cjs_entry = true; }
|
|
|
|
bool scr_loop_run(ScrPromise *top_level) {
|
|
/* The main module's body has returned: an enterWith it made ends here. */
|
|
scr_loop_started = true;
|
|
scr_cyc_note_event_loop();
|
|
if (scr_als_main_slot != NULL) scr_als_main_clear();
|
|
bool first_checkpoint = !scr_loop_cjs_entry;
|
|
bool rejection_failed;
|
|
for (;;) {
|
|
#ifdef SCR_NODE_ISLANDS
|
|
/* Node is loaded (in the main body or a callback of an earlier pass):
|
|
* its loop runs everything from here, the runtime's queues included,
|
|
* and ends the process itself on an uncaught exception or an
|
|
* unhandled rejection. */
|
|
if (scr_loop_hosted != NULL) {
|
|
scr_loop_run_hosted(first_checkpoint);
|
|
rejection_failed = false;
|
|
break;
|
|
}
|
|
#endif
|
|
rejection_failed = scr_loop_run_pass(top_level && !top_level->top_level_handled ? top_level : NULL, first_checkpoint);
|
|
first_checkpoint = false;
|
|
if (scr_exc_pending()) {
|
|
if (scr_exc_handle_uncaught(false)) continue;
|
|
break; /* main reports the original or handler-thrown exception */
|
|
}
|
|
#ifdef SCR_NODE_ISLANDS
|
|
if (scr_loop_hosted != NULL) continue;
|
|
#endif
|
|
if (rejection_failed) break;
|
|
if (top_level && top_level->state == SCR_PROM_REJECTED && !top_level->top_level_handled) {
|
|
scr_promise_rethrow(top_level);
|
|
if (scr_exc_handle_uncaught(true)) {
|
|
top_level->top_level_handled = true;
|
|
scr_prom_observe(top_level);
|
|
continue;
|
|
}
|
|
scr_discard_unhandled_rejections();
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
/* Exit can now leave UNREF'd timers armed in the heap (ordinary
|
|
* exhaustion, a fatal module root, or an unhandled rejection). They
|
|
* never fire, so release their closures here or the RC audit counts
|
|
* them as leaks. Uncaught callback throws still return above for main
|
|
* to report through the existing exceptional teardown path. */
|
|
scr_timers_teardown();
|
|
/* No reffed foreign registration or queued invocation remains at ordinary
|
|
* exhaustion. Disarm posting before exit listeners/global teardown so a
|
|
* post from a straggling native thread is dropped safely. */
|
|
if (scr_ffi_stop_fn != NULL) scr_ffi_stop_fn();
|
|
/* Retained native FFI registrations are deliberately NOT torn down
|
|
* here: process 'exit' listeners run after the loop returns (inline in
|
|
* main, before any atexit handler) and may legitimately release a
|
|
* registration or pump a native library one last time — exactly like
|
|
* the process.exit() path, where the ledger is also still intact. On
|
|
* returns that reach atexit, the sweep scr_ffi_retain registered (LIFO
|
|
* before the RC audit) then drops the ledger's references. It does NOT
|
|
* run on process.exit(): scr_process_exit ends in _Exit, skipping every
|
|
* atexit handler — the sweep and the RC audit alike — and leaves the
|
|
* ledger to the OS. */
|
|
/* Unref'd children the loop never reaped: release the
|
|
* registry's references (their listeners never fire — the process is
|
|
* exiting, Node's behavior; the OS reparents the children). */
|
|
if (scr_loop_children != NULL) scr_loop_children->teardown();
|
|
scr_fibers_abandoned = scr_fibers_live;
|
|
scr_note_abandoned_fibers(scr_fibers_abandoned);
|
|
return rejection_failed;
|
|
}
|
|
|
|
/* ── process.on('unhandledRejection') ─────────────────────────────────
|
|
* Dyn listeners are called per never-observed rejection at the completed
|
|
* nextTick/microtask checkpoint. A registered listener suppresses the
|
|
* default report and exit 1, exactly Node's handled-event contract. */
|
|
|
|
/* The unhandled-rejection LISTENER hook (scr_async_dyn.c installs it at
|
|
* registration — the loop-hook pattern, so listener-free binaries keep
|
|
* their size class): called per never-observed rejection; false = a
|
|
* listener threw (the uncaught crash path). */
|
|
SCR_TL bool (*scr_urj_deliver_fn)(ScrPromise *p) = NULL;
|
|
|
|
/* Unhandled rejections at a completed nextTick/microtask checkpoint:
|
|
* Node prints an error and exits 1 when no listener handles the event.
|
|
* Snapshot the ledger: a listener can reject another promise, but that
|
|
* new rejection belongs to the NEXT checkpoint rather than this report. */
|
|
bool scr_report_unhandled_rejections(void) {
|
|
bool any = false;
|
|
bool crashed = false;
|
|
size_t report_count = scr_nunhandled;
|
|
for (size_t i = 0; i < report_count; i++) {
|
|
ScrPromise *p = scr_maybe_unhandled[i];
|
|
if (p->state == SCR_PROM_REJECTED && !p->rejection_observed && !crashed) {
|
|
if (scr_urj_deliver_fn != NULL) {
|
|
/* Listener dispatch (scr_async_dyn.c installed the hook at
|
|
* registration): the event handles it, like Node — per entry,
|
|
* no report, exit 0. A listener throw is the uncaught crash.
|
|
* reported_unhandled arms the 'rejectionHandled' window: a
|
|
* handler the listener itself attaches fires the sibling event
|
|
* (scr_prom_observe). */
|
|
p->rejection_observed = true;
|
|
p->reported_unhandled = true;
|
|
if (!scr_urj_deliver_fn(p) && !scr_exc_handle_uncaught(false)) crashed = true;
|
|
} else if (scr_uncaught_exception_hook != NULL) {
|
|
scr_promise_rethrow(p);
|
|
if (scr_exc_handle_uncaught(true)) {
|
|
p->rejection_observed = true;
|
|
p->reported_unhandled = true;
|
|
} else {
|
|
crashed = true;
|
|
}
|
|
} else if (!any) {
|
|
any = true;
|
|
#ifdef SCR_WORKERS
|
|
if (scr_context_report_error) {
|
|
scr_promise_rethrow(p);
|
|
scr_context_report_rejection = true;
|
|
scr_exc_print_uncaught();
|
|
scr_context_report_rejection = false;
|
|
scr_promise_release(p);
|
|
continue;
|
|
}
|
|
#endif
|
|
fflush(stdout);
|
|
fputs("Unhandled promise rejection: ", stderr);
|
|
switch (p->payload_kind) {
|
|
case SCR_EXC_F64: {
|
|
char buf[32];
|
|
scr_f64_to_str(p->f64, buf);
|
|
fputs(buf, stderr);
|
|
break;
|
|
}
|
|
case SCR_EXC_BOOL: fputs(p->b ? "true" : "false", stderr); break;
|
|
case SCR_EXC_STR: fputs(((ScrStr *)p->payload)->data, stderr); break;
|
|
case SCR_EXC_OBJ:
|
|
/* Error rejections render "name: message", like the uncaught path. */
|
|
if (scr_error_is(p->payload)) {
|
|
ScrStr *s = scr_error_to_string((ScrError *)p->payload);
|
|
fwrite(s->data, 1, s->len, stderr);
|
|
scr_str_release(s);
|
|
break;
|
|
}
|
|
/* fall through */
|
|
default: fputs("[object]", stderr); break;
|
|
}
|
|
fputc('\n', stderr);
|
|
}
|
|
}
|
|
scr_promise_release(p);
|
|
}
|
|
size_t remaining = scr_nunhandled - report_count;
|
|
if (remaining > 0) {
|
|
memmove(scr_maybe_unhandled, scr_maybe_unhandled + report_count,
|
|
remaining * sizeof *scr_maybe_unhandled);
|
|
}
|
|
scr_nunhandled = remaining;
|
|
if (crashed) {
|
|
scr_exc_print_uncaught();
|
|
return true;
|
|
}
|
|
/* main returns 1 on a reported rejection — the 'exit' listeners (atexit)
|
|
* must see that code, like Node's. */
|
|
if (any) scr_exit_code_note(1);
|
|
return any;
|
|
}
|
|
|
|
/* A fatal executable-module rejection suppresses unrelated rejections
|
|
* created in the SAME checkpoint. Drop their retained ledger references
|
|
* without delivering process events or a competing default report. */
|
|
void scr_discard_unhandled_rejections(void) {
|
|
for (size_t i = 0; i < scr_nunhandled; i++) {
|
|
scr_promise_release(scr_maybe_unhandled[i]);
|
|
}
|
|
scr_nunhandled = 0;
|
|
}
|
|
|
|
/* ── new Promise(executor) ────────────────────────────────────────────── */
|
|
|
|
/* resolve closures: caps[0] is a box whose slot holds the promise (+1). */
|
|
static ScrPromise *scr_resolve_target(ScrClosure *self) {
|
|
return (ScrPromise *)scr_box_get_ref(self->caps[0]);
|
|
}
|
|
static void scr_resolve_thunk_f64(ScrClosure *self, double v) {
|
|
ScrPromise *p = scr_resolve_target(self);
|
|
scr_promise_fulfill_f64(p, v);
|
|
scr_promise_release(p);
|
|
}
|
|
static void scr_resolve_thunk_bool(ScrClosure *self, bool v) {
|
|
ScrPromise *p = scr_resolve_target(self);
|
|
scr_promise_fulfill_bool(p, v);
|
|
scr_promise_release(p);
|
|
}
|
|
static void scr_resolve_thunk_str(ScrClosure *self, ScrStr *v) {
|
|
ScrPromise *p = scr_resolve_target(self);
|
|
scr_promise_fulfill_str(p, v); /* callee owns its params: moves in */
|
|
scr_promise_release(p);
|
|
}
|
|
static void scr_resolve_thunk_void(ScrClosure *self) {
|
|
ScrPromise *p = scr_resolve_target(self);
|
|
scr_promise_fulfill_void(p);
|
|
scr_promise_release(p);
|
|
}
|
|
/* Ref payloads: emitted code passes the concrete retain/release via a
|
|
* second box slot? Keep simple: ref resolve thunks are monomorphized by the
|
|
* EMITTER (it knows the arm type) — it emits a tiny static thunk calling
|
|
* scr_promise_fulfill_ref with the concrete helpers, wrapped over caps[0].
|
|
* The runtime provides only the scalar/str/void thunks. */
|
|
|
|
ScrClosure *scr_make_resolve(ScrPromise *p, int kind /*0 f64,1 bool,2 str,3 void*/) {
|
|
void *fns[4] = {(void *)&scr_resolve_thunk_f64, (void *)&scr_resolve_thunk_bool,
|
|
(void *)&scr_resolve_thunk_str, (void *)&scr_resolve_thunk_void};
|
|
ScrClosure *c = scr_closure_new(fns[kind], 1);
|
|
ScrBox *b = scr_box_new_obj(scr_promise_retain_v, scr_promise_release_v,
|
|
scr_promise_trace_v);
|
|
scr_box_set_ref(b, scr_promise_retain(p));
|
|
c->caps[0] = b;
|
|
return c;
|
|
}
|
|
|
|
/* Emitted ref-kind resolve thunks call this. */
|
|
void scr_resolve_ref_impl(ScrClosure *self, void *v, void *(*retain)(void *), void (*release)(void *), ScrTraceFn trace) {
|
|
ScrPromise *p = scr_resolve_target(self);
|
|
scr_promise_fulfill_ref(p, v, retain, release, trace);
|
|
scr_promise_release(p);
|
|
}
|
|
|
|
/* ── emitter support ──────────────────────────────────────────────────── */
|
|
|
|
ScrPromise *scr_fiber_promise(ScrFiber *f) { return f->promise; }
|
|
|
|
/* Generalized resolve construction: `fn` is an emitted thunk with closure
|
|
* calling convention (self, value) that forwards to scr_resolve_ref_impl
|
|
* with the concrete RC helpers. */
|
|
ScrClosure *scr_make_resolve_fn(ScrPromise *p, void *fn) {
|
|
ScrClosure *c = scr_closure_new(fn, 1);
|
|
ScrBox *b = scr_box_new_obj(scr_promise_retain_v, scr_promise_release_v,
|
|
scr_promise_trace_v);
|
|
scr_box_set_ref(b, scr_promise_retain(p));
|
|
c->caps[0] = b;
|
|
return c;
|
|
}
|
|
|
|
/* ── reject closures (new Promise((resolve, reject) => ...)) ──────────
|
|
* Mirror of the resolve closures: caps[0] is a box whose slot holds the
|
|
* promise (+1). The reason is always an Error-hierarchy instance (the
|
|
* frontend pins the reject parameter to `(reason: Error) => void`), and it
|
|
* stores as SCR_EXC_OBJ — exactly the thrown-Error representation — so an
|
|
* await rethrows it, catch-side instanceof answers, and the uncaught
|
|
* printer prints "name: message". First settle wins, exactly JS: reject
|
|
* after any settle (resolve-then-reject, double reject) releases the
|
|
* reason and does nothing. The closure calling convention makes the thunk
|
|
* own its param, like every compiled callee. */
|
|
static void scr_reject_thunk(ScrClosure *self, ScrError *reason) {
|
|
ScrPromise *p = scr_resolve_target(self);
|
|
if (p->state != SCR_PROM_PENDING) {
|
|
scr_error_release(reason);
|
|
} else {
|
|
p->state = SCR_PROM_REJECTED;
|
|
p->payload_kind = SCR_EXC_OBJ;
|
|
p->payload = reason; /* ownership moves in */
|
|
p->retain_fn = scr_error_retain_v;
|
|
p->release_fn = scr_error_release_v;
|
|
p->trace_fn = scr_error_trace_arg();
|
|
scr_promise_settle_wake(p);
|
|
}
|
|
scr_promise_release(p);
|
|
}
|
|
|
|
ScrClosure *scr_make_reject(ScrPromise *p) {
|
|
ScrClosure *c = scr_closure_new((void *)&scr_reject_thunk, 1);
|
|
ScrBox *b = scr_box_new_obj(scr_promise_retain_v, scr_promise_release_v,
|
|
scr_promise_trace_v);
|
|
scr_box_set_ref(b, scr_promise_retain(p));
|
|
c->caps[0] = b;
|
|
return c;
|
|
}
|
|
|
|
/* Shared executor epilogue: an escaping throw rejects a still-pending
|
|
* promise (payload moved out of the active exception cell); a throw after
|
|
* the promise settled is swallowed, like resolve-then-throw in JS. */
|
|
static void scr_executor_check_throw(ScrPromise *p) {
|
|
if (!scr_exc_pending()) return;
|
|
if (p->state == SCR_PROM_PENDING) {
|
|
scr_promise_reject_from_cell(p, scr_exc_current_cell());
|
|
scr_promise_settle_wake(p);
|
|
} else {
|
|
scr_exc_clear();
|
|
}
|
|
}
|
|
|
|
/* Runs a `new Promise(executor)` executor synchronously; an escaping throw
|
|
* rejects the promise (JS-exact). Borrows `p` and `exec`; `resolve`
|
|
* ownership moves INTO the executor call (compiled callees own and finally
|
|
* release their params). */
|
|
void scr_promise_run_executor(ScrPromise *p, ScrClosure *exec, ScrClosure *resolve) {
|
|
((void (*)(ScrClosure *, ScrClosure *))exec->fn)(exec, resolve);
|
|
scr_executor_check_throw(p);
|
|
}
|
|
|
|
/* Zero-param executor (`new Promise(() => {})`): nothing can ever resolve —
|
|
* a deliberately-forever-pending promise. Borrows both. */
|
|
void scr_promise_run_executor0(ScrPromise *p, ScrClosure *exec) {
|
|
((void (*)(ScrClosure *))exec->fn)(exec);
|
|
scr_executor_check_throw(p);
|
|
}
|
|
|
|
/* Two-param executor (`new Promise((resolve, reject) => ...)`): same as
|
|
* scr_promise_run_executor with the reject closure's +1 also moving into
|
|
* the call. */
|
|
void scr_promise_run_executor2(ScrPromise *p, ScrClosure *exec, ScrClosure *resolve,
|
|
ScrClosure *reject) {
|
|
((void (*)(ScrClosure *, ScrClosure *, ScrClosure *))exec->fn)(exec, resolve, reject);
|
|
scr_executor_check_throw(p);
|
|
}
|
|
|
|
/* ── sync generators (function*) ──────────────────────────────────────
|
|
* Contract in scr_runtime.h. The fiber machinery above is the engine:
|
|
* scr_switch is a synchronous coroutine hop, so generators need no event
|
|
* loop — .next() from the main stack (or any fiber) blocks until the
|
|
* yield. */
|
|
|
|
enum {
|
|
SCR_GEN_UNSTARTED = 0,
|
|
SCR_GEN_SUSPENDED,
|
|
SCR_GEN_AWAITING,
|
|
SCR_GEN_RUNNING,
|
|
SCR_GEN_DONE
|
|
};
|
|
|
|
/* One type-erased value slot: the exception cell's payload technique,
|
|
* with only the release entry point (takes MOVE — nothing here retains). */
|
|
typedef struct {
|
|
ScrExcKind kind; /* NONE / F64 / BOOL / REF (strings ride REF) */
|
|
double f64;
|
|
bool b;
|
|
void *payload;
|
|
void (*release_fn)(void *);
|
|
} ScrGenSlot;
|
|
|
|
enum { SCR_AG_NEXT = 0, SCR_AG_RETURN, SCR_AG_THROW };
|
|
|
|
typedef struct ScrAsyncGenRequest {
|
|
int mode;
|
|
ScrGenSlot arg;
|
|
ScrExcCell thrown;
|
|
ScrPromise *promise; /* owned until this request settles */
|
|
struct ScrAsyncGenRequest *next;
|
|
} ScrAsyncGenRequest;
|
|
|
|
static void scr_gen_slot_reset(ScrGenSlot *s) {
|
|
if (s->kind == SCR_EXC_REF && s->payload != NULL) s->release_fn(s->payload);
|
|
s->kind = SCR_EXC_NONE;
|
|
s->payload = NULL;
|
|
s->release_fn = NULL;
|
|
}
|
|
|
|
static void scr_gen_exc_reset(ScrExcCell *cell) {
|
|
if (cell->kind == SCR_EXC_STR) {
|
|
scr_str_release((ScrStr *)cell->payload);
|
|
} else if (cell->kind == SCR_EXC_REF || cell->kind == SCR_EXC_OBJ ||
|
|
cell->kind == SCR_EXC_PRIMITIVE_REF) {
|
|
cell->release_fn(cell->payload);
|
|
}
|
|
ScrStackFrame *stack = cell->stack;
|
|
memset(cell, 0, sizeof *cell);
|
|
cell->stack = stack;
|
|
}
|
|
|
|
static void scr_gen_exc_move(ScrExcCell *dst, ScrExcCell *src) {
|
|
scr_gen_exc_reset(dst);
|
|
ScrStackFrame *dst_stack = dst->stack;
|
|
ScrStackFrame *src_stack = src->stack;
|
|
*dst = *src;
|
|
dst->stack = dst_stack;
|
|
memset(src, 0, sizeof *src);
|
|
src->stack = src_stack;
|
|
scr_exc_cell_changed();
|
|
}
|
|
|
|
struct ScrGen {
|
|
size_t rc;
|
|
ScrDyn *receiver;
|
|
ScrFiber *fiber; /* NULL once torn down (done, or unstarted release) */
|
|
int state;
|
|
ScrGenSlot out; /* yielded value / completion value */
|
|
ScrGenSlot in; /* the .next(v) argument */
|
|
ScrGenSlot ret; /* a parked .return(v) value */
|
|
/* The never-started teardown: drops the packed (already-retained)
|
|
* arguments the spawn wrapper built. Emitted per generator function. */
|
|
void (*drop_args)(void *);
|
|
/* scr_gen_set_unstarted_return's hook; NULL for most generators. */
|
|
void (*unstarted_return)(void *);
|
|
const char *tag; /* scr_gen_set_tag's; NULL is the default */
|
|
bool is_async;
|
|
void (*settle_async)(ScrGen *, ScrPromise *);
|
|
ScrAsyncGenRequest *requests_head;
|
|
ScrAsyncGenRequest *requests_tail;
|
|
ScrPromise *active_request;
|
|
ScrExcCell async_error;
|
|
bool settle_queued;
|
|
bool success_hop_done;
|
|
/* A return() request at a yield: the queued job resumes the body after
|
|
* the return value's await (return_queued), and the body's completion,
|
|
* on that request or a later one, settles without another turn
|
|
* (return_awaited). An explicit `return x` during the unwind clears it,
|
|
* since that return takes its own turn to await x. */
|
|
bool return_queued;
|
|
bool return_awaited;
|
|
#ifdef SCR_WORKERS
|
|
bool context_abandoned;
|
|
#endif
|
|
};
|
|
|
|
/* The LLVM emitter stores number and boolean yields into OUT and takes
|
|
* them back out inline, through %ScrGenHead { rc, receiver, fiber, state,
|
|
* out } and %ScrGenSlot { kind, f64, b, payload, release_fn }. */
|
|
struct ScrGenHeadMirror {
|
|
size_t rc;
|
|
ScrDyn *receiver;
|
|
ScrFiber *fiber;
|
|
int state;
|
|
ScrGenSlot out;
|
|
};
|
|
_Static_assert(offsetof(ScrGen, fiber) == offsetof(struct ScrGenHeadMirror, fiber) &&
|
|
offsetof(ScrGen, state) == offsetof(struct ScrGenHeadMirror, state) &&
|
|
offsetof(ScrGen, out) == offsetof(struct ScrGenHeadMirror, out),
|
|
"the LLVM emitter's %ScrGenHead");
|
|
_Static_assert(offsetof(ScrGenSlot, kind) == 0 && sizeof(ScrExcKind) == 4 &&
|
|
offsetof(ScrGenSlot, f64) == 8 && offsetof(ScrGenSlot, b) == 16 &&
|
|
sizeof(bool) == 1 && SCR_EXC_NONE == 0 && SCR_EXC_F64 == 1 &&
|
|
SCR_EXC_BOOL == 2,
|
|
"the LLVM emitter's %ScrGenSlot and its number/boolean tags");
|
|
|
|
static ScrGen *scr_gen_new_common(void (*entry)(ScrFiber *, void *), void *argpack,
|
|
void (*drop_args)(void *),
|
|
void (*settle_async)(ScrGen *, ScrPromise *),
|
|
bool stackless) {
|
|
ScrGen *g = scr_mem_calloc(sizeof *g);
|
|
if (!g) scr_oom();
|
|
g->rc = 1;
|
|
g->receiver = scr_dyn_this_get();
|
|
g->state = SCR_GEN_UNSTARTED;
|
|
g->drop_args = drop_args;
|
|
g->is_async = settle_async != NULL;
|
|
g->settle_async = settle_async;
|
|
scr_obj_alloc_note();
|
|
|
|
ScrFiber *f = scr_mem_calloc(sizeof *f);
|
|
if (!f) scr_oom();
|
|
f->gen = g;
|
|
scr_fiber_track(f);
|
|
f->entry = entry;
|
|
f->argpack = argpack; /* owned by the fiber start; drop_args if never started */
|
|
/* AsyncLocalStorage: generator bodies run in their CREATOR's context
|
|
* (the spawn-inheritance stance; resumes swap it active like any
|
|
* fiber switch). */
|
|
f->als = scr_als_ctx_retain(*SCR_ALS_SLOT());
|
|
scr_fibers_live++;
|
|
f->stackless = stackless;
|
|
#ifdef _WIN32
|
|
if (!stackless) {
|
|
f->ctx = CreateFiberEx(4096, 0, 0, scr_trampoline, NULL);
|
|
if (f->ctx == NULL) scr_oom();
|
|
}
|
|
#elif defined(__wasi__)
|
|
f->ctx = 0;
|
|
#else
|
|
if (!stackless) scr_fiber_context_init(f);
|
|
#endif
|
|
g->fiber = f;
|
|
return g;
|
|
}
|
|
|
|
ScrGen *scr_gen_new(void (*entry)(ScrFiber *, void *), void *argpack,
|
|
void (*drop_args)(void *)) {
|
|
return scr_gen_new_common(entry, argpack, drop_args, NULL, false);
|
|
}
|
|
|
|
ScrGen *scr_gen_new_stackless(void (*entry)(ScrFiber *, void *), void *argpack,
|
|
void (*drop_args)(void *)) {
|
|
return scr_gen_new_common(entry, argpack, drop_args, NULL, true);
|
|
}
|
|
|
|
ScrGen *scr_async_gen_new(void (*entry)(ScrFiber *, void *), void *argpack,
|
|
void (*drop_args)(void *),
|
|
void (*settle)(ScrGen *, ScrPromise *)) {
|
|
return scr_gen_new_common(entry, argpack, drop_args, settle, false);
|
|
}
|
|
|
|
ScrGen *scr_gen_retain(ScrGen *g) {
|
|
if (g) g->rc++;
|
|
return g;
|
|
}
|
|
|
|
void scr_gen_release(ScrGen *g) {
|
|
if (!g || --g->rc != 0) return;
|
|
#ifdef SCR_WORKERS
|
|
if (g->fiber && g->state != SCR_GEN_UNSTARTED && g->state != SCR_GEN_DONE) {
|
|
/* A dropped suspended generator cannot run user finalizers. Keep its
|
|
* native frame valid until context shutdown can unwind owned locals
|
|
* through the noncatchable stop path. */
|
|
g->rc = 1;
|
|
g->context_abandoned = true;
|
|
return;
|
|
}
|
|
#endif
|
|
scr_dyn_release(g->receiver);
|
|
scr_gen_slot_reset(&g->out);
|
|
scr_gen_slot_reset(&g->in);
|
|
scr_gen_slot_reset(&g->ret);
|
|
scr_gen_exc_reset(&g->async_error);
|
|
if (g->fiber != NULL) {
|
|
if (g->state == SCR_GEN_UNSTARTED) {
|
|
/* Never ran: nothing on the stack owns anything — clean teardown.
|
|
* The packed arguments (+1 each) drop through the emitted helper. */
|
|
if (g->drop_args != NULL) g->drop_args(g->fiber->argpack);
|
|
else free(g->fiber->argpack);
|
|
scr_fiber_destroy(g->fiber);
|
|
scr_fibers_live--;
|
|
} else {
|
|
/* Suspended: the stack holds live locals — unwinding would run user
|
|
* finally blocks Node's GC never runs, so the fiber is deliberately
|
|
* ABANDONED (it stays in the live count; a program that drops a
|
|
* suspended generator gets the abandoned-fiber audit note, the
|
|
* loop-exhaustion story). The fiber's back-pointer clears so a
|
|
* dangling resume can never reach the freed ScrGen. */
|
|
g->fiber->gen = NULL;
|
|
#ifdef SCR_ASAN_FIBERS
|
|
scr_fiber_keep_abandoned(g->fiber);
|
|
#endif
|
|
}
|
|
}
|
|
scr_obj_free_note();
|
|
scr_mem_free(g);
|
|
}
|
|
|
|
void *scr_gen_retain_v(void *g) { return scr_gen_retain((ScrGen *)g); }
|
|
void scr_gen_release_v(void *g) { scr_gen_release((ScrGen *)g); }
|
|
|
|
bool scr_gen_done(ScrGen *g) { return g->state == SCR_GEN_DONE; }
|
|
ScrGen *scr_gen_of_fiber(ScrFiber *f) { return f->gen; }
|
|
ScrDyn *scr_gen_receiver(void) {
|
|
return scr_dyn_retain(scr_current && scr_current->gen ? scr_current->gen->receiver : scr_dyn_undefined());
|
|
}
|
|
|
|
/* Slot setters (release the previous occupant; payloads MOVE in). */
|
|
static void scr_gen_slot_f64(ScrGenSlot *s, double v) {
|
|
scr_gen_slot_reset(s);
|
|
s->kind = SCR_EXC_F64;
|
|
s->f64 = v;
|
|
}
|
|
static void scr_gen_slot_bool(ScrGenSlot *s, bool v) {
|
|
scr_gen_slot_reset(s);
|
|
s->kind = SCR_EXC_BOOL;
|
|
s->b = v;
|
|
}
|
|
static void scr_gen_slot_ref(ScrGenSlot *s, void *v, void (*release)(void *)) {
|
|
scr_gen_slot_reset(s);
|
|
s->kind = SCR_EXC_REF;
|
|
s->payload = v;
|
|
s->release_fn = release;
|
|
}
|
|
|
|
void scr_gen_in_f64(ScrGen *g, double v) { scr_gen_slot_f64(&g->in, v); }
|
|
void scr_gen_in_bool(ScrGen *g, bool v) { scr_gen_slot_bool(&g->in, v); }
|
|
void scr_gen_in_ref(ScrGen *g, void *v, void (*release)(void *)) {
|
|
scr_gen_slot_ref(&g->in, v, release);
|
|
}
|
|
void scr_gen_in_none(ScrGen *g) { scr_gen_slot_reset(&g->in); }
|
|
|
|
void scr_gen_ret_f64(ScrGen *g, double v) { scr_gen_slot_f64(&g->ret, v); }
|
|
void scr_gen_ret_bool(ScrGen *g, bool v) { scr_gen_slot_bool(&g->ret, v); }
|
|
void scr_gen_ret_ref(ScrGen *g, void *v, void (*release)(void *)) {
|
|
scr_gen_slot_ref(&g->ret, v, release);
|
|
}
|
|
void scr_gen_ret_none(ScrGen *g) { scr_gen_slot_reset(&g->ret); }
|
|
|
|
void scr_gen_out_f64(ScrGen *g, double v) { scr_gen_slot_f64(&g->out, v); }
|
|
void scr_gen_out_bool(ScrGen *g, bool v) { scr_gen_slot_bool(&g->out, v); }
|
|
void scr_gen_out_ref(ScrGen *g, void *v, void (*release)(void *)) {
|
|
scr_gen_slot_ref(&g->out, v, release);
|
|
}
|
|
|
|
/* Takes MOVE the slot's value out (the slot resets — a done generator's
|
|
* later resumes answer NONE, JS's undefined). */
|
|
static double scr_gen_slot_take_f64(ScrGenSlot *s) {
|
|
double v = s->kind == SCR_EXC_F64 ? s->f64 : 0;
|
|
s->kind = SCR_EXC_NONE;
|
|
return v;
|
|
}
|
|
static bool scr_gen_slot_take_bool(ScrGenSlot *s) {
|
|
bool v = s->kind == SCR_EXC_BOOL ? s->b : false;
|
|
s->kind = SCR_EXC_NONE;
|
|
return v;
|
|
}
|
|
static void *scr_gen_slot_take_ref(ScrGenSlot *s) {
|
|
void *v = s->kind == SCR_EXC_REF ? s->payload : NULL;
|
|
s->kind = SCR_EXC_NONE;
|
|
s->payload = NULL;
|
|
s->release_fn = NULL;
|
|
return v;
|
|
}
|
|
|
|
bool scr_gen_out_has(ScrGen *g) { return g->out.kind != SCR_EXC_NONE; }
|
|
double scr_gen_take_out_f64(ScrGen *g) { return scr_gen_slot_take_f64(&g->out); }
|
|
bool scr_gen_take_out_bool(ScrGen *g) { return scr_gen_slot_take_bool(&g->out); }
|
|
void *scr_gen_take_out_ref(ScrGen *g) { return scr_gen_slot_take_ref(&g->out); }
|
|
|
|
/* The running fiber's generator — the body-side helpers' anchor. */
|
|
static ScrGen *scr_gen_self(void) {
|
|
ScrGen *g = scr_current != NULL ? scr_current->gen : NULL;
|
|
if (g == NULL) {
|
|
fputs("scriptc: internal error: yield outside a generator\n", stderr);
|
|
abort();
|
|
}
|
|
return g;
|
|
}
|
|
|
|
double scr_gen_take_in_f64(void) { return scr_gen_slot_take_f64(&scr_gen_self()->in); }
|
|
bool scr_gen_take_in_bool(void) { return scr_gen_slot_take_bool(&scr_gen_self()->in); }
|
|
void *scr_gen_take_in_ref(void) { return scr_gen_slot_take_ref(&scr_gen_self()->in); }
|
|
ScrDyn *scr_gen_take_in_dyn(void) {
|
|
ScrGenSlot *in = &scr_gen_self()->in;
|
|
if (in->kind == SCR_EXC_NONE) return scr_dyn_retain(scr_dyn_undefined());
|
|
return scr_gen_slot_take_ref(in);
|
|
}
|
|
|
|
ScrDyn *scr_gen_delegate_resume(ScrDyn *(*next_ref)(void *),
|
|
ScrDyn *(*return_ref)(void *),
|
|
ScrDyn *(*caught_ref)(const ScrCaught *)) {
|
|
ScrGen *g = scr_gen_self();
|
|
ScrExcCell *cell = scr_exc_current_cell();
|
|
int mode = cell->kind == SCR_EXC_GENRET ? 1 : cell->kind != SCR_EXC_NONE ? 2 : 0;
|
|
ScrDyn *value;
|
|
if (mode == 2) {
|
|
ScrCaught *caught = scr_exc_take();
|
|
value = caught_ref(caught);
|
|
scr_caught_release(caught);
|
|
} else {
|
|
if (mode == 1) scr_exc_clear();
|
|
ScrGenSlot *slot = mode == 1 ? &g->ret : &g->in;
|
|
ScrDyn *(*convert)(void *) = mode == 1 ? return_ref : next_ref;
|
|
switch (slot->kind) {
|
|
case SCR_EXC_F64: value = scr_dyn_new_num(slot->f64); break;
|
|
case SCR_EXC_BOOL: value = scr_dyn_new_bool(slot->b); break;
|
|
case SCR_EXC_REF: value = convert(slot->payload); break;
|
|
default: value = scr_dyn_retain(scr_dyn_undefined()); break;
|
|
}
|
|
scr_gen_slot_reset(slot);
|
|
}
|
|
if (scr_exc_pending()) {
|
|
scr_dyn_release(value);
|
|
return NULL;
|
|
}
|
|
ScrDyn *result = scr_dyn_new_obj();
|
|
ScrDyn *kind = scr_dyn_new_num(mode);
|
|
scr_dyn_obj_set(result, "kind", 4, kind);
|
|
scr_dyn_obj_set(result, "value", 5, value);
|
|
/* Object insertion takes ownership of both completion fields. */
|
|
return result;
|
|
}
|
|
|
|
/* yield* into a sync generator delegate. A channel transfer moves one
|
|
* slot into another: MOVE as is (same channel type), DYN boxed into a dyn
|
|
* channel, STRICT only when empty (any value would change type: the
|
|
* SC1071 fence), DROP onto an undefined channel. A return(v) whose value
|
|
* the delegate's return channel cannot hold keeps v parked in the
|
|
* delegating body (STRICT in the return position): the delegate returns
|
|
* without a value and v becomes the body's completion unless the
|
|
* delegate's finally returns its own. */
|
|
enum { SCR_GEN_XFER_MOVE, SCR_GEN_XFER_DYN, SCR_GEN_XFER_STRICT, SCR_GEN_XFER_DROP };
|
|
|
|
static bool scr_gen_delegate_fence(const char *message, size_t len) {
|
|
scr_throw_error_msg_code(SCR_ERR_ERROR, message, len, "SC1071");
|
|
return false;
|
|
}
|
|
|
|
static bool scr_gen_slot_transfer(ScrGenSlot *dst, ScrGenSlot *src, unsigned mode,
|
|
ScrDyn *(*to_dyn)(void *)) {
|
|
scr_gen_slot_reset(dst);
|
|
if (mode == SCR_GEN_XFER_DROP || src->kind == SCR_EXC_NONE) {
|
|
scr_gen_slot_reset(src);
|
|
return true;
|
|
}
|
|
if (mode == SCR_GEN_XFER_STRICT) {
|
|
static const char sc_m[] =
|
|
"yield* completing with a value outside the delegating generator's return type is not "
|
|
"supported";
|
|
return scr_gen_delegate_fence(sc_m, sizeof sc_m - 1);
|
|
}
|
|
if (mode == SCR_GEN_XFER_DYN) {
|
|
ScrDyn *v;
|
|
switch (src->kind) {
|
|
case SCR_EXC_F64: v = scr_dyn_new_num(src->f64); break;
|
|
case SCR_EXC_BOOL: v = scr_dyn_new_bool(src->b); break;
|
|
default: v = to_dyn(src->payload); break;
|
|
}
|
|
scr_gen_slot_reset(src);
|
|
scr_gen_slot_ref(dst, v, scr_dyn_release_v);
|
|
return true;
|
|
}
|
|
*dst = *src;
|
|
src->kind = SCR_EXC_NONE;
|
|
src->payload = NULL;
|
|
src->release_fn = NULL;
|
|
return true;
|
|
}
|
|
|
|
/* Whether the delegate is done. `typed_ret`: the delegating body reads the
|
|
* completion as a return type that cannot hold undefined, which an empty
|
|
* completion (a finished delegate, or a valueless return) would be. */
|
|
static bool scr_gen_delegate_done(ScrGen *d, bool typed_ret) {
|
|
if (d->state != SCR_GEN_DONE) return false;
|
|
if (typed_ret && d->out.kind == SCR_EXC_NONE && !scr_exc_pending()) {
|
|
static const char sc_m[] =
|
|
"yield* of a generator that completes with undefined through a return type that cannot "
|
|
"hold it is not supported";
|
|
scr_gen_delegate_fence(sc_m, sizeof sc_m - 1);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool scr_gen_delegate_start(ScrGen *d, unsigned flags) {
|
|
if ((flags & 1) && d->state == SCR_GEN_SUSPENDED) {
|
|
static const char sc_m[] =
|
|
"yield* into a started generator with a typed next channel is not supported (it would "
|
|
"deliver undefined into a typed yield)";
|
|
return scr_gen_delegate_fence(sc_m, sizeof sc_m - 1);
|
|
}
|
|
scr_gen_slot_reset(&d->in);
|
|
scr_gen_resume(d);
|
|
return scr_gen_delegate_done(d, flags & 2);
|
|
}
|
|
|
|
bool scr_gen_delegate_next(ScrGen *d, unsigned flags, ScrDyn *(*to_dyn)(void *)) {
|
|
scr_gen_slot_transfer(&d->in, &scr_gen_self()->in, flags & 3, to_dyn);
|
|
scr_gen_resume(d);
|
|
return scr_gen_delegate_done(d, flags & 4);
|
|
}
|
|
|
|
bool scr_gen_delegate_forward(ScrGen *d, unsigned modes, ScrDyn *(*next_dyn)(void *),
|
|
ScrDyn *(*ret_dyn)(void *), ScrDyn *(*done_dyn)(void *)) {
|
|
ScrGen *g = scr_gen_self();
|
|
ScrExcCell *cell = scr_exc_current_cell();
|
|
if (cell->kind == SCR_EXC_NONE) {
|
|
scr_gen_slot_transfer(&d->in, &g->in, modes & 3, next_dyn);
|
|
scr_gen_resume(d);
|
|
return scr_gen_delegate_done(d, modes & 64);
|
|
}
|
|
if (cell->kind != SCR_EXC_GENRET) {
|
|
/* throw(e): the pending payload moves into the delegate. */
|
|
scr_gen_resume_throw(d);
|
|
return scr_gen_delegate_done(d, modes & 64);
|
|
}
|
|
/* return(v): the delegate's return runs first; only its completion
|
|
* returns from this body. */
|
|
unsigned ret_mode = (modes >> 2) & 3;
|
|
scr_exc_clear();
|
|
if (ret_mode == SCR_GEN_XFER_STRICT) scr_gen_slot_reset(&d->ret);
|
|
else scr_gen_slot_transfer(&d->ret, &g->ret, ret_mode, ret_dyn);
|
|
scr_gen_resume_return(d);
|
|
if (scr_exc_pending()) return false;
|
|
if (d->state == SCR_GEN_DONE) {
|
|
if (d->out.kind == SCR_EXC_NONE ||
|
|
scr_gen_slot_transfer(&g->ret, &d->out, (modes >> 4) & 3, done_dyn)) {
|
|
scr_exc_current_cell()->kind = SCR_EXC_GENRET;
|
|
scr_exc_cell_changed();
|
|
}
|
|
return true;
|
|
}
|
|
if (g->ret.kind != SCR_EXC_NONE) {
|
|
/* The delegate's finally yielded; its later completion could not
|
|
* carry the parked value. */
|
|
static const char sc_m[] =
|
|
"yield* forwarding return(value) to a generator with a different return type whose "
|
|
"finally block yields is not supported";
|
|
scr_gen_delegate_fence(sc_m, sizeof sc_m - 1);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* yield: park the value in OUT and hop back to the resumer. Control
|
|
* returns here at the next resume — possibly with an injected .throw
|
|
* payload or the GENRET sentinel pending (the emitted check handles it). */
|
|
static void scr_gen_yield_switch(void) {
|
|
ScrFiber *self = scr_current;
|
|
scr_switch(&self->ctx, self->return_to, NULL);
|
|
}
|
|
void scr_gen_yield_f64(double v) {
|
|
scr_gen_slot_f64(&scr_gen_self()->out, v);
|
|
scr_gen_yield_switch();
|
|
}
|
|
void scr_gen_yield_bool(bool v) {
|
|
scr_gen_slot_bool(&scr_gen_self()->out, v);
|
|
scr_gen_yield_switch();
|
|
}
|
|
void scr_gen_yield_ref(void *v, void (*release)(void *)) {
|
|
scr_gen_slot_ref(&scr_gen_self()->out, v, release);
|
|
scr_gen_yield_switch();
|
|
}
|
|
|
|
/* Coroutine-body yields: park the value in the body's own generator; the
|
|
* emitted suspension follows. */
|
|
void scr_coro_yield_f64(ScrFiber *self, double v) { scr_gen_slot_f64(&self->gen->out, v); }
|
|
void scr_coro_yield_bool(ScrFiber *self, bool v) { scr_gen_slot_bool(&self->gen->out, v); }
|
|
void scr_coro_yield_ref(ScrFiber *self, void *v, void (*release)(void *)) {
|
|
scr_gen_slot_ref(&self->gen->out, v, release);
|
|
}
|
|
|
|
void scr_async_gen_return_awaits(void) { scr_gen_self()->return_awaited = false; }
|
|
|
|
void scr_async_gen_hop_done(void) {
|
|
ScrGen *g = scr_gen_self();
|
|
if (!g->is_async) {
|
|
fputs("scriptc: internal error: async-generator hop on sync generator\n", stderr);
|
|
abort();
|
|
}
|
|
g->success_hop_done = true;
|
|
}
|
|
|
|
bool scr_exc_genret_pending(void) {
|
|
return scr_exc_current_cell()->kind == SCR_EXC_GENRET;
|
|
}
|
|
|
|
void scr_gen_ret_to_out(ScrGen *g) {
|
|
scr_gen_slot_reset(&g->out);
|
|
g->out = g->ret;
|
|
g->ret.kind = SCR_EXC_NONE;
|
|
g->ret.payload = NULL;
|
|
g->ret.release_fn = NULL;
|
|
}
|
|
|
|
/* Back from the body (a yield, a finish, or an async generator's await):
|
|
* settle the generator's state, moving a finished body's exception to the
|
|
* resumer and tearing the finished fiber down. `release` drops the hop's
|
|
* own reference (see scr_gen_switch_in); `sync` marks a body known to be a
|
|
* synchronous generator (every stackless one is). */
|
|
__attribute__((always_inline)) static inline void scr_gen_switched_back(ScrGen *g, ScrFiber *f,
|
|
bool release, bool sync) {
|
|
if (__builtin_expect(f->done, 0)) {
|
|
g->state = SCR_GEN_DONE;
|
|
if (f->exc.kind != SCR_EXC_NONE) {
|
|
if (!sync && g->is_async) {
|
|
/* Async-generator failures reject the active request; they never
|
|
* throw synchronously from next/return/throw. */
|
|
scr_gen_exc_move(&g->async_error, &f->exc);
|
|
} else {
|
|
/* The sync body's exception escapes at the resume site. */
|
|
ScrExcCell *mine = scr_exc_current_cell();
|
|
scr_gen_exc_move(mine, &f->exc);
|
|
}
|
|
}
|
|
scr_fiber_destroy(f);
|
|
g->fiber = NULL;
|
|
scr_fibers_live--;
|
|
} else {
|
|
/* An async generator can return to its resumer because it yielded OR
|
|
* because await parked it. OUT distinguishes the two. */
|
|
g->state = !sync && g->is_async && !scr_gen_out_has(g)
|
|
? SCR_GEN_AWAITING
|
|
: SCR_GEN_SUSPENDED;
|
|
}
|
|
if (release) scr_gen_release(g);
|
|
}
|
|
|
|
#ifndef __wasi__
|
|
/* The stackless form of the hop: the coroutine frame runs on this stack
|
|
* until its next yield or its final suspension returns here. `retain`
|
|
* holds the generator across the hop; a resumer whose own reference
|
|
* provably outlives the hop passes false. */
|
|
__attribute__((always_inline)) static inline void scr_gen_coro_hop(ScrGen *g, bool retain) {
|
|
ScrFiber *f = g->fiber;
|
|
if (retain) scr_gen_retain(g);
|
|
g->state = SCR_GEN_RUNNING;
|
|
f->resumer = scr_current;
|
|
ScrAlsCtx **resumer_slot = scr_als_active;
|
|
scr_current = f;
|
|
scr_exc_swap_cell(&f->exc);
|
|
/* A synchronous generator has no AsyncLocalStorage context of its own:
|
|
* its step runs in the resumer's slot, as Node runs it in the execution
|
|
* that called next(). An async generator's step runs in its own. */
|
|
if (g->is_async) scr_als_active = &f->als;
|
|
if (f->coro == NULL) f->entry(f, f->argpack);
|
|
else scr_coro_resume(f->coro);
|
|
/* A running generator cannot be resumed again, so neither its fiber nor
|
|
* the parked resumer changed while the body ran. */
|
|
f = g->fiber;
|
|
ScrFiber *me = f->resumer;
|
|
scr_current = me;
|
|
scr_exc_swap_cell(me != NULL ? &me->exc : NULL);
|
|
scr_als_active = resumer_slot;
|
|
scr_gen_switched_back(g, f, retain, true);
|
|
}
|
|
|
|
static void scr_gen_coro_in(ScrGen *g) { scr_gen_coro_hop(g, true); }
|
|
#endif
|
|
|
|
/* The consumer→fiber hop shared by every resume mode: switches in, and on
|
|
* the way back moves a pending body exception into the RESUMER's cell
|
|
* (synchronous propagation — the await-rethrow analog) and tears down a
|
|
* finished fiber. The gen is retained across the hop so a body that
|
|
* releases the consumer's last reference cannot free it mid-run. */
|
|
static void scr_gen_switch_in(ScrGen *g) {
|
|
ScrFiber *f = g->fiber;
|
|
#ifndef __wasi__
|
|
if (f->stackless) {
|
|
scr_gen_coro_in(g);
|
|
return;
|
|
}
|
|
#endif
|
|
scr_gen_retain(g);
|
|
g->state = SCR_GEN_RUNNING;
|
|
if (!g->is_async) scr_als_step_begin(f);
|
|
#ifdef _WIN32
|
|
ScrCtx here = scr_win_self();
|
|
#elif defined(__wasi__)
|
|
ScrCtx here = 0;
|
|
#else
|
|
ScrCtx here;
|
|
#endif
|
|
f->return_to = &here;
|
|
ScrFiber *me = scr_current;
|
|
ScrAlsCtx **resumer_slot = scr_als_active;
|
|
#ifdef __wasi__
|
|
scr_current = f;
|
|
scr_exc_swap_cell(&f->exc);
|
|
scr_als_active = &f->als;
|
|
if (f->coro == NULL) f->entry(f, f->argpack);
|
|
else scr_wasi_coro_resume(f->coro);
|
|
scr_current = me;
|
|
scr_exc_swap_cell(me != NULL ? &me->exc : NULL);
|
|
scr_als_active = resumer_slot;
|
|
#else
|
|
scr_switch(&here, &f->ctx, f);
|
|
/* Back on the consumer: restore identity, the consumer's cell and its
|
|
* AsyncLocalStorage slot (the yield/finish switch targeted NULL — main's
|
|
* cell and slot). */
|
|
scr_current = me;
|
|
scr_exc_swap_cell(me != NULL ? &me->exc : NULL);
|
|
scr_als_active = resumer_slot;
|
|
#endif
|
|
scr_als_prefix_end(f);
|
|
scr_gen_switched_back(g, f, true, false);
|
|
}
|
|
|
|
void scr_gen_resume(ScrGen *g) {
|
|
switch (g->state) {
|
|
case SCR_GEN_RUNNING: {
|
|
static const char sc_m[] = "Generator is already running";
|
|
scr_throw_error_msg(SCR_ERR_TYPE, sc_m, sizeof sc_m - 1);
|
|
return;
|
|
}
|
|
case SCR_GEN_DONE:
|
|
/* next() on a done generator: { value: undefined, done: true } —
|
|
* OUT is NONE (the completing resume took it). */
|
|
return;
|
|
default:
|
|
scr_gen_switch_in(g);
|
|
}
|
|
}
|
|
|
|
void scr_gen_set_unstarted_return(ScrGen *g, void (*hook)(void *)) { g->unstarted_return = hook; }
|
|
|
|
void scr_gen_set_tag(ScrGen *g, const char *tag) { g->tag = tag; }
|
|
|
|
void scr_gen_copy_tag(ScrGen *g, const ScrGen *from) { g->tag = from->tag; }
|
|
|
|
const char *scr_gen_tag(const ScrGen *g) {
|
|
return g->tag ? g->tag : g->is_async ? "AsyncGenerator" : "Generator";
|
|
}
|
|
|
|
__attribute__((constructor)) static void scr_gen_tag_reader_init(void) {
|
|
scr_gen_tag_reader = scr_gen_tag;
|
|
}
|
|
|
|
void scr_gen_close(ScrGen *g) {
|
|
scr_gen_slot_reset(&g->ret);
|
|
scr_gen_resume_return(g);
|
|
scr_gen_slot_reset(&g->out);
|
|
}
|
|
|
|
/* The compiler's own drives (for-of, spread, destructuring, valueless
|
|
* yield* steps). Every emitted caller passes a generator it owns or lends
|
|
* from a binding nothing can rebind during the step, so the body cannot
|
|
* drop the last reference and a stackless hop needs no retain of its own. */
|
|
bool scr_gen_step(ScrGen *g) {
|
|
if (g->in.kind != SCR_EXC_NONE) scr_gen_slot_reset(&g->in);
|
|
#ifndef __wasi__
|
|
if ((g->state == SCR_GEN_SUSPENDED || g->state == SCR_GEN_UNSTARTED) && g->fiber->stackless) {
|
|
scr_gen_coro_hop(g, false);
|
|
return g->state == SCR_GEN_DONE;
|
|
}
|
|
#endif
|
|
scr_gen_resume(g);
|
|
return g->state == SCR_GEN_DONE;
|
|
}
|
|
|
|
void scr_gen_resume_return(ScrGen *g) {
|
|
switch (g->state) {
|
|
case SCR_GEN_RUNNING: {
|
|
static const char sc_m[] = "Generator is already running";
|
|
scr_throw_error_msg(SCR_ERR_TYPE, sc_m, sizeof sc_m - 1);
|
|
return;
|
|
}
|
|
case SCR_GEN_DONE:
|
|
scr_gen_ret_to_out(g);
|
|
return;
|
|
case SCR_GEN_UNSTARTED:
|
|
/* The body never runs: tear the fiber down cleanly (drop the packed
|
|
* arguments) and complete with the parked value. The generator is
|
|
* done before the hook runs, so a resume from inside it answers done;
|
|
* an exception the hook raises stays pending for the caller. */
|
|
g->state = SCR_GEN_DONE;
|
|
if (g->unstarted_return != NULL) g->unstarted_return(g->fiber->argpack);
|
|
if (g->drop_args != NULL) g->drop_args(g->fiber->argpack);
|
|
else free(g->fiber->argpack);
|
|
scr_fiber_destroy(g->fiber);
|
|
g->fiber = NULL;
|
|
scr_fibers_live--;
|
|
scr_gen_ret_to_out(g);
|
|
return;
|
|
default:
|
|
/* Suspended at a yield: inject the sentinel (an earlier .return whose
|
|
* unwind a finally-yield parked leaves it already set) and resume. */
|
|
if (g->fiber->exc.kind == SCR_EXC_NONE) g->fiber->exc.kind = SCR_EXC_GENRET;
|
|
scr_gen_switch_in(g);
|
|
}
|
|
}
|
|
|
|
void scr_gen_resume_throw(ScrGen *g) {
|
|
switch (g->state) {
|
|
case SCR_GEN_RUNNING: {
|
|
/* Replace the parked payload with Node's reentrancy TypeError. */
|
|
static const char sc_m[] = "Generator is already running";
|
|
scr_throw_error_msg(SCR_ERR_TYPE, sc_m, sizeof sc_m - 1);
|
|
return;
|
|
}
|
|
case SCR_GEN_DONE:
|
|
/* .throw on a done generator: it stays done; the payload stays
|
|
* pending in the caller — the call site's check rethrows it. */
|
|
return;
|
|
case SCR_GEN_UNSTARTED:
|
|
/* The body never runs; the generator becomes done and the payload
|
|
* stays pending in the caller (probed Node behavior). */
|
|
if (g->drop_args != NULL) g->drop_args(g->fiber->argpack);
|
|
else free(g->fiber->argpack);
|
|
scr_fiber_destroy(g->fiber);
|
|
g->fiber = NULL;
|
|
scr_fibers_live--;
|
|
g->state = SCR_GEN_DONE;
|
|
return;
|
|
default: {
|
|
/* Move the caller's pending payload into the fiber's cell (an
|
|
* earlier sentinel parked by a finally-yield is replaced — the
|
|
* injected throw wins, like a throw inside that finally). */
|
|
ScrExcCell *mine = scr_exc_current_cell();
|
|
ScrExcCell *dst = &g->fiber->exc;
|
|
dst->kind = mine->kind;
|
|
dst->f64 = mine->f64;
|
|
dst->b = mine->b;
|
|
dst->payload = mine->payload;
|
|
dst->retain_fn = mine->retain_fn;
|
|
dst->release_fn = mine->release_fn;
|
|
dst->trace_fn = mine->trace_fn;
|
|
mine->kind = SCR_EXC_NONE;
|
|
mine->payload = NULL;
|
|
mine->trace_fn = NULL;
|
|
scr_exc_cell_changed();
|
|
scr_gen_switch_in(g);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ── async generators ────────────────────────────────────────────────
|
|
* The body is the same suspended generator fiber, now allowed to await.
|
|
* Each resume request owns a promise and a retained generator. The first
|
|
* idle request starts synchronously; successful yield/return settlement
|
|
* takes AsyncGeneratorYield's Await hop (the emitter marks a yielded
|
|
* promise whose await already supplied it), while thrown completions reject
|
|
* immediately. Requests queued behind an active one begin in that
|
|
* settlement continuation, preserving Node's ordering. */
|
|
|
|
static void scr_async_gen_start_queued(ScrGen *g);
|
|
|
|
static void scr_async_gen_schedule_settle(ScrGen *g) {
|
|
if (g->settle_queued) return;
|
|
ScrFiber *job = scr_mem_calloc(sizeof *job);
|
|
if (!job) scr_oom();
|
|
job->gen_job = scr_gen_retain(g);
|
|
g->settle_queued = true;
|
|
scr_ready_push(job);
|
|
}
|
|
|
|
static void scr_async_gen_finish_active(ScrGen *g, bool rejected) {
|
|
ScrPromise *p = g->active_request;
|
|
if (p == NULL) {
|
|
fputs("scriptc: internal error: async generator settled without a request\n", stderr);
|
|
abort();
|
|
}
|
|
g->active_request = NULL;
|
|
if (rejected) {
|
|
scr_promise_reject_from_cell(p, &g->async_error);
|
|
scr_promise_settle_wake(p);
|
|
} else {
|
|
g->settle_async(g, p);
|
|
}
|
|
scr_promise_release(p);
|
|
/* Every queued request retains its generator until settlement. */
|
|
scr_gen_release(g);
|
|
}
|
|
|
|
static void scr_async_gen_capture_caller_error(ScrGen *g) {
|
|
ScrExcCell *cell = scr_exc_current_cell();
|
|
if (cell->kind != SCR_EXC_NONE) scr_gen_exc_move(&g->async_error, cell);
|
|
}
|
|
|
|
/* Whether the body's latest stop settles its request without a further
|
|
* turn: a yield whose await already supplied the value, or a completion
|
|
* after a return() request's await. */
|
|
static bool scr_async_gen_settles_now(ScrGen *g) {
|
|
if (g->success_hop_done) {
|
|
g->success_hop_done = false;
|
|
return true;
|
|
}
|
|
if (g->return_awaited && g->state == SCR_GEN_DONE) {
|
|
g->return_awaited = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) {
|
|
const bool was_done = g->state == SCR_GEN_DONE;
|
|
if (req->mode == SCR_AG_NEXT) {
|
|
scr_gen_slot_reset(&g->in);
|
|
g->in = req->arg;
|
|
memset(&req->arg, 0, sizeof req->arg);
|
|
scr_gen_resume(g);
|
|
} else if (req->mode == SCR_AG_RETURN) {
|
|
scr_gen_slot_reset(&g->ret);
|
|
g->ret = req->arg;
|
|
memset(&req->arg, 0, sizeof req->arg);
|
|
if (g->state == SCR_GEN_SUSPENDED) {
|
|
/* At a yield, AsyncGeneratorYield awaits the return value before
|
|
* the body resumes, so finally blocks run one turn later. */
|
|
g->return_queued = true;
|
|
scr_async_gen_schedule_settle(g);
|
|
return;
|
|
}
|
|
scr_gen_resume_return(g);
|
|
} else {
|
|
scr_gen_exc_move(scr_exc_current_cell(), &req->thrown);
|
|
scr_gen_resume_throw(g);
|
|
}
|
|
scr_async_gen_capture_caller_error(g);
|
|
|
|
if (g->async_error.kind != SCR_EXC_NONE) {
|
|
scr_async_gen_finish_active(g, true);
|
|
return;
|
|
}
|
|
if (g->state == SCR_GEN_AWAITING) return;
|
|
|
|
/* next() on an already-completed generator produces its undefined done
|
|
* result immediately. A yield and return(value) on an unstarted or
|
|
* completed generator pass through an Await step; a body that falls off
|
|
* its end, or whose `return x` already awaited x, marks its completion
|
|
* as settled. */
|
|
if (was_done && req->mode == SCR_AG_NEXT) {
|
|
scr_async_gen_finish_active(g, false);
|
|
} else if (scr_async_gen_settles_now(g)) {
|
|
scr_async_gen_finish_active(g, false);
|
|
} else {
|
|
scr_async_gen_schedule_settle(g);
|
|
}
|
|
}
|
|
|
|
/* The return() request's await has taken its turn: resume the body with
|
|
* the return completion. */
|
|
static void scr_async_gen_resume_returning(ScrGen *g) {
|
|
g->return_queued = false;
|
|
g->return_awaited = true;
|
|
scr_gen_resume_return(g);
|
|
scr_async_gen_capture_caller_error(g);
|
|
if (g->async_error.kind != SCR_EXC_NONE) {
|
|
scr_async_gen_finish_active(g, true);
|
|
} else if (g->state == SCR_GEN_AWAITING) {
|
|
return;
|
|
} else if (scr_async_gen_settles_now(g)) {
|
|
scr_async_gen_finish_active(g, false);
|
|
} else {
|
|
scr_async_gen_schedule_settle(g);
|
|
return;
|
|
}
|
|
scr_async_gen_start_queued(g);
|
|
}
|
|
|
|
static void scr_async_gen_start_queued(ScrGen *g) {
|
|
/* Keep the object alive while an immediately-settled request releases
|
|
* its own retain and the loop inspects the next queue node. */
|
|
scr_gen_retain(g);
|
|
while (g->active_request == NULL && g->requests_head != NULL) {
|
|
ScrAsyncGenRequest *req = g->requests_head;
|
|
g->requests_head = req->next;
|
|
if (g->requests_head == NULL) g->requests_tail = NULL;
|
|
g->active_request = req->promise; /* request ownership transfers */
|
|
scr_async_gen_run_active(g, req);
|
|
scr_gen_slot_reset(&req->arg);
|
|
scr_gen_exc_reset(&req->thrown);
|
|
free(req);
|
|
/* Awaiting or deferred successful settlement keeps this request
|
|
* active. Immediate done/error settlement cleared it and can consume
|
|
* the next queued request in this same turn. */
|
|
if (g->active_request != NULL) break;
|
|
}
|
|
scr_gen_release(g);
|
|
}
|
|
|
|
static ScrPromise *scr_async_gen_enqueue(ScrGen *g, int mode,
|
|
ScrGenSlot *arg,
|
|
ScrExcCell *thrown) {
|
|
if (!g->is_async || g->settle_async == NULL) {
|
|
fputs("scriptc: internal error: async resume of a sync generator\n", stderr);
|
|
abort();
|
|
}
|
|
ScrAsyncGenRequest *req = calloc(1, sizeof *req);
|
|
if (!req) scr_oom();
|
|
req->mode = mode;
|
|
if (arg != NULL) {
|
|
req->arg = *arg;
|
|
memset(arg, 0, sizeof *arg);
|
|
}
|
|
if (thrown != NULL) scr_gen_exc_move(&req->thrown, thrown);
|
|
req->promise = scr_promise_new();
|
|
/* The request owns the original promise reference and one generator
|
|
* reference; the caller receives a second promise reference. */
|
|
ScrPromise *result = scr_promise_retain(req->promise);
|
|
scr_gen_retain(g);
|
|
if (g->requests_tail != NULL) g->requests_tail->next = req;
|
|
else g->requests_head = req;
|
|
g->requests_tail = req;
|
|
if (g->active_request == NULL && !g->settle_queued) {
|
|
scr_async_gen_start_queued(g);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
ScrPromise *scr_async_gen_next_none(ScrGen *g) {
|
|
ScrGenSlot arg = {0};
|
|
return scr_async_gen_enqueue(g, SCR_AG_NEXT, &arg, NULL);
|
|
}
|
|
|
|
ScrPromise *scr_async_gen_next_f64(ScrGen *g, double value) {
|
|
ScrGenSlot arg = {0};
|
|
scr_gen_slot_f64(&arg, value);
|
|
return scr_async_gen_enqueue(g, SCR_AG_NEXT, &arg, NULL);
|
|
}
|
|
|
|
ScrPromise *scr_async_gen_next_bool(ScrGen *g, bool value) {
|
|
ScrGenSlot arg = {0};
|
|
scr_gen_slot_bool(&arg, value);
|
|
return scr_async_gen_enqueue(g, SCR_AG_NEXT, &arg, NULL);
|
|
}
|
|
|
|
ScrPromise *scr_async_gen_next_ref(ScrGen *g, void *value,
|
|
void (*release)(void *)) {
|
|
ScrGenSlot arg = {0};
|
|
scr_gen_slot_ref(&arg, value, release);
|
|
return scr_async_gen_enqueue(g, SCR_AG_NEXT, &arg, NULL);
|
|
}
|
|
|
|
ScrPromise *scr_async_gen_return_none(ScrGen *g) {
|
|
ScrGenSlot arg = {0};
|
|
return scr_async_gen_enqueue(g, SCR_AG_RETURN, &arg, NULL);
|
|
}
|
|
|
|
ScrPromise *scr_async_gen_return_f64(ScrGen *g, double value) {
|
|
ScrGenSlot arg = {0};
|
|
scr_gen_slot_f64(&arg, value);
|
|
return scr_async_gen_enqueue(g, SCR_AG_RETURN, &arg, NULL);
|
|
}
|
|
|
|
ScrPromise *scr_async_gen_return_bool(ScrGen *g, bool value) {
|
|
ScrGenSlot arg = {0};
|
|
scr_gen_slot_bool(&arg, value);
|
|
return scr_async_gen_enqueue(g, SCR_AG_RETURN, &arg, NULL);
|
|
}
|
|
|
|
ScrPromise *scr_async_gen_return_ref(ScrGen *g, void *value,
|
|
void (*release)(void *)) {
|
|
ScrGenSlot arg = {0};
|
|
scr_gen_slot_ref(&arg, value, release);
|
|
return scr_async_gen_enqueue(g, SCR_AG_RETURN, &arg, NULL);
|
|
}
|
|
|
|
ScrPromise *scr_async_gen_throw(ScrGen *g) {
|
|
ScrExcCell thrown = {0};
|
|
scr_gen_exc_move(&thrown, scr_exc_current_cell());
|
|
return scr_async_gen_enqueue(g, SCR_AG_THROW, NULL, &thrown);
|
|
}
|
|
|
|
static void scr_async_gen_job(ScrGen *g) {
|
|
g->settle_queued = false;
|
|
if (g->active_request == NULL || g->async_error.kind != SCR_EXC_NONE) {
|
|
fputs("scriptc: internal error: invalid async generator settlement job\n", stderr);
|
|
abort();
|
|
}
|
|
if (g->return_queued) {
|
|
scr_gen_retain(g);
|
|
scr_async_gen_resume_returning(g);
|
|
scr_gen_release(g);
|
|
return;
|
|
}
|
|
scr_async_gen_finish_active(g, false);
|
|
scr_async_gen_start_queued(g);
|
|
}
|
|
|
|
static void scr_async_gen_resume_ready(ScrGen *g) {
|
|
if (!g->is_async || g->active_request == NULL) {
|
|
fputs("scriptc: internal error: async generator resumed without a request\n", stderr);
|
|
abort();
|
|
}
|
|
/* The active request normally owns the generator. Keep an independent
|
|
* reference across settlement, which releases that request ownership. */
|
|
scr_gen_retain(g);
|
|
scr_gen_switch_in(g);
|
|
if (g->async_error.kind != SCR_EXC_NONE) {
|
|
scr_async_gen_finish_active(g, true);
|
|
scr_async_gen_start_queued(g);
|
|
} else if (g->state != SCR_GEN_AWAITING) {
|
|
if (scr_async_gen_settles_now(g)) {
|
|
scr_async_gen_finish_active(g, false);
|
|
scr_async_gen_start_queued(g);
|
|
} else {
|
|
scr_async_gen_schedule_settle(g);
|
|
}
|
|
}
|
|
scr_gen_release(g);
|
|
}
|
|
|
|
#ifdef SCR_WORKERS
|
|
static void scr_context_ready_discard(void) {
|
|
while (scr_ready_len) {
|
|
ScrFiber *fiber = scr_ready[scr_ready_head++];
|
|
scr_ready_len--;
|
|
if (fiber->micro_cb) {
|
|
scr_closure_release(fiber->micro_cb);
|
|
scr_mem_free(fiber);
|
|
} else if (fiber->gen_job) {
|
|
ScrGen *generator = fiber->gen_job;
|
|
generator->settle_queued = false;
|
|
scr_gen_release(generator);
|
|
scr_mem_free(fiber);
|
|
} else if (fiber->cb_job) {
|
|
scr_promise_release(fiber->cb.dst);
|
|
if (fiber->cb.all) scr_promise_all_state_release(fiber->cb.all);
|
|
scr_promise_release(fiber->cb_src);
|
|
scr_mem_free(fiber);
|
|
}
|
|
/* Real fibers belong to the context registry, including queued ones. */
|
|
}
|
|
scr_ready_head = 0;
|
|
}
|
|
|
|
/* Thread exit reclaims suspended native stacks by resuming only their
|
|
* compiler-owned unwind paths. No JavaScript catch/finally or queued user
|
|
* callback runs after the stop flag is installed. */
|
|
void scr_loop_context_shutdown(void) {
|
|
if (scr_current) scr_trap("scriptc: context shutdown on an active fiber\n");
|
|
scr_context_stop(scr_exit_code_hint_get());
|
|
scr_timers_teardown();
|
|
scr_context_ready_discard();
|
|
while (scr_context_fibers) {
|
|
ScrFiber *fiber = scr_context_fibers;
|
|
if (fiber->gen) {
|
|
ScrGen *generator = scr_gen_retain(fiber->gen);
|
|
if (generator->state == SCR_GEN_UNSTARTED) {
|
|
if (generator->drop_args) generator->drop_args(fiber->argpack);
|
|
else free(fiber->argpack);
|
|
scr_fiber_destroy(fiber);
|
|
generator->fiber = NULL;
|
|
generator->state = SCR_GEN_DONE;
|
|
scr_fibers_live--;
|
|
} else {
|
|
scr_gen_switch_in(generator);
|
|
if (generator->fiber) scr_trap("scriptc: generator did not unwind during shutdown\n");
|
|
}
|
|
while (generator->requests_head) {
|
|
ScrAsyncGenRequest *request = generator->requests_head;
|
|
generator->requests_head = request->next;
|
|
scr_gen_slot_reset(&request->arg);
|
|
scr_gen_exc_reset(&request->thrown);
|
|
scr_promise_release(request->promise);
|
|
free(request);
|
|
scr_gen_release(generator);
|
|
}
|
|
generator->requests_tail = NULL;
|
|
if (generator->active_request) {
|
|
scr_promise_release(generator->active_request);
|
|
generator->active_request = NULL;
|
|
scr_gen_release(generator);
|
|
}
|
|
if (generator->context_abandoned) {
|
|
generator->context_abandoned = false;
|
|
scr_gen_release(generator);
|
|
}
|
|
scr_gen_release(generator);
|
|
} else {
|
|
scr_resume_fiber(fiber);
|
|
if (scr_context_fibers == fiber) scr_trap("scriptc: async function did not unwind during shutdown\n");
|
|
}
|
|
scr_context_ready_discard();
|
|
}
|
|
scr_discard_unhandled_rejections();
|
|
free(scr_ready); scr_ready = NULL; scr_ready_cap = 0;
|
|
free(scr_timers); scr_timers = NULL; scr_timers_cap = 0;
|
|
free(scr_immediates); scr_immediates = NULL; scr_immediates_cap = 0;
|
|
free(scr_maybe_unhandled); scr_maybe_unhandled = NULL; scr_unhandled_cap = 0;
|
|
scr_als_ctx_release(scr_als_main_slot); scr_als_main_slot = NULL;
|
|
scr_fibers_abandoned = 0;
|
|
scr_note_abandoned_fibers(0);
|
|
}
|
|
#endif
|