/* 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 #include #include #include #include #include #include #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 #elif defined(__wasi__) #include #else #include #include #include #include #include #include #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[] 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