Files
scriptc/packages/runtime/src/scr_runtime.h
T
Chris Tate 13718c7ac1 Preserve native class state and handle terminal process events (#571)
- Preserve fields, accessors, added properties, and cyclic references across untyped class boundaries.
- Support platform signal names and native exception handlers that resume queued work.
2026-09-29 09:09:34 -05:00

7119 lines
415 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* scriptc runtime — public API.
* Every scriptc binary compiles these sources in; there is no shared library.
*
* Prefix conventions: the runtime owns "scr_" and "SCR_" macros/"Scr" type names.
* Compiler-emitted symbols use "sc_f_" (functions), "sc_l_" (locals),
* "sc_t" (temps) and never collide.
*/
#ifndef SCR_RUNTIME_H
#define SCR_RUNTIME_H
#include <stdarg.h> /* va_list in the emitter listener adapters */
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h> /* memcpy in the inline slot accessors */
#include <sys/types.h> /* ssize_t in the transport ops table */
/* ── libc shims ─────────────────────────────────────────────────────────
* Win32's missing POSIX/BSD functions live in scr_win.c. Zig's musl sysroot
* additionally lacks arc4random_buf; scr_musl.c supplies it from Linux's
* getrandom syscall. Both files are selected by native-toolchain.ts only for their target. */
#if defined(_WIN32) && defined(_MSC_VER)
/* MSVC's C headers do not define POSIX ssize_t. Keep the runtime's byte-count
* interfaces pointer-sized on Windows without requiring a Windows SDK header
* (and leave the Zig/MinGW compatibility route's native typedef intact). */
typedef intptr_t ssize_t;
#endif
#ifdef _WIN32
#include <time.h> /* time_t / struct tm for the gmtime_r shim */
char *stpcpy(char *dst, const char *src);
void arc4random_buf(void *buf, size_t n);
struct tm *gmtime_r(const time_t *t, struct tm *out);
char *strcasestr(const char *hay, const char *needle);
#elif defined(SCR_MUSL)
void arc4random_buf(void *buf, size_t n);
#endif
/* ── thread-instanced library state ─────────────────────────────────────
* Archives built under the profile's abi.instance_per_thread compile every
* TU with -DSCR_THREAD_INSTANCES, and SCR_TL moves each unit's mutable
* state — and the emitted program's globals — into thread-local storage.
* A thread that calls the profile's init entry then owns a complete,
* independent instance: its own collector, result arena, panic-sink
* registration, poison flag, and program state. The instance's lifetime is
* the thread's; the one-thread-per-instance contract (an instance is never
* entered from two threads) is unchanged — this mode adds instances, not
* thread awareness. Expands to nothing everywhere else, so executable
* builds and classic library builds carry the exact bytes they always
* carried. Truly immutable tables (static const data) stay shared. */
#if defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)
#define SCR_TL _Thread_local
#else
#define SCR_TL
#endif
/* ── process ──────────────────────────────────────────────────────────── */
/* Called once at the top of main: private stdout formatter buffer,
* flush-at-exit, RC audit registration (when built with -DSCR_RC_AUDIT).
* JavaScript-visible writes flush before returning. */
void scr_init(void);
/* Program objects emitted by the bundled LLVM helper reference this symbol.
* Its versioned spelling makes a mismatched manual runtime link fail before
* the program can start. */
void scr_runtime_abi_v4(void);
/* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ──────
* Every unrecoverable runtime trap — OOM, semantic range traps, internal-
* invariant failures — funnels through this pair instead of open-coded
* fputs/fprintf + abort. Executable builds expand to exactly the historical
* behavior (the message's bytes on stderr, then abort — the default lane
* must not change by a byte). Library builds (-DSCR_LIB) route the message to
* the host-registered panic sink and abort only as the last resort: before
* registration, or if the sink returns (the ruled host-contract violation —
* a conforming sink longjmps to a host frame BELOW the entry, never back
* into library frames). Messages keep their trailing newline in both lanes;
* the library funnel additionally assembles every DETECTED trap into the
* structured trap-teaching form before delivery (a message that already
* begins with the 0x01 marker passes verbatim) — see ScrLibSinkFn below. */
_Noreturn void scr_trap(const char *msg);
_Noreturn void scr_trap_fmt(const char *fmt, ...);
/* ── library mode (scr_library.c, linked only into library artifacts) ─────
* A library artifact has no main, no event loop, no signal handlers, no
* atexit registrations, and never touches host stdio modes or buffering:
* initialization runs inside the profile-named init entry (re-runnable
* deterministically), traps route to the sink above, and buffer-class
* results live in a library-owned result arena. Everything here compiles only
* under -DSCR_LIB; executable builds never contain it. */
#ifdef SCR_LIB
typedef struct ScrStr ScrStr; /* full definitions below (C11 repeat) */
typedef struct ScrBytes ScrBytes;
/* The host's panic sink: msg is UTF-8, valid only for the duration of the
* call; address is the trap site's return address (0 when the toolchain
* cannot supply one); ctx is the registration's opaque pointer. The sink
* must not call back into any library entry and must not unwind or longjmp
* back into library frames.
*
* Message shape (the ratified structured trap-teaching encoding): a
* BASELINE message is plain text whose first byte is printable (>= 0x20) —
* no emitter path ever produces an unstructured message starting below
* 0x20. A STRUCTURED message begins with the marker byte 0x01 followed by
* the human teaching text and 0x1F-separated fields:
*
* 0x01 text 0x1F code 0x1F symbol [ 0x1F remediation ]
*
* so msg_len > 0 && msg[0] == 0x01 is the one version test. Parse: split
* the bytes after the marker on 0x1F — field 0 is the human text, 1 the
* diagnostic code, 2 the trapping symbol as the host linked it, 3 the
* remediation; a missing or empty field means none; ignore any field past
* the fourth. Fields are (pointer, length) — never assume NUL termination.
* A plain-text host may print the whole buffer: the teaching leads it.
*
* Every trap the runtime DETECTS arrives structured: the funnel assembles
* the baseline human line into field 0 unchanged, a stable code for the
* trap kind (the compiler registry's runtime family — SC4013–SC4019 plus
* the SC4025 unregistered-callback and SC4026 callback-re-entry traps,
* classified in scr_library.c), the
* entry symbol recorded by the trapping
* entry's prologue, and the profile's remediation for that code when the
* program TU's overlay table declares one (the whole fourth field is
* absent otherwise). A message that already begins with the marker — a
* facade-authored structured throw, or the wrapper's compile-time-
* assembled SC4012 contract trap — passes through byte-for-byte. */
typedef void (*ScrLibSinkFn)(void *ctx, const uint8_t *msg, size_t msg_len,
uint64_t address);
void scr_library_set_sink(ScrLibSinkFn fn, void *ctx); /* latest wins */
/* ── host-callback channels ───────────────────────────────────────────────
* The panic sink's registration pattern generalized into a synchronous
* outbound seam: the profile declares named channels (bytes/scalar
* signatures), the generated registration symbol maps a channel name to a
* slot index here, and compiled call sites fetch the slot through
* scr_library_cb_require — which returns the host's pointer or delivers
* the call site's trap message through the funnel (the SC4025 detected
* trap) when the host never registered. The typed shape of each stored
* pointer is the channel's, with the opaque context first:
*
* <ret> (*)(void *ctx, <params...>)
*
* Registration is a pure store like the sink's (no entry prologue, no
* poison guard, legal before init); latest wins, NULL clears, and
* registrations persist across init/reset. While a host callback is active,
* its registration entry is rejected before name dispatch or a store, just
* like every runtime-touching ABI entry. Slots are per-copy of this
* state, exactly the sink's story: per-archive under abi.localize_runtime,
* per-thread instance under abi.instance_per_thread (SCR_TL) — a callback
* registered on thread T fires only for T's instance. The host's callback
* runs on the calling thread inside the entry's dynamic extent and must
* NOT call back into any library entry (exports, init, reset, collect, sink
* registration, or callback registration) or unwind/longjmp across library
* frames: read the borrowed buffers, copy what outlives the call, return.
* A re-entry is a detected SC4026 trap: it poisons only this library
* instance, delivers exactly once to the already-registered sink, names the
* attempted inner ABI symbol in structured field 2, then aborts if the sink
* returns. A later host-loop turn may enter normally after the callback has
* returned. Buffer parameters are borrowed for the duration of the call
* only. */
#define SCR_LIB_MAX_CALLBACKS 32 /* keep in step with LIB_MAX_CALLBACKS (library/library-profile.ts) */
/* The stored shape: generated call sites cast a slot's pointer to the
* channel's typed shape before calling. */
typedef void (*ScrLibCbFn)(void);
void scr_library_cb_set(size_t slot, ScrLibCbFn fn, void *ctx);
/* The call-site fetch: the registered pointer, or the funnel trap with
* trap_msg (never returns NULL). */
ScrLibCbFn scr_library_cb_require(size_t slot, const char *trap_msg);
void *scr_library_cb_ctx(size_t slot);
/* Generated typed call sites bracket only the actual host-function call.
* End is reached only after a normal return; an illegal unwind deliberately
* leaves the depth active so the next ABI entry is rejected. */
void scr_library_callback_begin(void);
void scr_library_callback_end(void);
/* Registration wrappers bypass scr_library_entry because their normal path
* is a pure store. They call this first so callback-time registration is
* rejected before dispatch, NULL handling, or mutation. */
void scr_library_callback_entry_guard(const char *entry_symbol);
/* Entry prologue: aborts deterministically when the library is poisoned (a
* trap already fired — no profile entry may run again; recovery is process
* restart). reset_arena additionally drops the result arena (the
* auto-reset posture, and the reset/collect entries' shared body).
* entry_symbol is the generated entry's external symbol exactly as the
* host linked it (a static string in the program TU): the prologue records
* it in the funnel's current-entry slot so a detected trap's structured
* message can name the trapping entry. A host callback's attempted nested
* entry is rejected first and replaces this slot with that inner symbol.
* Init and the mode entries (reset, collect) record theirs too. The two
* profile identity getters are the explicit pure-data exception: they touch
* no mutable runtime state and remain callable before init and after poison. */
void scr_library_entry(bool reset_arena, const char *entry_symbol);
void scr_library_arena_reset(void);
/* The mode-provided collect entry's body: arena reset + a full cycle
* collection (snapshot-invariant by construction — collection frees only
* unreachable cycles). */
void scr_library_collect(void);
/* Full session reset, called by the generated init entry AFTER the program
* TU released and zeroed its globals: pending-exception clear, arena
* reset, the reset registry's drains (the units' repointed atexit halves),
* the library's interned process values, a cycle collection, and — under
* SCR_RC_AUDIT — the zero-live-heap assertion (a failure is a trap through
* the sink, never _Exit). */
void scr_library_reset(void);
/* Where a unit would atexit() a lazy teardown, library builds register it
* here instead (called on every scr_library_reset, registered once). */
void scr_library_register_reset(void (*fn)(void));
/* An escaped exception at an entry boundary: renders the same "Uncaught
* ..." text the executable epilogue prints, releases the payload, and
* routes the text through the trap funnel. No-op when nothing is pending.
* The ratified verbatim rule: a thrown message that ALREADY begins with the
* structured marker 0x01 (a thrown string, or an Error whose .message
* starts with it) is delivered byte-for-byte — no "Uncaught " prefix, no
* added newline — which is how facade-authored structured teachings ride
* the throw channel to the sink. Defined in scr_exception.c (it owns the
* cell). */
void scr_library_check_exc(void);
/* The length-taking funnel entry (library lane only): delivers exactly the
* given bytes to the sink — the verbatim path above needs it because a
* structured message is length-delimited, never NUL-scanned. */
_Noreturn void scr_trap_len(const char *msg, size_t len);
/* The runtime-trap overlay table, DEFINED by the generated program TU
* (both emissions emit identical data) and consumed by the funnel when it
* assembles a detected trap's structured message: flat triples of
* (code, teaching-or-NULL, remediation-or-NULL), one per runtime trap code
* (the SC4013–SC4019 family plus SC4025 and SC4026) the profile declares
* text for;
* _len counts triples. A declared teaching replaces the baseline human line as field 0;
* a declared remediation becomes the optional fourth field. */
extern const char *const scr_library_trap_overlays[];
extern const size_t scr_library_trap_overlays_len;
/* Marshalling helpers the generated wrappers call (both emissions share
* these bodies, which is how the two lanes stay identical by
* construction). Inbound is borrowed-and-copied; outbound values MOVE into
* the result arena and stay valid until the next arena reset. String
* results are NUL-terminated after *out_len bytes (ScrStr's layout). */
ScrStr *scr_library_str_in(const uint8_t *p, size_t len); /* +1 */
/* trap_msg is the wrapper's compiler-assembled host-contract trap message
* (structured trap-teaching bytes naming this entry's symbol), delivered
* through the funnel when len falls outside the marshalling class. */
ScrBytes *scr_library_bytes_in(const uint8_t *p, size_t len, const char *trap_msg); /* +1, u8 */
/* The inbound declared-integer edge (ask 4's i64/u64 parameter classes):
* exact conversion for |v| <= 2^53-1, the host-contract trap (same
* assembled SC4012 message shape as the bytes trap) past it — silent
* rounding is a coercion the author never wrote. */
double scr_library_i64_in(int64_t v, const char *trap_msg);
double scr_library_u64_in(uint64_t v, const char *trap_msg);
void scr_library_str_out(ScrStr *s, const uint8_t **out, size_t *out_len);
void scr_library_bytes_out(ScrBytes *b, const uint8_t **out, size_t *out_len);
#define scr_atexit(fn) scr_library_register_reset(fn)
#else
#define scr_atexit(fn) atexit(fn)
#endif /* SCR_LIB */
/* ── cycle collection (scr_cycle.c) ───────────────────────────────────
* Reference counting alone cannot free cycles, so every object that can
* participate in one — capture boxes, heap closures, unions, promises, and
* compiler-emitted class/record shapes with cycle-capable fields — is
* allocated with a hidden header directly BEFORE the object: a trace
* function (enumerates the object's cycle-capable children), a teardown
* function (frees the object WITHOUT releasing traced children), and the
* trial-deletion bookkeeping (color + candidate-root buffer state). Types
* that can never be in a cycle — strings above all, plus arrays of scalar/
* string/bytes elements, dyn trees, and shapes whose fields are all acyclic
* — keep the lean 1-word `rc` header and pay nothing. Arrays and maps are
* cycle-capable exactly when their element/value type is (a record element
* can point back at the array holding it).
*
* The collector is synchronous Bacon–Rajan trial deletion: a release that
* leaves a candidate's rc above zero buffers it as a possible cycle root;
* collection walks the buffer (markGray: trial-decrement internal edges;
* scan: restore externally-referenced subgraphs; collectWhite: free the
* dead cycle members, releasing only edges that LEAVE the white set).
* It is GENERATIONAL: each header carries a generation, a pass names the
* oldest one it will walk, and objects that survive a pass are promoted out
* of the nursery so later nursery passes never re-walk them. That is what
* keeps a pass proportional to recent allocation rather than to the whole
* live heap — see the generation note in scr_cycle.c for the soundness
* argument and the schedule. Collection points: program exit (before the RC
* audit), event-loop quiescence, and the per-generation triggers.
* SCR_CYCLE_THRESHOLD pins the nursery trigger to a fixed candidate count.
* There is no concurrent or incremental collection.
*
* Contract for trace/teardown pairs (the compiler emits them for shapes,
* the runtime owns its own): trace(obj) visits exactly the strong
* references to children that themselves carry a cycle header (visit
* callbacks tolerate NULL and immortal children); the teardown releases
* exactly the complement (strings, arrays, acyclic shapes), frees internal
* buffers, and frees the block via scr_cyc_free. Every heap object's
* FIRST member is `size_t rc`, which is what lets the collector adjust
* counts generically.
*/
typedef void (*ScrTraceVisit)(void *child, void *ctx);
typedef void (*ScrTraceFn)(void *obj, ScrTraceVisit visit, void *ctx);
typedef void (*ScrCycFreeFn)(void *obj);
/* DOOMED is WHITE that collectWhite has already gathered: it stops the
* gather recursing twice, and distinguishes "about to be freed" from a
* survivor for the re-buffering step (see scr_cycle.c). */
enum {
SCR_CYC_BLACK = 0, SCR_CYC_PURPLE = 1, SCR_CYC_GRAY = 2, SCR_CYC_WHITE = 3,
SCR_CYC_DOOMED = 4
};
/* Generations. A candidate sits in the buffer named by its own `gen`, and a
* pass walks only objects at or below the generation it collects. */
enum { SCR_CYC_NURSERY = 0, SCR_CYC_MATURE = 1, SCR_CYC_NGENS = 2 };
typedef struct ScrCycHdr {
ScrTraceFn trace;
ScrCycFreeFn free_fn;
uint32_t color; /* SCR_CYC_* */
uint16_t buffered; /* 1 = sitting in its generation's candidate buffer */
uint16_t gen; /* SCR_CYC_NURSERY..SCR_CYC_MATURE (the walk filter) */
size_t buf_index; /* position there (O(1) removal when rc hits 0) */
} ScrCycHdr;
/* This layout is an ABI, not an implementation detail. The LLVM backend
* inlines scr_cyc_mark_live as a raw `store i32 0`: color is at obj-16 on
* 64-bit targets and obj-12 on wasm32. Three sites emit it —
* llvm/shapes.ts, llvm/classes.ts, and llvm/emitter.ts. Nothing but `color`
* may share those four bytes: a field placed in them is silently zeroed by
* every retain, which is invisible to the type system and to the C
* compiler. Hence the target-width assertions. */
#if UINTPTR_MAX == UINT64_MAX
_Static_assert(sizeof(ScrCycHdr) == 32, "LLVM backend expects a 32-byte cycle header");
_Static_assert(offsetof(ScrCycHdr, color) == 16,
"LLVM backend's inlined mark-live stores i32 0 at obj-16");
#elif UINTPTR_MAX == UINT32_MAX
_Static_assert(sizeof(ScrCycHdr) == 20, "LLVM backend expects a 20-byte cycle header");
_Static_assert(offsetof(ScrCycHdr, color) == 8,
"LLVM backend's inlined mark-live stores i32 0 at obj-12");
#endif
_Static_assert(sizeof(((ScrCycHdr *)0)->color) == 4,
"mark-live is an i32 store: color must own all four bytes");
_Static_assert(SCR_CYC_BLACK == 0,
"the emitted mark-live stores the LITERAL 0, not the enumerator "
"— reordering the colors would make every compiled retain write "
"the wrong one");
static inline ScrCycHdr *scr_cyc_hdr(void *obj) { return (ScrCycHdr *)obj - 1; }
/* Zeroed allocation with a cycle header in front; returns the OBJECT
* pointer (header at scr_cyc_hdr). Aborts on OOM. */
void *scr_cyc_alloc(size_t size, ScrTraceFn trace, ScrCycFreeFn free_fn);
void scr_cyc_free(void *obj); /* frees the block, header included */
/* RC hooks for cycle-headered types. on_release: rc was decremented and
* stayed above zero — buffer the object as a possible cycle root (may run
* a collection when the buffer crosses the threshold; the caller must not
* touch the object afterwards). on_dead: rc hit zero — drop any buffer
* entry BEFORE tearing the object down. mark_live: retain hook (a
* re-retained candidate is certainly not garbage). */
void scr_cyc_on_release(void *obj);
void scr_cyc_on_dead(void *obj);
static inline void scr_cyc_mark_live(void *obj) {
scr_cyc_hdr(obj)->color = SCR_CYC_BLACK;
}
/* Full sweep: trial-deletion passes over EVERY generation, to a fixpoint.
* This is the exit / session-reset entry point (the RC audit runs straight
* after and wants nothing reclaimable left), and it costs a walk of the
* live heap — do not put it on a per-turn path. */
void scr_collect_cycles(void);
/* One pass on the normal generational schedule, for callers that reach a
* natural collection point rather than a threshold (the event loop between
* turns). Cheap: usually a nursery pass; a waiting mature backlog ages into
* a bounded full pass so sparse roots cannot float forever. */
void scr_cyc_collect_scheduled(void);
/* ── class hierarchies (single inheritance) ───────────────────────────
* Classes in an `extends` hierarchy share a two-word object prefix: the
* usual `size_t rc`, then a pointer to the class's static vtable (emitted
* per class by the compiler; standalone classes carry no vtable word and
* none of this applies). A derived class embeds its base's fields as a
* layout PREFIX, so an upcast is a pointer reinterpret and base-field
* offsets agree through any static type.
*
* The vtable begins with this header; the compiler-emitted concrete
* per-hierarchy struct appends one member per virtual method slot (only
* methods actually overridden somewhere get slots — never-overridden
* methods keep direct static calls). `pre`/`post` are the class's preorder
* interval in the whole-program class forest: `x instanceof C` is the O(1)
* range check `C.pre <= x->vt->pre && x->vt->pre <= C.post`. `release` is
* the class's own whole-object release: releasing through a base-typed
* pointer must tear down the DERIVED object's fields, so the emitted
* release of every hierarchy class dispatches through the stored vtable
* (retain needs no dispatch — rc is at offset 0 in every layout). The
* cycle collector needs no vtable involvement: a cycle-capable hierarchy
* object's header trace/teardown are stamped with the concrete class's
* functions at allocation, which already is dynamic dispatch. */
typedef struct ScrVt {
size_t pre, post;
void (*release)(void *obj);
} ScrVt;
/* ── strings ──────────────────────────────────────────────────────────
* UTF-8 bytes, refcounted, immutable. rc == SIZE_MAX marks an immortal
* interned literal (emitted as a static object; retain/release are no-ops).
* data is NUL-terminated for C convenience; len excludes the NUL.
*
* cap is the usable byte capacity of data[] excluding the NUL (allocation
* is sizeof(ScrStr) + cap + 1); cap == len for interned literals and plain
* allocations. Spare capacity (cap > len) exists only on concat results so
* scr_str_concat can append in place when the left operand is uniquely
* owned (rc == 1) — observable immutability is preserved: a string with
* rc > 1 or rc == SIZE_MAX is never mutated.
*/
typedef struct ScrStr {
size_t rc;
size_t len;
size_t cap;
char data[];
} ScrStr;
ScrStr *scr_str_new(const char *bytes, size_t len); /* returns +1 */
/* Native callback boundary: copy and WHATWG-decode a UTF-8 span, replacing
* malformed subsequences with U+FFFD. NULL with len == 0 is an empty span. */
ScrStr *scr_str_from_utf8_lossy(const uint8_t *bytes, size_t len); /* +1 */
/* Internal allocators for buffer builders (scr_json.c): a +1 string with
* UNINITIALIZED data (the builder fills bytes, then len and the NUL), and
* an rc==1-only realloc that grows capacity in place. Both keep the RC
* audit's live count exact. */
ScrStr *scr_str_alloc_raw(size_t len, size_t cap);
ScrStr *scr_str_regrow(ScrStr *s, size_t newcap);
static inline ScrStr *scr_str_retain(ScrStr *s) {
if (s->rc != SIZE_MAX) s->rc++;
return s;
}
void scr_str_release(ScrStr *s); /* NULL-tolerant (uninitialized locals) */
/* Borrow both args, return +1. */
ScrStr *scr_str_concat(ScrStr *a, ScrStr *b);
bool scr_str_eq(ScrStr *a, ScrStr *b);
/* memcmp byte order == code-point order (see SEMANTICS.md: diverges from
* JS UTF-16 code-unit order only for non-BMP vs U+E000..U+FFFF). Returns
* <0, 0, >0. */
int scr_str_cmp(ScrStr *a, ScrStr *b);
/* ECMAScript string-list ordering: compare UTF-16 code units even though
* ScrStr stores well-formed UTF-8. Returns <0, 0, >0. */
int scr_str_cmp_u16(ScrStr *a, ScrStr *b);
/* ── class objects (classes as first-class values) ────────────────────
* Top-level classes use immortal templates. Evaluating a function-local
* class creates a fresh, traced object that owns its captured binding boxes.
* Every construct thunk takes the borrowed class object before source
* arguments. Instances of a local class retain that object for identity and
* method captures. `pre`/`post` still describe the compiled class hierarchy;
* local-class instanceof additionally compares the fresh object identity. */
typedef struct ScrClassObj {
size_t rc;
size_t pre, post;
void *ctor;
const ScrStr *name;
size_t ncaps;
size_t length;
struct ScrBox *caps[];
} ScrClassObj;
static inline ScrClassObj *scr_classobj_retain(ScrClassObj *c) {
if (c && c->rc != SIZE_MAX) {
c->rc++;
scr_cyc_mark_live(c);
}
return c;
}
void scr_classobj_release(ScrClassObj *c);
ScrClassObj *scr_classobj_new(const ScrClassObj *template, size_t ncaps);
void scr_classobj_trace_v(void *c, ScrTraceVisit visit, void *ctx);
/* void*-signature RC adapters (container slots) — scr_object.c. */
void *scr_classobj_retain_v(void *c);
void scr_classobj_release_v(void *c);
/* `X.name` (+1 — a no-op retain on the interned immortal). */
ScrStr *scr_classobj_name(ScrClassObj *c);
/* The keyed-write miss on a fixed-shape record: throws the catchable
* TypeError naming the key (JS would add the property — the documented
* monomorphic-struct divergence). scr_object.c. */
void scr_record_key_miss(ScrStr *k);
/* ── error objects (scr_error.c) ──────────────────────────────────────
* `Error` and its standard subclasses are a
* RUNTIME-PROVIDED hierarchy: ScrError lays out exactly like a compiler-
* emitted hierarchy class (rc, vt, then the fields), so `class MyError
* extends Error` compiles as an ordinary derived class whose struct embeds
* this prefix, and every vtable mechanism (base-typed release, preorder-
* interval instanceof) applies unchanged.
*
* The builtin classes' vtables live HERE as mutable globals because
* the runtime itself creates error instances (JSON/dynCheck/regex failures,
* the island bridge) — but their preorder intervals depend on the whole
* program's class forest, which only the compiler knows. Every emitted
* main() stamps pre/post into these vtables before user code runs (the
* defaults below only cover the moments before that), so runtime-made and
* compiler-made error objects always agree on instanceof.
*
* Cycle capability is hierarchy-uniform and program-dependent (a user
* subclass may hold closures): when the compiler's fixpoint marks the Error
* hierarchy cycle-capable, main() also calls scr_error_set_traced() and
* the runtime allocates its own error objects with collector headers. */
typedef struct ScrError {
size_t rc;
const ScrVt *vt;
ScrStr *name; /* "TypeError", or whatever the user assigned */
ScrStr *message; /* "" when constructed without one, like Node */
ScrStr *code; /* NULL = absent (Node: no `code` property); fs/exec
* throw sites stamp the errno name ("ENOENT"). Part of
* the layout prefix: the compiler's %Error class defs
* carry a matching third field, so user subclasses
* embed the slot and release it NULL-guarded. */
struct ScrDyn *error_cause; /* NULL = absent; dyn undefined = present */
bool cause_enumerable; /* assignment creates an enumerable own property */
} ScrError;
enum {
SCR_ERR_ERROR = 0,
SCR_ERR_TYPE = 1,
SCR_ERR_RANGE = 2,
SCR_ERR_SYNTAX = 3,
SCR_ERR_DOMEX = 4, /* DOMException — ScrDomException, the wider layout */
SCR_ERR_REFERENCE = 5,
SCR_ERR_EVAL = 6,
SCR_ERR_URI = 7,
SCR_ERR_COUNT = 8,
};
extern SCR_TL ScrVt scr_error_vts[SCR_ERR_COUNT]; /* indexed by SCR_ERR_*; main() stamps pre/post */
struct ScrDyn; /* full declaration below (the checked-dynamic tree section) */
/* DOMException: the ScrError prefix (identical member order — an upcast is
* a pointer reinterpret) plus the WebIDL slots. The extra slots are HIDDEN
* from the compiler's IR field list (user `extends DOMException` is fenced
* so no subclass layout ever overlaps them); reads go through the
* scr_domex_* accessors. */
typedef struct ScrDomException {
size_t rc;
const ScrVt *vt;
ScrStr *name; /* "Error" default, or the resolved WebIDL name */
ScrStr *message; /* "" when constructed without one */
ScrStr *code; /* the Node string-code slot (stays NULL here) */
struct ScrDyn *error_cause; /* shared ScrError prefix; unused by DOMException */
bool cause_enumerable;
double dom_code; /* the WebIDL legacy code (0 when the name is off-table) */
bool has_cause; /* the options form carried a `cause` member */
struct ScrDyn *cause; /* owned; NULL when has_cause is false */
} ScrDomException;
/* new DOMException(message?, nameOrOptions?) — both dyn args borrowed,
* NULL-tolerant (NULL = absent = the dyn undefined). Returns +1. Never
* throws (dyn ToString is total). Lives in scr_json.c (the args are dyn
* values); the dyn-free half below stays in scr_error.c so the error
* unit links without the checked-dynamic tree. */
ScrError *scr_domex_new(const struct ScrDyn *message, const struct ScrDyn *name_or_options);
/* scr_error.c's dyn-free DOMException half: the blank allocation the
* constructors fill, the WebIDL name→legacy-code table, and the cause
* teardown hook scr_json.c installs before any cause can exist. */
ScrError *scr_domex_alloc(void);
double scr_domex_code_of(const ScrStr *name);
void scr_domex_install_cause_drop(void (*fn)(void *obj));
double scr_domex_code(ScrError *e); /* borrowed receiver */
bool scr_domex_has_cause(ScrError *e); /* borrowed receiver */
struct ScrDyn *scr_domex_cause(ScrError *e); /* +1 (dyn undefined when absent) */
/* structuredClone of a DOMException: WebIDL serialization — name/message
* copy, the code re-derives, cause does not serialize. Borrowed receiver
* and options (validated; throws on bad options / non-empty transfer).
* +1, NULL after a throw. */
ScrError *scr_domex_clone(ScrError *e, const struct ScrDyn *options);
/* Throw a fresh DOMException through the exception cell (atob/btoa's
* InvalidCharacterError sites). Copies both C strings; the _str form
* takes ownership of the message. */
void scr_throw_domex(const char *name, const char *message);
void scr_throw_domex_str(const char *name, ScrStr *message);
/* Switch runtime-side error allocation to collector-headered (called from
* main() when the compiler's cycle fixpoint marks the hierarchy). */
void scr_error_set_traced(void);
/* Allocate + initialize (name = the kind's builtin name, message retained
* from the borrowed argument; NULL means ""). Returns +1. */
ScrError *scr_error_new(int kind, ScrStr *message);
/* Initialize the error prefix of an already-allocated (zeroed) object —
* the super(message) call of a compiled `extends Error` constructor. Both
* arguments are borrowed. */
void scr_error_init(void *obj, int kind, ScrStr *message);
/* ECMAScript constructor arguments: dyn undefined defaults the message
* to ""; an options object's present cause is retained. All args borrowed.
* Message conversion can throw; new returns NULL with an exception pending.
* Kept separate so runtime-only throw sites need not link scr_json.c. */
ScrError *scr_error_new_options(int kind, const struct ScrDyn *message, const struct ScrDyn *options);
void scr_error_init_options(void *obj, int kind, const struct ScrDyn *message, const struct ScrDyn *options);
void scr_error_install_cause_drop(void (*fn)(void *obj));
bool scr_error_has_cause(ScrError *e);
struct ScrDyn *scr_error_cause(ScrError *e); /* +1, undefined when absent */
void scr_error_set_cause(ScrError *e, struct ScrDyn *value); /* borrowed operands */
void scr_error_delete_cause(ScrError *e);
/* ECMA Error.prototype.toString: "", name, message, or "name: message".
* Borrows e, returns +1. */
ScrStr *scr_error_to_string(ScrError *e);
ScrError *scr_error_retain(ScrError *e);
void scr_error_release(ScrError *e); /* dispatches through e->vt */
void *scr_error_retain_v(void *e);
void scr_error_release_v(void *e);
void scr_error_trace(void *e, ScrTraceVisit visit, void *ctx); /* no headered children */
ScrTraceFn scr_error_trace_arg(void); /* &scr_error_trace when traced, else NULL */
/* True when a thrown hierarchy object (SCR_EXC_OBJ payload) is an Error —
* its vtable's preorder lies inside Error's stamped interval. */
bool scr_error_is(const void *obj);
/* Throw a fresh builtin error through the exception cell. scr_throw_error
* takes ownership of message; the _msg form copies the C string. The _named
* form (island bridge) takes ownership of both strings and picks the
* builtin vtable whose class name matches `name` (Error otherwise). */
void scr_throw_error(int kind, ScrStr *message);
void scr_throw_error_msg(int kind, const char *message, size_t len);
void scr_throw_error_named(ScrStr *name, ScrStr *message);
/* A read of a declare-d const nothing defines: throws Node's catchable
* ReferenceError "<name> is not defined". Borrows name; always throws. */
void scr_undef_global_read(ScrStr *name);
/* The `code` slot (NodeJS.ErrnoException's .code): set stamps a fresh
* string from the C literal (replacing any previous value); get answers
* +1 or NULL when absent (the compiler's undefined arm). The _msg_code
* thrower is scr_throw_error_msg with the code stamped on the payload. */
void scr_error_set_code(ScrError *e, const char *code);
ScrStr *scr_error_code(ScrError *e);
void scr_throw_error_msg_code(int kind, const char *message, size_t len, const char *code);
/* The compiler-resolved Node-parity throw (error.nodeThrow): builtin
* error of `kind`, `code` stamped when non-empty. Borrows both. */
void scr_throw_node_coded(double kind, const ScrStr *code, const ScrStr *msg);
/* ── string methods ─────────────────────────────────────────────────
* ECMA-262 observable semantics over UTF-8 storage with UTF-16 code-unit
* indices. A per-instance side cache keeps length/cursor state and sparse
* navigation checkpoints for large strings, making warmed
* non-local indexed operations bounded by one checkpoint interval. All
* double index/count arguments go through ToIntegerOrInfinity (NaN → 0, trunc
* toward zero, ±Infinity kept), exactly like JS. Every function borrows its
* ScrStr arguments; functions returning ScrStr* return a +1 reference.
*
* One documented divergence (JS lone surrogates are unrepresentable in
* well-formed UTF-8): where JS would produce a *lone surrogate* — charAt on
* half of an astral pair, or a slice boundary that splits a pair — the
* result contains U+FFFD (EF BF BD) in its place. Numeric results
* (charCodeAt) are NOT affected: they return the exact surrogate code unit
* value, computed from the code point.
*/
/* .length — number of UTF-16 code units (astral chars count as 2). */
double scr_str_utf16_len(ScrStr *s);
/* charCodeAt(i): the i-th UTF-16 code unit as a number; for astral chars
* returns the high (0xD800+) or low (0xDC00+) surrogate value depending on
* which half i addresses. NaN when ToIntegerOrInfinity(i) is outside
* [0, length) — note charCodeAt(1.5) is index 1, matching Node. */
double scr_str_char_code_at(ScrStr *s, double i);
/* indexOf(needle, fromIndex): UTF-16 index of the first occurrence at or
* after fromIndex (clamped to [0, length]), or -1. Empty needle returns the
* clamped fromIndex per spec. */
double scr_str_index_of(ScrStr *s, ScrStr *needle, double fromIndex);
/* lastIndexOf(needle): the one-argument form returns the last occurrence's
* UTF-16 index, or -1. Empty needle returns length. */
double scr_str_last_index_of(ScrStr *s, ScrStr *needle);
/* lastIndexOf(needle, position): search at or before the clamped UTF-16
* position. NaN starts at the end, including for an empty needle. */
double scr_str_last_index_of_from(ScrStr *s, ScrStr *needle, double position);
/* includes(needle) — no position argument. Empty needle → true. */
bool scr_str_includes(ScrStr *s, ScrStr *needle);
/* startsWith(needle) / endsWith(needle) — one-argument forms. */
bool scr_str_starts_with(ScrStr *s, ScrStr *needle);
bool scr_str_ends_with(ScrStr *s, ScrStr *needle);
/* Positioned forms clamp UTF-16 indices to [0, length]. The empty needle
* matches at every clamped position. */
bool scr_str_starts_with_from(ScrStr *s, ScrStr *needle, double position);
bool scr_str_ends_with_from(ScrStr *s, ScrStr *needle, double end_position);
/* slice(start, end): UTF-16 indices, negatives count from length, clamped
* to [0, length]; empty when start >= end. The frontend passes
* end = Infinity for the one-argument form. Boundaries that split an astral
* pair yield U+FFFD for the half character (divergence, see above).
* Returns +1. */
ScrStr *scr_str_slice(ScrStr *s, double start, double end);
/* substring: clamp-and-swap (negatives clamp to 0, start > end swaps). */
ScrStr *scr_str_substring(ScrStr *s, double start, double end);
/* repeat(count): count < 0 or Infinity is a JS RangeError; scriptc has no
* exceptions, so it prints "scriptc: RangeError: Invalid count value" to
* stderr and abort()s. Returns +1. */
ScrStr *scr_str_repeat(ScrStr *s, double count);
/* trim(): strips the exact JS WhiteSpace ∪ LineTerminator set from both
* ends (U+0009-U+000D, U+0020, U+00A0, U+1680, U+2000..U+200A, U+2028,
* U+2029, U+202F, U+205F, U+3000, U+FEFF). Returns +1. */
ScrStr *scr_str_trim(ScrStr *s);
/* toLowerCase()/toUpperCase(): ECMA-262 Default Case Conversion (final
* sigma included) via libunicode's lre_case_conv — implemented in
* scr_regex.c, so they link exactly into lre-using binaries (the compiler
* sets the regex link flag for case-conversion sites). Borrow s; +1. */
ScrStr *scr_str_to_lower(const ScrStr *s);
ScrStr *scr_str_to_upper(const ScrStr *s);
/* charAt(i): 1-code-unit string; out of range → empty string. Half of an
* astral pair → U+FFFD (divergence, see above). Returns +1. */
ScrStr *scr_str_char_at(ScrStr *s, double i);
/* The string iterator's step (for-of over strings): the full code-POINT
* character starting at UTF-16 index i — astral chars come back whole
* (two units); the consuming loop advances by the result's length.
* Out of range → empty string. Returns +1. */
ScrStr *scr_str_cp_at(ScrStr *s, double i);
/* trimStart()/trimEnd(): the one-sided halves of trim. Returns +1. */
ScrStr *scr_str_trim_start(ScrStr *s);
ScrStr *scr_str_trim_end(ScrStr *s);
/* split(separator[, limit]) with a STRING separator: limit uses ToUint32;
* the no-limit wrapper supplies 2^32-1. Empty separator
* splits into single UTF-16 code units (each half of an astral char is
* U+FFFD — divergence, see above); otherwise splits on every occurrence,
* empty pieces kept. Borrows both; returns a +1 string[] (SCR_ELEM_STR). */
struct ScrArr;
struct ScrArr *scr_str_split(ScrStr *s, ScrStr *sep);
struct ScrArr *scr_str_split_limit(ScrStr *s, ScrStr *sep, double limit);
/* padStart(maxLength, fill)/padEnd — ECMA StringPad, UTF-16 unit counts.
* Target at or below the length (or empty fill) returns the receiver
* (retained). A truncated fill that would split an astral char emits
* U+FFFD for the kept half. Oversized pads abort (the repeat() policy).
* Borrow both; return +1. */
ScrStr *scr_str_pad_start(ScrStr *s, double maxLength, ScrStr *fill);
ScrStr *scr_str_pad_end(ScrStr *s, double maxLength, ScrStr *fill);
/* isWellFormed()/toWellFormed(): storage is well-formed by invariant
* (lone surrogates became U+FFFD at their producers, SEMANTICS.md 2), so
* isWellFormed is constant true and toWellFormed the identity (per spec
* both are no-ops on well-formed input). Borrow s; toWellFormed +1. */
bool scr_str_is_well_formed(ScrStr *s);
ScrStr *scr_str_to_well_formed(ScrStr *s);
/* RegExp.escape(s) — ES2025 EncodeForRegExpEscape per code point: leading
* ASCII alphanumeric hex-escapes, SyntaxCharacters and '/' take a
* backslash, other punctuators/whitespace/line terminators hex-escape
* (\xNN / \uNNNN, lowercase), the rest passes through. Total (the spec's
* surrogate arm is unreachable over well-formed storage). Lives in
* scr_regex.c (no engine needed — the placement keeps the always-linked
* string TU out of hello-world's size class); use sites flip the regex
* link switch (moduleUsesRegex). Borrows; +1. */
ScrStr *scr_regexp_escape(ScrStr *s);
/* parseInt(s, radix) — ECMA-262 19.2.5 exactly: JS whitespace skip, sign,
* ToInt32 radix (0 → 10 with the 0x hex escape; outside 2..36 → NaN),
* longest digit prefix, exact value correctly rounded (overflow →
* ±Infinity). The frontend completes an omitted radix to 0. Borrows s. */
double scr_parse_int(ScrStr *s, double radix);
double scr_parse_float(ScrStr *s);
/* ToNumber(string) — ECMA-262 7.1.4.1 StringToNumber exactly: trim
* StrWhiteSpace (the JS set, line terminators included) from both ends,
* empty/whitespace-only → +0, then the WHOLE remaining span must be one
* StrNumericLiteral — a signed decimal literal ("Infinity" exact-case
* included, correctly rounded via strtod over the validated span) or an
* UNSIGNED 0x/0o/0b integer literal (exact value rounded to nearest-even;
* a sign on those is NaN, per grammar). Any trailing garbage → NaN.
* Borrows s; never throws. */
double scr_string_to_number(ScrStr *s);
/* encodeURIComponent — ECMA-262 Encode with the component unreserved set
* (ALPHA/DIGIT/- _ . ! ~ * ' ( )): every other byte of the UTF-8 string
* percent-encodes as uppercase %XX (the spec's per-code-point UTF-8
* encoding IS a byte scan over UTF-8 storage). Never throws — the spec's
* URIError is the unpaired surrogate, which well-formed UTF-8 cannot
* hold. Borrows s; result +1. */
ScrStr *scr_str_encode_uri_component(ScrStr *s);
/* decodeURIComponent — ECMA-262 Decode with the empty reserved set: %XX
* escapes decode bytewise (raw non-escape bytes copy through) and escaped
* multibyte sequences must be strictly valid UTF-8 (overlong forms,
* surrogates, >U+10FFFF refused — UTF8-decode without replacement). Bad
* hex or an invalid sequence THROWS the spec's catchable URIError ("URI
* malformed") and returns NULL. Borrows s; result +1. */
ScrStr *scr_str_decode_uri_component(ScrStr *s);
/* The non-throwing core of decodeURIComponent: NULL on malformed input
* instead of the URIError — the try/catch shape node:querystring's
* unescape wraps around decodeURIComponent (scr_qs.c's strict pass). */
ScrStr *scr_str_decode_uri_component_try(ScrStr *s);
/* ── node:querystring (scr_qs.c — LINK-GATED by moduleUsesQs; the
* scr_url_params.c precedent: pure data transforms, no loop hooks).
* querystring.escape needs no entry here: Node's qsEscape encodes exactly
* the component unreserved set, so the frontend lowers it to
* scr_str_encode_uri_component (always linked; a program using only
* escape never pulls this unit). */
/* querystring.unescape — Node's qsUnescape: strict decodeURIComponent
* first, and on failure the lenient legacy unescapeBuffer(s).toString()
* (valid %XX escapes decode to their byte, malformed escapes copy
* literally, every non-escape UTF-16 CODE UNIT truncates to its low byte
* — Node's Buffer element write — and the byte buffer decodes as UTF-8
* with U+FFFD replacement per maximal subpart, Buffer.toString's rule).
* Borrows s; result +1; never throws. */
ScrStr *scr_qs_unescape(const ScrStr *s);
/* querystring.parse — Node v24's scan state machine, byte-wise over the
* UTF-8 storage (equivalent to the code-unit scan for well-formed input:
* the machine compares sep/eq sequences positionally and treats '%'/'+'/
* hex as ASCII, and multibyte alignment is preserved). Decoded pairs land
* in `out` — the PURE-index-signature Dict record's overflow map (the
* emitters pass rec->sc_ovf) — grouped like Node's addKeyVal: a first
* value stores the `str_tag` union arm, a repeat REPLACES it with a
* two-element string[] wrapped in `arr_tag`, later repeats push. sep/eq
* fall back to "&"/"=" when empty (Node's falsy rule; the frontend
* completes omitted/null arguments to the defaults). max_keys is Node's
* rule exactly: > 0 caps the PAIR count (empty skipped segments count,
* like Node's --pairs), anything else (0, negatives, NaN) is unlimited;
* the frontend completes the omitted option to 1000. Only the DEFAULT
* decoder runs (custom decodeURIComponent options fence at compile time),
* so '+' means ' ' and segments decode with scr_qs_unescape's semantics
* only when they carry a full valid %XX triple (Node's encodeCheck).
* Borrows everything; never throws. */
typedef struct ScrMap ScrMap; /* full definition below (C11 repeat) */
void scr_qs_parse_into(ScrMap *out, const ScrStr *qs, const ScrStr *sep,
const ScrStr *eq, double max_keys, uint32_t str_tag,
uint32_t arr_tag);
/* node:util.parseArgs — the static checked-dynamic boundary. `config` is
* the documented JSON-safe configuration object; the returned dyn tree is
* a fresh ParsedResults object whose `values` child has null prototype.
* Borrows config; returns +1, or NULL with a Node-coded TypeError pending. */
struct ScrDyn *scr_util_parse_args(const struct ScrDyn *config);
/* querystring.stringify — Node's stringify over a borrowed dyn value (the
* frontend dynFroms the typed record; JS-world dyn values pass straight
* through). Non-object dyn values answer "" like Node; object keys iterate in
* JS own-key order (scr_dyn_obj_keys — array-index keys ascending first,
* then insertion order, Node's ObjectKeys). Values serialize per Node's
* encodeStringified: strings escape, finite numbers render shortest-
* roundtrip then escape ('1e+21' → '1e%2B21'), booleans are bare
* true/false, arrays expand to repeated keys (empty arrays emit NOTHING,
* key included), and everything else (null, undefined, nested objects/
* arrays, functions) is the empty value — key and eq still emitted.
* sep/eq fall back to "&"/"=" when empty (Node's `sep ||= '&'`). Result
* +1; never throws. */
ScrStr *scr_qs_stringify(const struct ScrDyn *obj, const ScrStr *sep,
const ScrStr *eq);
/* encodeURI — the same ECMA-262 Encode() with the reserved set and '#'
* kept unescaped (scr_encode_uri_impl's keep_reserved arm). Total by the
* well-formed-UTF-8 invariant. Borrows s; result +1. */
ScrStr *scr_encode_uri(ScrStr *s);
/* atob/btoa — the WHATWG base64 globals (Node globals since v16). The
* argument is a borrowed dyn value: WebIDL ToString runs here over the
* dyn kind (Node's atob(null) decodes "null"). scr_atob decodes
* forgiving-base64 (ASCII whitespace stripped, %4==0 strips up to two
* '=', %4==1 refuses, leftover bits discarded) into the latin1 code
* points as a UTF-8 string; malformed input THROWS the catchable
* DOMException InvalidCharacterError ("The string to be decoded is not
* correctly encoded.") and returns NULL. scr_btoa encodes the string's
* code points (any over U+00FF throws InvalidCharacterError "Invalid
* character"). scr_b64_missing_arg is the zero-argument call of either:
* always throws Node's TypeError [ERR_MISSING_ARGS] and returns NULL.
* Results +1. */
ScrStr *scr_atob(const struct ScrDyn *data);
ScrStr *scr_btoa(const struct ScrDyn *data);
ScrStr *scr_b64_missing_arg(void);
/* ── arrays ─────────────────────────────────────────────────────────
* Monomorphic, growable, refcounted. Elements live in 8-byte slots
* (doubles/bools/pointers via memcpy and casts); `elem` records what the
* slots hold so the array can release its own reference elements — ScrStr's
* layout is untouched (interned-literal statics depend on it), so the
* element kind lives here instead of in a shared object header. A separate
* presence bitset distinguishes a hole from a present `undefined`-carrying
* reference value. Very large indices stay in a sorted sparse side store;
* the dense slot array is retained for ordinary packed arrays.
*
* Index arguments are doubles (JS numbers). A valid array index is a
* non-negative integer below 2^32-1; reads must be < len and writes may
* extend len through a sparse hole. Noncanonical numeric keys are ordinary
* properties stored by their JavaScript number-to-string spelling. Anything
* else prints
* scriptc: RangeError: array index <i> out of bounds (length <n>)
* to stderr and abort()s in the proven-presence accessors. `scr_arr_has` is
* the shared presence query used before those accessors when the language
* surface permits an undefined result.
*
* Ownership: _get_ref returns +1 (retains before returning); _set_ref and
* _push_ref take ownership of the new element (and _set_ref releases the
* old one); _pop_ref transfers the element's reference out to the caller.
*/
typedef enum {
SCR_ELEM_F64,
SCR_ELEM_BOOL,
SCR_ELEM_STR, /* slots are ScrStr* — released with the array */
SCR_ELEM_ARR, /* slots are ScrArr* — released recursively */
SCR_ELEM_BYTES, /* slots are ScrBytes* — released with the array */
/* Slots are void* to values the runtime cannot lay out itself — records,
* class instances, unions, and cycle-capable inner arrays. The element
* type's RC entry points arrive at construction (scr_arr_new_ref) and are
* stored ONCE per array, the SCR_BOX_OBJ / map-value technique; every
* array is still monomorphic (one element type, one set of entry points). */
SCR_ELEM_REF,
} ScrElemKind;
typedef enum {
SCR_ARR_HOLE = 0,
SCR_ARR_VALUE = 1,
SCR_ARR_UNDEFINED = 2,
} ScrArrState;
typedef struct ScrArr {
size_t rc; /* SIZE_MAX = immortal (unused for arrays; kept per convention) */
size_t len;
size_t cap; /* dense capacity; sparse entries cover indices >= cap */
ScrElemKind elem;
/* SCR_ELEM_REF only; NULL for every other element kind. elem_trace is
* non-NULL exactly when the element type carries a cycle header: such
* arrays are CYCLE-CAPABLE (an element can point back at the array that
* owns it) and allocate with the hidden collector header — their trace
* visits every element, and the collector teardown frees only the slot
* storage (all elements are traced children). Arrays of acyclic ref
* elements and the historic element kinds keep the lean 1-word header. */
void *(*elem_retain)(void *);
void (*elem_release)(void *);
ScrTraceFn elem_trace;
uint64_t *data;
uint8_t *present; /* one ScrArrState per dense slot */
struct ScrArrSparseSlot *sparse;
size_t sparse_len;
size_t sparse_cap;
struct ScrArrProp *props; /* noncanonical numeric properties */
size_t prop_len;
size_t prop_cap;
} ScrArr;
typedef struct ScrArrSparseSlot {
size_t index;
uint64_t slot;
uint8_t state;
} ScrArrSparseSlot;
typedef struct ScrArrProp {
char *key;
size_t key_len;
uint64_t slot;
uint8_t state; /* SCR_ARR_VALUE or SCR_ARR_UNDEFINED */
} ScrArrProp;
ScrArr *scr_arr_new(ScrElemKind elem, size_t initial_cap); /* returns +1 */
/* SCR_ELEM_REF construction: retain/release must be non-NULL (the compiler
* passes the element type's `_v` adapters); trace is non-NULL iff the
* element type carries a cycle header (the array then allocates with the
* collector header — see the struct comment). Returns +1. */
ScrArr *scr_arr_new_ref(void *(*elem_retain)(void *),
void (*elem_release)(void *),
ScrTraceFn elem_trace, size_t initial_cap);
static inline ScrArr *scr_arr_retain(ScrArr *a) {
if (a->rc != SIZE_MAX) {
a->rc++;
if (a->elem_trace) scr_cyc_mark_live(a);
}
return a;
}
/* Releases ref elements (SCR_ELEM_STR/ARR/BYTES/REF) recursively at rc == 0.
* NULL-tolerant (uninitialized locals). Cycle-capable arrays (elem_trace
* non-NULL) feed the candidate-root buffer like every headered type. */
void scr_arr_release(ScrArr *a);
/* Trace entry point for cycle-capable arrays — stored in their collector
* header and passed wherever a container stores an ARRAY payload's RC entry
* points and the array type is cycle-capable (boxes, union arms, map
* values, promise payloads), exactly like scr_map_trace_v. */
void scr_arr_trace_v(void *a, ScrTraceVisit visit, void *ctx);
double scr_arr_len(ScrArr *a);
/* Shared hole/property query. Canonical indices answer from indexed storage;
* noncanonical numeric keys answer from the ordinary-property vector. Missing
* keys and indices >= length answer false without trapping; callers that need
* a value must still use the proven-presence get accessors below. */
bool scr_arr_has(const ScrArr *a, double i);
double scr_arr_state(const ScrArr *a, double i);
double scr_arr_next_present(const ScrArr *a, double start);
/* Set Array.prototype.length semantics for the supported native array
* surface. Growth creates holes without allocating a dense prefix; shrinking
* releases removed reference elements. Invalid lengths set a catchable
* RangeError and leave the array unchanged. */
void scr_arr_set_len(ScrArr *a, double length);
/* Copy one slot from src into dst, retaining reference elements when the
* source slot is present. The destination must already have a length that
* includes dst_index. Holes remain holes. */
void scr_arr_copy_index(ScrArr *dst, size_t dst_index, const ScrArr *src,
size_t src_index);
/* Copy a range while preserving holes. `dst_start` and `src_start` are the
* starts of equal-length ranges; with reverse=true the source range is
* copied in reverse order. Reference elements are retained. */
void scr_arr_copy_range(ScrArr *dst, size_t dst_start, const ScrArr *src,
size_t src_start, size_t count, bool reverse);
void scr_arr_copy_range_ex(ScrArr *dst, size_t dst_start, const ScrArr *src,
size_t src_start, size_t count, bool reverse,
bool materialize_holes);
/* Math.max(...xs) / Math.min(...xs) over an f64-element array: the JS
* fold (NaN poisons, ±0 by the JS preferences, empty → ∓Infinity). */
double scr_math_max_arr(ScrArr *a);
double scr_math_min_arr(ScrArr *a);
double scr_math_hypot_arr(ScrArr *a);
/* The scalar Math statics (scr_lib.c): min/max are the ECMA two-argument
* folds (NaN poisons; max prefers +0, min prefers -0 — NOT C's fmin/fmax);
* random is a uniform [0,1) double at 53-bit granularity from
* arc4random_buf (SEMANTICS.md 62: Node's distribution, not its sequence). */
double scr_math_min(double a, double b);
double scr_math_round(double x);
double scr_math_pow(double base, double exponent);
double scr_math_clz32(double x);
double scr_math_fround(double x);
double scr_math_imul(double a, double b);
double scr_math_max(double a, double b);
double scr_math_random(void);
double scr_arr_get_f64(ScrArr *a, double i); /* trap missing/hole */
/* ToNumber(a[i]) for f64 storage: borrows a; missing/hole/undefined -> NaN. */
double scr_arr_get_number(const ScrArr *a, double i);
/* Strict equality of ordinary indexed reads on matching primitive arrays.
* Borrows both arrays, preserves undefined vs NaN, and never allocates. */
bool scr_arr_index_eq(const ScrArr *a, double i, const ScrArr *b, double j);
bool scr_arr_get_bool(ScrArr *a, double i); /* trap missing/hole */
void *scr_arr_get_ref(ScrArr *a, double i); /* trap missing/hole; +1 */
/* i == len appends; the _ref variant releases the old element (when
* replacing) and takes ownership of the new one. */
void scr_arr_set_f64(ScrArr *a, double i, double v);
void scr_arr_set_bool(ScrArr *a, double i, bool v);
void scr_arr_set_ref(ScrArr *a, double i, void *v);
void scr_arr_set_undefined(ScrArr *a, double i);
bool scr_arr_delete(ScrArr *a, double i);
/* slice(start?, end?): a fresh +1 shallow copy of the index range —
* ToIntegerOrInfinity indices, negatives from the end, clamping; ref
* elements retain into the copy. Borrows a. */
ScrArr *scr_arr_slice(ScrArr *a, double start, double end);
/* ES2023 copying methods. All borrow their inputs and return fresh +1
* shallow copies. with() raises Node's catchable RangeError for an invalid
* relative index; the ref variant retains the borrowed replacement. */
ScrArr *scr_arr_to_reversed(const ScrArr *a);
ScrArr *scr_arr_to_spliced(const ScrArr *a, double start,
double delete_count, const ScrArr *items);
ScrArr *scr_arr_with_f64(ScrArr *a, double index, double value);
ScrArr *scr_arr_with_bool(ScrArr *a, double index, bool value);
ScrArr *scr_arr_with_ref(ScrArr *a, double index, void *value);
ScrArr *scr_arr_with_undefined(ScrArr *a, double index);
/* push returns the new length (JS-exact); _ref takes ownership. */
double scr_arr_push_f64(ScrArr *a, double v);
double scr_arr_push_bool(ScrArr *a, bool v);
double scr_arr_push_ref(ScrArr *a, void *v);
/* Append a same-element-kind array through the JS iterator contract: holes
* become present undefined states. The source is borrowed and self-spread
* duplicates the original indexed state. */
double scr_arr_push_spread(ScrArr *a, const ScrArr *src);
/* Append a concat argument by indexed property copy, preserving holes. */
double scr_arr_concat_copy(ScrArr *a, const ScrArr *src);
/* unshift returns the new length and _ref takes ownership. The spread form
* borrows a same-element-kind source, retains copied refs, and snapshots
* self-spread. reverse mutates in place and returns the receiver at +1. */
double scr_arr_unshift_f64(ScrArr *a, double v);
double scr_arr_unshift_bool(ScrArr *a, bool v);
double scr_arr_unshift_ref(ScrArr *a, void *v);
double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src);
ScrArr *scr_arr_reverse(ScrArr *a);
/* pop traps on an empty array; _ref transfers ownership out (+1 to the
* caller, no release). */
double scr_arr_pop_f64(ScrArr *a);
bool scr_arr_pop_bool(ScrArr *a);
void *scr_arr_pop_ref(ScrArr *a);
uint8_t scr_arr_pop_state(ScrArr *a, uint64_t *slot_out);
/* shift: the first element out, tail sliding down. The emitter guards the
* empty array (JS's undefined — the `elem | undefined` union), so empty
* here is an internal error; _ref transfers ownership out like pop. */
double scr_arr_shift_f64(ScrArr *a);
bool scr_arr_shift_bool(ScrArr *a);
void *scr_arr_shift_ref(ScrArr *a);
uint8_t scr_arr_shift_state(ScrArr *a, uint64_t *slot_out);
/* splice(start, deleteCount) — the REMOVAL forms: Node-exact
* relative/clamped start, count clamped to [0, len - start] (+Infinity =
* to the end). Returns the removed elements in order as a fresh +1 array,
* ownership MOVED out of the receiver. Borrows a. */
ScrArr *scr_arr_splice(ScrArr *a, double start, double deleteCount);
ScrArr *scr_arr_splice_insert(ScrArr *a, double start, double deleteCount,
const ScrArr *items);
ScrArr *scr_arr_flat_copy(const ScrArr *a, ScrArr *out, bool flatten);
/* indexOf: first index whose element strictly equals (JS ===) the needle,
* or -1. Per element kind: f64 by value (NaN never matches — NaN !== NaN;
* -0 matches 0), bool by value, SCR_ELEM_STR by CONTENT (JS strings are
* primitives), SCR_ELEM_ARR/SCR_ELEM_REF by reference identity (JS object
* equality; the compiler fences union-element arrays, whose boxes are
* compiler artifacts pointer identity would misjudge). The _ref variants
* BORROW the needle (no ownership change). */
double scr_arr_index_of_f64(ScrArr *a, double v);
double scr_arr_index_of_bool(ScrArr *a, bool v);
double scr_arr_index_of_ref(ScrArr *a, void *v);
/* includes: SameValueZero — exactly indexOf's equality EXCEPT NaN matches
* NaN (JS: [NaN].indexOf(NaN) === -1 but [NaN].includes(NaN) === true). */
bool scr_arr_includes_f64(ScrArr *a, double v);
bool scr_arr_includes_bool(ScrArr *a, bool v);
bool scr_arr_includes_ref(ScrArr *a, void *v);
/* join(sep): elements stringified exactly like JS String(x) — numbers via
* scr_f64_to_str (shortest roundtrip), booleans "true"/"false", strings
* verbatim — with sep between. f64/bool/str element kinds only (the
* compiler rejects the rest). Borrows both args; returns +1. */
ScrStr *scr_arr_join(ScrArr *a, ScrStr *sep);
/* String.raw over the raw literals and pre-stringified substitutions
* (both SCR_ELEM_STR): the spec's interleave — extra substitutions drop,
* missing ones skip. Borrows both; +1 result; never throws. */
ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs);
/* ── regular expressions (scr_regex.c — linked ONLY when the program
* contains a regex literal; see native-toolchain.ts) ────────────────────────────────
* The engine is quickjs-ng's libregexp (the same bytecode interpreter the
* dynamic island uses), compiled standalone. A ScrRegex is today ALWAYS an
* immortal interned literal: the compiler emits one static per distinct
* (pattern, flags) pair, exactly like string literals, and the bytecode is
* compiled lazily on first use (cached on the struct, freed at exit).
* Subjects are matched as UTF-16 (converted per call from the UTF-8
* storage), so all observable index behavior is UTF-16-exact like Node.
*
* Statefulness fence: /g and /y regexes carry mutable lastIndex in JS.
* Supported only where iteration is internal (replace/replaceAll/split);
* test() on a g/y-flagged regex aborts with a clear message.
*
* Ownership: subjects/replacements are BORROWED; string/array results
* return +1. replace_all without /g THROWS a catchable TypeError
* (callers are compiler-emitted pending
* checks); every other failure mode aborts.
*/
typedef struct ScrRegex {
size_t rc; /* SIZE_MAX = immortal (every regex literal) */
ScrStr *source; /* pattern text between the slashes */
ScrStr *flags; /* flags text, source order (alphabet fenced to gimsuy) */
uint8_t *bc; /* lazily compiled libregexp bytecode; NULL until first use */
} ScrRegex;
static inline ScrRegex *scr_regex_retain(ScrRegex *re) {
if (re->rc != SIZE_MAX) re->rc++;
return re;
}
void scr_regex_release(ScrRegex *re); /* NULL-tolerant */
void *scr_regex_retain_v(void *re);
void scr_regex_release_v(void *re);
/* new RegExp(pattern, flags): a heap ScrRegex over the same engine. The
* pattern compiles EAGERLY — an invalid pattern or flag throws Node's
* catchable SyntaxError at construction (detail text is libregexp's,
* approximate fidelity; e.name exact). An empty pattern stores the
* spec's "(?:)" source. Borrows both; +1, NULL after a throw. */
ScrRegex *scr_regex_new(ScrStr *pattern, ScrStr *flags);
bool scr_regex_test(ScrRegex *re, ScrStr *s); /* aborts on /g or /y */
/* s.match(re): +1 string[] of [whole, ...captures] (nonparticipating
* captures hold "" — SEMANTICS.md), or NULL for no match. Aborts on /g
* or /y like test(). Borrows both. */
ScrArr *scr_regex_match(ScrStr *s, ScrRegex *re);
/* s.search(re): the first match's UTF-16 index, or -1 — a fresh exec from
* position 0 (Symbol.search never touches lastIndex, so no flag fence:
* /g is irrelevant, /y anchors at 0). Borrows both; never throws. */
double scr_regex_search(ScrStr *s, ScrRegex *re);
ScrStr *scr_regex_source(ScrRegex *re); /* +1 */
ScrStr *scr_regex_flags(ScrRegex *re); /* +1 */
ScrStr *scr_regex_to_string(ScrRegex *re); /* +1 */
/* replace: first match without /g, every match with it. */
ScrStr *scr_regex_replace(ScrStr *s, ScrRegex *re, ScrStr *rep);
/* replaceAll: throws Node's TypeError when /g is missing (may-throw). */
ScrStr *scr_regex_replace_all(ScrStr *s, ScrRegex *re, ScrStr *rep);
/* split: captured substrings are interleaved with the split pieces;
* unmatched groups become present undefined elements, not holes.
* limit uses ToUint32; the no-limit wrapper supplies 2^32-1. */
ScrArr *scr_regex_split(ScrStr *s, ScrRegex *re);
ScrArr *scr_regex_split_limit(ScrStr *s, ScrRegex *re, double limit);
/* matchAll drained eagerly: +1 string[][] of honest match slices; throws
* Node's TypeError on a non-global regex (catchable). */
ScrArr *scr_regex_match_all(ScrStr *s, ScrRegex *re);
/* matchAll + the companion-index drain: also pushes each match's UTF-16
* start index (f64) onto `indices` — the .index the for-of-over-matchAll
* desugar reads. Same throw/result contract as scr_regex_match_all. */
ScrArr *scr_regex_match_all_into(ScrStr *s, ScrRegex *re, ScrArr *indices);
/* ── maps (scr_map.c) ───────────────────────────────────────────────
* ES Map<K, V> with the compact-dict layout: a dense, insertion-ordered
* entries array (deletions become tombstones; compaction happens on growth,
* and NEVER while an iteration is active) plus an open-addressing bucket
* table of entry indices. That is what gives JS's observable semantics for
* free: forEach visits in insertion order, entries added during iteration
* are visited (they append), deleted entries are skipped (tombstones), and
* a delete + re-add moves the key to the end (Node-verified).
*
* Keys are identity references, string (content), or number with SameValueZero: NaN equals NaN
* (canonicalized before hashing) and -0 is normalized to +0 at insertion,
* exactly like JS (a stored -0 key reads back as +0). Hash is FNV-1a over
* the reference address, string bytes, or canonicalized f64 bit pattern.
*
* Values are one uniform kind per map (like ScrArr elements): f64, bool, or
* a refcounted pointer whose RC entry points arrive as function pointers at
* construction (the SCR_BOX_OBJ technique — the runtime cannot know
* per-class/per-record layouts). key_trace and val_trace are non-NULL when
* their respective types carry cycle headers. Either side can point back
* at its map, so either adapter requires a hidden collector header. Trace
* visits each live edge on the traced sides; collector teardown releases
* the untraced complement. Maps with neither adapter use a lean RC header.
*
* Ownership: set BORROWS the key (the map retains string/reference keys it stores)
* and OWNS the value (+1 moves in; replacing releases the old value).
* get/has/delete borrow the key; get returns +1 on ref values (NULL = not
* found) or fills an out-param and returns a found flag for scalars.
* delete releases the entry's key and value. iter_key_str/ref and iter_val_ref
* return +1. iter_enter/iter_exit bracket a forEach loop: while the depth
* is nonzero, growth keeps tombstones (indices stay stable) and clear only
* tombstones entries — live-iteration semantics stay exact.
*/
/* REF keys hash/compare pointer identity and carry their own RC adapters.
* UNION_REF owns the union wrapper but hashes/compares its reference payload;
* the frontend permits only unions of reference-identity arms for this kind.
* DYN owns a checked-value box and compares its JavaScript value using
* SameValueZero, including reference identity for object payloads.
* These key kinds serve both Maps and Sets. */
typedef enum { SCR_MAP_KEY_F64, SCR_MAP_KEY_STR, SCR_MAP_KEY_REF, SCR_MAP_KEY_UNION_REF, SCR_MAP_KEY_DYN } ScrMapKeyKind;
typedef enum { SCR_MAP_VAL_F64, SCR_MAP_VAL_BOOL, SCR_MAP_VAL_REF } ScrMapValKind;
typedef struct {
uint64_t key; /* double bits (normalized), ScrStr* (owned), or ref ptr (owned) */
uint64_t val; /* double bits, bool, or owned pointer */
bool live; /* false = tombstone (key/val already released) */
} ScrMapEntry;
typedef struct ScrMap {
size_t rc; /* SIZE_MAX = immortal (unused for maps; kept per convention) */
ScrMapKeyKind key_kind;
ScrMapValKind val_kind;
/* SCR_MAP_VAL_REF only; val_trace non-NULL iff the value type carries a
* cycle header. The map has a header when either trace is non-NULL. */
void *(*val_retain)(void *);
void (*val_release)(void *);
ScrTraceFn val_trace;
/* SCR_MAP_KEY_REF, SCR_MAP_KEY_UNION_REF and SCR_MAP_KEY_DYN only. */
void *(*key_retain)(void *);
void (*key_release)(void *);
ScrTraceFn key_trace;
size_t nentries; /* dense entries used, tombstones included */
size_t nlive; /* live entries (Map.size) */
size_t ecap; /* entries capacity */
ScrMapEntry *entries;
size_t nbuckets; /* power of two; >= 2 * ecap so probes terminate */
size_t *buckets; /* entry indices; SIZE_MAX = empty */
size_t iter_depth; /* > 0: an iteration is active — no compaction */
} ScrMap;
/* retain/release/trace are NULL for scalar value kinds; retain/release are
* required for SCR_MAP_VAL_REF; trace is non-NULL iff the value type is
* cycle-capable (the map then allocates with the collector header). */
ScrMap *scr_map_new(ScrMapKeyKind key_kind, ScrMapValKind val_kind,
void *(*val_retain)(void *), void (*val_release)(void *),
ScrTraceFn val_trace); /* returns +1 */
/* Typed identity keys retain their original representation for iteration.
* UNION_REF compares the wrapped reference, so reboxing cannot change key
* identity. The compiler admits only unions of identity-bearing arms.
* Either trace adapter requires a collector header on the map itself. */
ScrMap *scr_map_new_typed(ScrMapKeyKind key_kind, ScrMapValKind val_kind,
void *(*key_retain)(void *), void (*key_release)(void *),
ScrTraceFn key_trace,
void *(*val_retain)(void *), void (*val_release)(void *),
ScrTraceFn val_trace);
ScrMap *scr_map_retain(ScrMap *m);
void scr_map_release(ScrMap *m); /* NULL-tolerant */
void *scr_map_retain_v(void *m);
void scr_map_release_v(void *m);
void scr_map_trace_v(void *m, ScrTraceVisit visit, void *ctx);
double scr_map_size(const ScrMap *m); /* live entries (Map.size) */
void scr_map_clear(ScrMap *m);
bool scr_map_has_f64(const ScrMap *m, double key);
bool scr_map_has_str(const ScrMap *m, const ScrStr *key);
bool scr_map_has_ref(const ScrMap *m, const void *key);
bool scr_map_delete_f64(ScrMap *m, double key);
bool scr_map_delete_str(ScrMap *m, const ScrStr *key);
bool scr_map_delete_ref(ScrMap *m, const void *key);
/* set: key borrowed (string/reference keys are retained when stored), value moves in
* for _ref (replacing releases the old value; the stored key is kept, like
* JS — only the value changes on overwrite). */
void scr_map_set_f64_f64(ScrMap *m, double key, double v);
void scr_map_set_f64_bool(ScrMap *m, double key, bool v);
void scr_map_set_f64_ref(ScrMap *m, double key, void *v);
void scr_map_set_str_f64(ScrMap *m, ScrStr *key, double v);
void scr_map_set_str_bool(ScrMap *m, ScrStr *key, bool v);
void scr_map_set_str_ref(ScrMap *m, ScrStr *key, void *v);
void scr_map_set_ref_f64(ScrMap *m, void *key, double v);
void scr_map_set_ref_bool(ScrMap *m, void *key, bool v);
void scr_map_set_ref_ref(ScrMap *m, void *key, void *v);
/* get: scalar variants fill *out and return the found flag; ref variants
* return +1 or NULL (values are never NULL, so NULL means "absent"). The
* compiler wraps the result into the `V | undefined` union type-directedly
* (the runtime knows no tags). */
bool scr_map_get_f64_f64(const ScrMap *m, double key, double *out);
bool scr_map_get_f64_bool(const ScrMap *m, double key, bool *out);
void *scr_map_get_f64_ref(const ScrMap *m, double key);
bool scr_map_get_str_f64(const ScrMap *m, const ScrStr *key, double *out);
bool scr_map_get_str_bool(const ScrMap *m, const ScrStr *key, bool *out);
void *scr_map_get_str_ref(const ScrMap *m, const ScrStr *key);
bool scr_map_get_ref_f64(const ScrMap *m, const void *key, double *out);
bool scr_map_get_ref_bool(const ScrMap *m, const void *key, bool *out);
void *scr_map_get_ref_ref(const ScrMap *m, const void *key);
/* Iteration primitives behind the compiler's forEach desugar: an index loop
* over the dense entries array, re-reading iter_count every pass (appends
* during iteration are visited) and skipping tombstones via iter_live.
* iter_key/iter_val on an out-of-range or dead index abort (the desugar
* guards with iter_live first — reaching one is a compiler bug). */
double scr_map_iter_count(const ScrMap *m); /* entries incl. tombstones */
bool scr_map_iter_live(const ScrMap *m, double i);
double scr_map_iter_key_f64(const ScrMap *m, double i);
ScrStr *scr_map_iter_key_str(const ScrMap *m, double i); /* +1 */
void *scr_map_iter_key_ref(const ScrMap *m, double i); /* +1 */
double scr_map_iter_val_f64(const ScrMap *m, double i);
bool scr_map_iter_val_bool(const ScrMap *m, double i);
void *scr_map_iter_val_ref(const ScrMap *m, double i); /* +1 */
void scr_map_iter_enter(ScrMap *m);
void scr_map_iter_exit(ScrMap *m);
/* Seeded Set construction (`new Set(values)`): add() every element of one
* borrowed T[] in order — duplicates keep their first insertion position
* (SameValueZero, exactly JS). `set` is a set-shaped map (f64 value kind,
* stored values 0); the array's element kind matches the set's key kind. */
void scr_set_add_all(ScrMap *set, ScrArr *values);
/* `[...set]` (scr_lib.c): the live entries drained into a fresh +1 elem[]
* in insertion order. Borrows the set; string elements retained in. */
ScrArr *scr_set_to_arr_f64(const ScrMap *s);
ScrArr *scr_set_to_arr_str(const ScrMap *s);
ScrArr *scr_set_to_arr_ref(const ScrMap *s);
/* REF-element Set construction: a set-shaped map whose keys are refcounted
* pointers under identity hashing (see SCR_MAP_KEY_REF above). The element
* type's `_v` adapters arrive once, the scr_arr_new_ref technique. */
ScrMap *scr_set_new_ref(void *(*elem_retain)(void *), void (*elem_release)(void *));
/* The live STRING keys of a map in JS OWN-KEY ORDER (Object.keys/values/
* entries over an index-signature record's overflow): canonical array
* indices first in ascending numeric order, then the rest in insertion
* order. Fresh ScrStr* array (+1); the map is borrowed. */
ScrArr *scr_map_keys_js_order(const ScrMap *m);
#ifdef SCR_RC_AUDIT
long scr_map_live_count(void);
#endif
/* ── closures ───────────────────────────────────────────────────────
* A closure is a function pointer plus the boxes of its captured bindings.
* A box is one shared, refcounted variable cell: every function that
* captures a binding (and the function that declared it) reads and writes
* the same box, which is what makes mutation through a closure visible
* everywhere (JS shared-binding semantics).
*
* The compiler emits the callee's C signature as (ScrClosure *env, params…);
* the callee borrows env->caps[] (the closure owns them). rc == SIZE_MAX
* marks an immortal closure (the compiler interns one per top-level
* function referenced as a value, so `f === f` is true like in JS).
*
* Ownership: box _get_ref returns +1; _set_ref releases the old value and
* takes ownership of the new one; releasing a box releases its contents;
* releasing a closure releases its boxes.
*/
typedef enum {
SCR_BOX_F64,
SCR_BOX_BOOL,
SCR_BOX_STR,
SCR_BOX_ARR,
SCR_BOX_FUNC,
SCR_BOX_OBJ, /* class instance: retain/release via the fn ptrs below */
} ScrBoxKind;
typedef struct ScrBox {
size_t rc;
ScrBoxKind kind;
/* SCR_BOX_OBJ only: class instances are per-class C structs the runtime
* knows nothing about, so the compiler supplies their RC entry points.
* obj_trace is non-NULL exactly when the payload type carries a cycle
* header (the box's own trace then visits the payload; NULL means the
* payload is acyclic and the box releases it itself at teardown). */
void *(*obj_retain)(void *);
void (*obj_release)(void *);
ScrTraceFn obj_trace;
uint64_t slot; /* double/bool/pointer via memcpy and casts */
} ScrBox;
ScrBox *scr_box_new(ScrBoxKind kind); /* returns +1, slot zeroed */
ScrBox *scr_box_new_obj(void *(*retain)(void *), void (*release)(void *),
ScrTraceFn trace);
static inline ScrBox *scr_box_retain(ScrBox *b) {
if (b->rc != SIZE_MAX) {
b->rc++;
scr_cyc_mark_live(b);
}
return b;
}
void scr_box_release(ScrBox *b); /* releases ref contents by kind; NULL-tolerant */
double scr_box_get_f64(ScrBox *b);
bool scr_box_get_bool(ScrBox *b);
void *scr_box_get_ref(ScrBox *b); /* returns +1 */
void scr_box_set_f64(ScrBox *b, double v);
void scr_box_set_bool(ScrBox *b, bool v);
void scr_box_set_ref(ScrBox *b, void *v); /* releases old, owns new */
typedef struct ScrClosure {
size_t rc; /* SIZE_MAX = immortal (interned top-level function value) */
void *fn;
size_t ncaps;
/* The function's OWN-PROPERTY table: a box whose payload is an OBJ
* ScrDyn (owned; lazily allocated by Object.defineProperties —
* test/common copying name/length onto the mustCall wrapper), NULL
* until first defined. It lives on the CLOSURE, not the dyn box:
* boxing the same function value twice (the declaration's box, the
* return's box) yields distinct boxes sharing one closure, and JS has
* ONE function object — property writes must be visible through every
* box. A ScrBox so this unit stays dyn-free (the payload's release
* rides the box, like every capture). Untraced by the cycle collector
* (like the dyn→closure edge): a cycle through it is merely never
* collected. */
ScrBox *props;
ScrBox *caps[];
} ScrClosure;
ScrClosure *scr_closure_new(void *fn, size_t ncaps); /* +1; caller fills caps with +1 box refs */
static inline ScrClosure *scr_closure_retain(ScrClosure *c) {
if (c->rc != SIZE_MAX) {
c->rc++;
scr_cyc_mark_live(c);
}
return c;
}
void scr_closure_release(ScrClosure *c); /* releases the boxes; NULL-tolerant */
/* ── outbound FFI retained callbacks (scr_ffi.c) ─────────────────────
* One compiler-emitted table per retained callback descriptor. For
* context-bearing descriptors, entries are counted rather than
* deduplicated: registering the same closure twice requires two matching
* releases. Raw singleton slots replace instead: commit_slot retires
* every pin the new registration superseded — a duplicate of the same
* closure included — so after any number of set calls exactly one
* release is pending. The table owns one closure reference per entry and
* joins a process-global teardown list on first use. Script-thread retained
* callbacks do not contribute event-loop liveness; foreign registrations do,
* and their queued invocations keep released closure pins alive until the
* script-thread dispatch has finished. */
typedef struct ScrFfiTable {
ScrClosure **entries;
size_t len;
size_t cap;
struct ScrFfiTable *next;
bool linked;
/* Raw retained singletons only: the compiler-emitted global the
* trampoline dispatches through. Teardown and release disarm it so a
* late native invocation hits the trampoline's NULL trap instead of a
* freed closure. NULL for context-bearing descriptors. */
ScrClosure **slot;
/* Foreign descriptors only. The global FFI queue lock protects these
* fields together with entries/len/cap. Retired pins were explicitly
* released while a queued invocation could still name them; dispatch
* drops them on the script thread after this table's queue reaches zero. */
ScrClosure **retired;
size_t retired_len;
size_t retired_cap;
size_t queued;
/* Reserved process-loop identity captured by queued calls. Executables use
* one global loop today; library mode can make this per instance later
* without changing the post/call ABI. */
void *loop;
/* Optional format-5 teardown. NULL keeps format-4 tables on the compact
* always-linked path; foreign tables point into scr_ffi_queue.c. */
void (*teardown)(struct ScrFfiTable *table);
} ScrFfiTable;
void scr_ffi_link(ScrFfiTable *table);
void scr_ffi_retain(ScrFfiTable *table, ScrClosure *callback);
void scr_ffi_retain_foreign(ScrFfiTable *table, ScrClosure *callback);
/* Raw singleton registration, split around the native set call:
* retain_slot pins the incoming closure BEFORE the call without touching
* the current registration; commit_slot repoints the slot and retires the
* superseded pins after the call returns. */
void scr_ffi_retain_slot(ScrFfiTable *table, ScrClosure **slot, ScrClosure *callback);
void scr_ffi_commit_slot(ScrFfiTable *table, ScrClosure *callback);
/* Traps unless the registration exists — emitted BEFORE a native release
* call so a bogus release cannot reach native code. */
void scr_ffi_require(ScrFfiTable *table, ScrClosure *callback);
void scr_ffi_release(ScrFfiTable *table, ScrClosure *callback);
void scr_ffi_release_optional(ScrFfiTable *table, ScrClosure *callback);
void scr_ffi_require_foreign(ScrFfiTable *table, ScrClosure *callback);
void scr_ffi_release_foreign(ScrFfiTable *table, ScrClosure *callback);
void scr_ffi_teardown(ScrFfiTable *table);
void scr_ffi_teardown_all(void);
/* Format-5 foreign-thread delivery. A generated native trampoline creates a
* plain malloc-backed call, stages scalar values or copied byte spans, and
* posts it. It never touches closure RC, exception cells, fibers, or other
* script-thread-only runtime state. The generated dispatch thunk runs later
* on the event loop and materializes script strings/bytes there. */
typedef struct ScrFfiCall ScrFfiCall;
typedef void (*ScrFfiDispatchFn)(ScrClosure *callback, ScrFfiCall *call);
ScrFfiCall *scr_ffi_call_new(ScrFfiTable *table, ScrClosure *callback,
ScrFfiDispatchFn dispatch, size_t nargs);
void scr_ffi_call_set_f64(ScrFfiCall *call, size_t index, double value);
void scr_ffi_call_set_bool(ScrFfiCall *call, size_t index, uint8_t value);
void scr_ffi_call_set_u8(ScrFfiCall *call, size_t index, uint8_t value);
void scr_ffi_call_set_u32(ScrFfiCall *call, size_t index, uint32_t value);
void scr_ffi_call_set_i32(ScrFfiCall *call, size_t index, int32_t value);
void scr_ffi_call_copy_cstring(ScrFfiCall *call, size_t index, const char *value);
void scr_ffi_call_copy_string(ScrFfiCall *call, size_t index,
const uint8_t *value, size_t len);
void scr_ffi_call_copy_bytes(ScrFfiCall *call, size_t index,
const uint8_t *value, size_t len);
void scr_ffi_post(ScrFfiCall *call);
double scr_ffi_call_get_f64(const ScrFfiCall *call, size_t index);
bool scr_ffi_call_get_bool(const ScrFfiCall *call, size_t index);
double scr_ffi_call_get_u8(const ScrFfiCall *call, size_t index);
double scr_ffi_call_get_u32(const ScrFfiCall *call, size_t index);
double scr_ffi_call_get_i32(const ScrFfiCall *call, size_t index);
const uint8_t *scr_ffi_call_get_data(const ScrFfiCall *call, size_t index);
size_t scr_ffi_call_get_len(const ScrFfiCall *call, size_t index);
void scr_ffi_install(void);
void scr_ffi_stop(void);
void scr_ffi_teardown_foreign(ScrFfiTable *table);
void scr_loop_set_ffi(bool (*pending)(void), bool (*dispatch)(void),
int (*pollfd)(void), void (*stop)(void));
/* ── unions ─────────────────────────────────────────────────────────
* A union value (`A | B`) is an IMMUTABLE tagged box: a refcounted header,
* the arm's tag (its index in the compiler's canonical arm order), and one
* 8-byte payload slot (double/bool/pointer via memcpy, like ScrBox and
* ScrArr slots). Reassigning a union-typed variable constructs a NEW box —
* immutability keeps the RC story trivial.
*
* Scalar arms (f64/bool) live in the slot with NULL RC entry points. Ref
* arms OWN their payload (+1 moves in at construction) and carry the arm's
* retain/release as function pointers, exactly like SCR_BOX_OBJ: the
* runtime cannot know per-class/per-record struct layouts, so the compiler
* supplies its emitted `_v` adapters (and the runtime provides `_v`
* adapters for its own refcounted kinds below).
*
* Ownership: _new_ref takes ownership of the payload; scr_union_peek
* BORROWS the payload pointer (the compiler retains through the arm's
* concrete helper when it needs +1); releasing the union releases the
* payload through the stored fn ptr.
*/
typedef struct ScrUnion {
size_t rc; /* SIZE_MAX = immortal (unused for unions; kept per convention) */
uint32_t tag;
/* Ref arms only: the payload's RC entry points. NULL for scalar arms.
* arm_trace is non-NULL exactly when the arm type carries a cycle header
* (like ScrBox.obj_trace: the union's trace then visits the payload). */
void *(*arm_retain)(void *);
void (*arm_release)(void *);
ScrTraceFn arm_trace;
uint64_t slot; /* double/bool/pointer via memcpy and casts */
} ScrUnion;
ScrUnion *scr_union_new_f64(uint32_t tag, double v); /* returns +1 */
ScrUnion *scr_union_new_bool(uint32_t tag, bool v); /* returns +1 */
/* Takes ownership of v (+1 moves in); retain/release must be non-NULL;
* trace is non-NULL iff the arm type carries a cycle header. */
ScrUnion *scr_union_new_ref(uint32_t tag, void *v,
void *(*retain)(void *),
void (*release)(void *),
ScrTraceFn trace);
static inline ScrUnion *scr_union_retain(ScrUnion *u) {
if (u->rc != SIZE_MAX) {
u->rc++;
scr_cyc_mark_live(u);
}
return u;
}
void scr_union_release(ScrUnion *u); /* releases a ref payload; NULL-tolerant */
double scr_union_get_f64(ScrUnion *u);
bool scr_union_get_bool(ScrUnion *u);
/* BORROWED payload pointer of a ref arm (no ownership change). */
static inline void *scr_union_peek(const ScrUnion *u) {
void *p;
memcpy(&p, &u->slot, sizeof p);
return p;
}
/* void*-signature RC adapters for the runtime's own refcounted kinds —
* stored in ScrUnion (ref arms) and ScrBox (SCR_BOX_OBJ-style union boxes)
* alongside the compiler's emitted per-class/per-record `_v` adapters. */
void *scr_str_retain_v(void *s);
void scr_str_release_v(void *s);
void *scr_arr_retain_v(void *a);
void scr_arr_release_v(void *a);
void *scr_closure_retain_v(void *c);
void scr_closure_release_v(void *c);
void *scr_union_retain_v(void *u);
void scr_union_release_v(void *u);
/* Trace entry points for the runtime's cycle-headered kinds — passed
* wherever a container stores a payload's RC entry points (ScrBox obj
* slots, union ref arms, promise payloads, the exception cell) and the
* payload type is one of these. The compiler passes its emitted per-shape
* trace for cycle-capable classes/records, and NULL for acyclic types. */
void scr_closure_trace_v(void *c, ScrTraceVisit visit, void *ctx);
/* ── node:events EventEmitter (scr_events_emitter.c, link-gated) ──────
* The runtime-provided emitter base class — the ScrError precedent:
* ScrEmitter lays out exactly like a compiler-emitted hierarchy class
* (rc, vt, then the prefix fields), so `class My extends EventEmitter`
* compiles as an ordinary derived class whose struct embeds this prefix
* (`ScrEeReg *` then `const char *`, stamped by the emitted allocation),
* and every vtable mechanism applies unchanged. The vtable of BARE
* emitters lives here as a mutable global; the emitted main() stamps its
* preorder interval (the scr_error_vts story). The emitter hierarchy is
* UNCONDITIONALLY cycle-capable — the registry owns listener closures —
* so bare instances always allocate collector-headered and emitted
* subclasses are always in the traced set.
*
* Dispatch: listeners are invoked through compiler-emitted va_list
* adapters; scr_emitter_emit's variadic tail carries the event's typed
* argument tuple (frontend-unified per event name), every argument
* BORROWED — each adapter retains the +1 its callee owns. emit and
* emit_error may leave an exception pending (a listener threw; an
* unhandled 'error' throws its payload). */
typedef struct ScrEeReg ScrEeReg; /* the listener registry (lazy) */
typedef void (*ScrEeInvoke)(ScrClosure *cb, va_list ap);
typedef struct ScrEmitter {
size_t rc;
const ScrVt *vt;
ScrEeReg *reg; /* NULL until the first listener/setMaxListeners */
const char *cls; /* display name for the leak warning ("EventEmitter") */
} ScrEmitter;
extern SCR_TL ScrVt scr_emitter_vt; /* main() stamps pre/post */
extern SCR_TL double scr_emitter_default_max;
ScrEmitter *scr_emitter_new(void); /* +1, collector-headered */
void scr_emitter_init(void *obj); /* super() into the prefix (no-op today) */
ScrEmitter *scr_emitter_retain(ScrEmitter *em);
void scr_emitter_release(ScrEmitter *em); /* dispatches through em->vt */
void *scr_emitter_retain_v(void *em);
void scr_emitter_release_v(void *em);
void scr_emitter_trace(void *obj, ScrTraceVisit visit, void *ctx);
/* The emitted-subclass prefix helpers (the compiler's direct release /
* trace / collector teardown call these for the embedded registry). */
void scr_emitter_reg_drop(ScrEeReg *reg);
void scr_emitter_reg_trace(ScrEeReg *reg, ScrTraceVisit visit, void *ctx);
void scr_emitter_reg_gcfree(ScrEeReg *reg);
/* on/once/prependListener/prependOnceListener: cb MOVES, name borrowed;
* returns em +1 (the `return this` chaining value). 'newListener' fires
* BEFORE the add; the leak warning prints at the crossing add. */
ScrEmitter *scr_emitter_on(ScrEmitter *em, ScrStr *name, ScrClosure *cb /*moves*/,
ScrEeInvoke inv, bool once, bool prepend);
/* The dyn-adapted registration family (JS-lane checked-dynamic
* listeners). check_listener throws Node's ERR_INVALID_ARG_TYPE TypeError
* (catchable, pending on return) when cb is not a function value; the
* emitted registration helper calls it FIRST, so on_dyn/off_dyn take the
* SCR_DYN_FUNC kind as given. on_dyn registers the compiler-built adapter
* (what emit invokes — it boxes the tuple to dyn and calls the original
* through the checked-dynamic machinery) and keeps the original (the dyn
* box's underlying closure) as the entry's IDENTITY: off/removeListener
* match it, listenerCount(name, fn) counts it, listeners() answers it.
* cb is borrowed, adapter MOVES; both return em +1 (chaining). */
typedef struct ScrDyn ScrDyn; /* full definition below (C11 repeat) */
void scr_emitter_check_listener(const ScrDyn *cb);
ScrEmitter *scr_emitter_on_dyn(ScrEmitter *em, ScrStr *name, const ScrDyn *cb,
ScrClosure *adapter /*moves*/,
ScrEeInvoke inv, bool once, bool prepend);
/* Computed-name listeners retain the original closure and its checked-
* dynamic call thunk, preserving the number of arguments passed to emit.
* The frontend keeps these names disjoint from fixed-tuple event names. */
ScrEmitter *scr_emitter_on_flex(ScrEmitter *em, ScrStr *name, const ScrDyn *cb,
bool once, bool prepend);
ScrEmitter *scr_emitter_off_dyn(ScrEmitter *em, ScrStr *name, const ScrDyn *cb);
/* The adapter-mediated registration (the LLVM backend's listeners): the
* ADAPTER closure is what emit invokes, the ORIGINAL closure is the
* entry's identity (the on_dyn split, with a plain-closure identity).
* Both MOVE in; returns em +1 (chaining). */
ScrEmitter *scr_emitter_on_via(ScrEmitter *em, ScrStr *name, ScrClosure *orig /*moves*/,
ScrClosure *adapter /*moves*/,
ScrEeInvoke inv, bool once, bool prepend);
/* Fixed-arity invoke shims for backends that cannot read a va_list
* (LLVM textual IR is not va_arg-portable): shim k reads k POINTER-SIZE
* slots off the emit tuple and calls cb->fn behind the fixed signature
* `void (ScrClosure *, void * ×k)` — the fixed-signature adapter the
* backend provides. Such backends pass every user tuple argument
* pointer-classed at the emit site: scalar values point at call-lived
* typed stack slots and reference values ride directly. The runtime's own
* emits (data/error/pipe/meta) carry pointers only, so the shims read every
* tuple either lane produces on 32- and 64-bit targets.
* SCR_EE_FIXED_MAX is the registry's audited arity ceiling — backends
* refuse listeners past it rather than guess. */
#define SCR_EE_FIXED_MAX 4
void scr_ee_inv_fixed0(ScrClosure *cb, va_list ap);
void scr_ee_inv_fixed1(ScrClosure *cb, va_list ap);
void scr_ee_inv_fixed2(ScrClosure *cb, va_list ap);
void scr_ee_inv_fixed3(ScrClosure *cb, va_list ap);
void scr_ee_inv_fixed4(ScrClosure *cb, va_list ap);
/* removeListener/off (LAST matching occurrence, Node's search order) and
* removeAllListeners; both return em +1. 'removeListener' fires after
* each removal when listened for. */
ScrEmitter *scr_emitter_off(ScrEmitter *em, ScrStr *name, ScrClosure *cb /*borrowed*/);
ScrEmitter *scr_emitter_remove_all(ScrEmitter *em, ScrStr *name, bool all);
/* emit: snapshot dispatch in listener order (once entries leave before
* running); returns whether the event had listeners. The _error form is
* emit('error', err): no listener ⇒ THROWS err (borrowed; +1 taken). */
bool scr_emitter_emit(ScrEmitter *em, ScrStr *name, ...);
bool scr_emitter_emit_flex(ScrEmitter *em, ScrStr *name, ScrDyn *const *args, size_t argc);
bool scr_emitter_emit_error(ScrEmitter *em, ScrStr *name, ScrError *err);
double scr_emitter_listener_count(ScrEmitter *em, ScrStr *name);
double scr_emitter_listener_count_fn(ScrEmitter *em, ScrStr *name, ScrClosure *fn);
double scr_emitter_listener_count_dyn(ScrEmitter *em, ScrStr *name, const ScrDyn *fn);
ScrArr *scr_emitter_event_names(ScrEmitter *em); /* +1 string[] */
/* Pre-create a name's eventNames() rank with no listener (Node's stream
* classes pre-create their known _events keys; empty = absent for every
* other read). The stream constructors call this. */
void scr_emitter_reserve(ScrEmitter *em, const char *name);
ScrArr *scr_emitter_listeners(ScrEmitter *em, ScrStr *name); /* +1 closures */
ScrEmitter *scr_emitter_set_max(ScrEmitter *em, double n); /* returns em +1 */
double scr_emitter_get_max(ScrEmitter *em);
void scr_emitter_set_default_max(double n);
double scr_emitter_get_default_max(void);
/* True when the emitter has at least one listener for `name` — the
* stream unit's registration probe (and anyone else's). */
bool scr_emitter_has(ScrEmitter *em, const char *name);
/* Post-registration hook (one registrant: scr_stream_install) — called at
* the END of scr_emitter_on with the emitter and the event name, so the
* stream unit can start flowing on the first 'data' listener. NULL when
* the stream unit is not linked; emitter behavior is byte-identical. */
extern void (*scr_emitter_on_hook)(ScrEmitter *em, ScrStr *name);
/* ── node:stream (scr_stream.c, link-gated by moduleUsesStream) ───────
* The runtime-provided stream classes (Readable/Writable/Duplex/
* Transform/PassThrough) behind the options-object constructor forms.
* ONE layout for all five: the ScrEmitter prefix (so every emitter
* mechanism — listener registry, emit dispatch, upcasts — applies
* unchanged) plus a lazily-detailed state block. User `extends` of these
* classes compiles (phase 2): the emitted subclass struct embeds this
* full prefix (one extra slot over an emitter subclass — the state
* pointer), allocation stamps the SUBCLASS vtable and display name and
* leaves st NULL, and the constructor's super(options) call lands on a
* scr_stream_init_* entry below. Stream identity for such instances is
* preorder-interval membership under a runtime stream vtable (main()
* stamps the intervals, the emitter-vt story).
*
* Event timing: Node defers most stream emissions by a process.nextTick.
* Here those deferrals ride a dedicated TICK QUEUE drained at the top of
* every loop turn (before the events/net/timer stations — the closest
* station to nextTick; SEMANTICS.md documents the microtask-order
* divergence), registered via scr_loop_set_stream at install. Emits run
* user code synchronously on the dispatching stack and may leave an
* exception pending, exactly like scr_emitter_emit — an unhandled
* 'error' event throws its payload (Node's crash).
*
* Ownership: constructors take their option callbacks +1 (moves);
* chunks are BORROWED at push/write (the buffer retains its copy);
* chaining forms return the receiver +1; read()/errored answer +1 or
* NULL. The *_done completion entries take err/data +1 (moves) — they
* are called by compiler-emitted completion-callback closures. */
typedef struct ScrStreamState ScrStreamState;
typedef struct ScrBytes ScrBytes; /* full definition below (C11 repeat) */
typedef struct ScrStream {
size_t rc;
const ScrVt *vt;
ScrEeReg *reg; /* ScrEmitter prefix */
const char *cls; /* ScrEmitter prefix (display name) */
ScrStreamState *st;
} ScrStream;
extern ScrVt scr_readable_vt, scr_writable_vt, scr_duplex_vt,
scr_transform_vt, scr_passthrough_vt; /* main() stamps pre/post */
/* Compiler-emitted option-callback invoke adapters (the leading-`this`
* closures): the runtime calls the user's read/write/final/destroy/
* transform/flush through these. */
typedef void (*ScrStreamReadInv)(ScrClosure *cb, ScrStream *s, double size);
typedef void (*ScrStreamChunkInv)(ScrClosure *cb, ScrStream *s, ScrBytes *chunk);
typedef void (*ScrStreamPlainInv)(ScrClosure *cb, ScrStream *s);
typedef void (*ScrStreamErrInv)(ScrClosure *cb, ScrStream *s, ScrError *err);
/* Constructors (+1 result; option callbacks MOVE; NULL = absent). hwm < 0
* means the byte default (65536 — Node 24's getDefaultHighWaterMark). */
ScrStream *scr_stream_new_readable(double hwm, bool auto_destroy, bool emit_close,
ScrClosure *read, ScrStreamReadInv read_inv,
ScrClosure *destroy, ScrStreamErrInv destroy_inv);
ScrStream *scr_stream_new_writable(double hwm, bool auto_destroy, bool emit_close,
ScrClosure *write, ScrStreamChunkInv write_inv,
ScrClosure *final_cb, ScrStreamPlainInv final_inv,
ScrClosure *destroy, ScrStreamErrInv destroy_inv);
ScrStream *scr_stream_new_duplex(double rhwm, double whwm, bool auto_destroy,
bool emit_close, bool allow_half_open, bool readable_side, bool writable_side,
ScrClosure *read, ScrStreamReadInv read_inv,
ScrClosure *write, ScrStreamChunkInv write_inv,
ScrClosure *final_cb, ScrStreamPlainInv final_inv,
ScrClosure *destroy, ScrStreamErrInv destroy_inv);
ScrStream *scr_stream_new_transform(double rhwm, double whwm, bool auto_destroy,
bool emit_close, bool allow_half_open, bool readable_side, bool writable_side,
ScrClosure *transform, ScrStreamChunkInv transform_inv,
ScrClosure *flush, ScrStreamPlainInv flush_inv,
ScrClosure *destroy, ScrStreamErrInv destroy_inv);
ScrStream *scr_stream_new_passthrough(double rhwm, double whwm, bool auto_destroy,
bool emit_close, bool allow_half_open, bool readable_side, bool writable_side,
ScrClosure *transform, ScrStreamChunkInv transform_inv,
ScrClosure *flush, ScrStreamPlainInv flush_inv,
ScrClosure *destroy, ScrStreamErrInv destroy_inv);
/* Subclass initialization (the emitted super(options) call over an
* ALREADY-ALLOCATED subclass struct — same tails as the constructors;
* receiver borrowed, callbacks move). Overridden underscore methods
* (_read/_write/...) arrive as compiler-synthesized wrapper closures
* that dispatch through the receiver's vtable. */
void scr_stream_init_readable(ScrStream *s, double hwm, bool auto_destroy, bool emit_close,
ScrClosure *read, ScrStreamReadInv read_inv,
ScrClosure *destroy, ScrStreamErrInv destroy_inv);
void scr_stream_init_writable(ScrStream *s, double hwm, bool auto_destroy, bool emit_close,
ScrClosure *write, ScrStreamChunkInv write_inv,
ScrClosure *final_cb, ScrStreamPlainInv final_inv,
ScrClosure *destroy, ScrStreamErrInv destroy_inv);
void scr_stream_init_duplex(ScrStream *s, double rhwm, double whwm, bool auto_destroy,
bool emit_close, bool allow_half_open, bool readable_side, bool writable_side,
ScrClosure *read, ScrStreamReadInv read_inv,
ScrClosure *write, ScrStreamChunkInv write_inv,
ScrClosure *final_cb, ScrStreamPlainInv final_inv,
ScrClosure *destroy, ScrStreamErrInv destroy_inv);
void scr_stream_init_transform(ScrStream *s, double rhwm, double whwm, bool auto_destroy,
bool emit_close, bool allow_half_open, bool readable_side, bool writable_side,
ScrClosure *transform, ScrStreamChunkInv transform_inv,
ScrClosure *flush, ScrStreamPlainInv flush_inv,
ScrClosure *destroy, ScrStreamErrInv destroy_inv);
void scr_stream_init_passthrough(ScrStream *s, double rhwm, double whwm, bool auto_destroy,
bool emit_close, bool allow_half_open, bool readable_side, bool writable_side,
ScrClosure *transform, ScrStreamChunkInv transform_inv,
ScrClosure *flush, ScrStreamPlainInv flush_inv,
ScrClosure *destroy, ScrStreamErrInv destroy_inv);
/* The dyn-options twins (a checked-dynamic options record at
* construction — `super(options)` forwarding a JS-lane parameter, `new
* Readable(dynVar)`): the option walk runs at runtime with Node's own
* reading rules (unknown keys ignored; consumed-but-unlowered options
* throw the loud unsupported Error — MAY THROW, NULL result / no init
* with the exception pending). opts is BORROWED; the fallback closures
* (a subclass's overridden underscore-method wrappers, shadowed by
* options callbacks per Node's instance-property rule) MOVE. */
ScrStream *scr_stream_new_readable_dyn(struct ScrDyn *opts);
ScrStream *scr_stream_new_writable_dyn(struct ScrDyn *opts);
ScrStream *scr_stream_new_duplex_dyn(struct ScrDyn *opts);
ScrStream *scr_stream_new_transform_dyn(struct ScrDyn *opts);
ScrStream *scr_stream_new_passthrough_dyn(struct ScrDyn *opts);
void scr_stream_init_readable_dyn(ScrStream *s, struct ScrDyn *opts,
ScrClosure *read_fb, ScrStreamReadInv read_fb_inv,
ScrClosure *destroy_fb, ScrStreamErrInv destroy_fb_inv);
void scr_stream_init_writable_dyn(ScrStream *s, struct ScrDyn *opts,
ScrClosure *write_fb, ScrStreamChunkInv write_fb_inv,
ScrClosure *final_fb, ScrStreamPlainInv final_fb_inv,
ScrClosure *destroy_fb, ScrStreamErrInv destroy_fb_inv);
void scr_stream_init_duplex_dyn(ScrStream *s, struct ScrDyn *opts,
ScrClosure *read_fb, ScrStreamReadInv read_fb_inv,
ScrClosure *write_fb, ScrStreamChunkInv write_fb_inv,
ScrClosure *final_fb, ScrStreamPlainInv final_fb_inv,
ScrClosure *destroy_fb, ScrStreamErrInv destroy_fb_inv);
void scr_stream_init_transform_dyn(ScrStream *s, struct ScrDyn *opts,
ScrClosure *transform_fb, ScrStreamChunkInv transform_fb_inv,
ScrClosure *flush_fb, ScrStreamPlainInv flush_fb_inv,
ScrClosure *destroy_fb, ScrStreamErrInv destroy_fb_inv);
void scr_stream_init_passthrough_dyn(ScrStream *s, struct ScrDyn *opts,
ScrClosure *transform_fb, ScrStreamChunkInv transform_fb_inv,
ScrClosure *flush_fb, ScrStreamPlainInv flush_fb_inv,
ScrClosure *destroy_fb, ScrStreamErrInv destroy_fb_inv);
/* stream.finished(s, cb) — the callback form: cb fires once at the
* stream's terminal point (right after 'close'; asynchronously when
* already closed) through the ErrInv ABI with the finish status (NULL /
* the error / ERR_STREAM_PREMATURE_CLOSE). Returns the +1 CLEANUP
* closure (a () => void unhooking the callback). cb MOVES. A registered
* watcher marks lifecycle errors handled (no unhandled-'error' crash —
* Node's eos registers listeners). */
ScrClosure *scr_stream_finished(ScrStream *s, ScrClosure *cb, ScrStreamErrInv inv);
/* The dyn-valued finished/pipeline callback inv (a mustCall wrapper in
* the slot): success calls with NO arguments, errors box through the
* boundary encoding. cb's cap boxes the dyn callable. */
void scr_stream_finished_dyn_inv(ScrClosure *cb, ScrStream *s, ScrError *err);
/* stream.pipeline(s1..sn, cb) — the callback form over stream arguments:
* chains pipes (end: true), destroys every other stream with the first
* error (Node's destroyer, pipeline order), and calls cb once after the
* LAST 'close' with that error (or NULL). Streams borrowed; cb MOVES;
* answers the destination +1 (pipeline's return value). */
ScrStream *scr_stream_pipeline(double n, ScrStream **streams, ScrClosure *cb, ScrStreamErrInv inv);
/* The dyn-valued twins (the callback as a checked-dynamic value —
* mustCall wrappers; borrowed). */
ScrClosure *scr_stream_finished_dyn(ScrStream *s, struct ScrDyn *cb);
ScrStream *scr_stream_pipeline_dyn(double n, ScrStream **streams, struct ScrDyn *cb);
/* Emitted subclass RC/trace delegates for the state block (NULL-safe —
* an exception before super(options) leaves st unset). The registry
* slot rides scr_emitter_reg_* like any emitter subclass. */
void scr_stream_st_release(ScrStreamState *st);
void scr_stream_st_gcfree(ScrStreamState *st);
void scr_stream_st_trace(ScrStreamState *st, ScrTraceVisit visit, void *ctx);
/* The dyn-flavored completion-callback glues (the JS lane's implicitly-
* any option callbacks / underscore methods): the emitted invoke thunks
* mint a callable dyn (scr_dyn_new_func) over a 1-cap closure boxing the
* retained stream, with the kind's glue — arguments arrive as dyn
* values (error: null/undefined, a {message} object, or a string;
* transform/flush data: bytes, a string, or absent). */
typedef struct ScrDyn ScrDyn; /* full declaration below (C11 repeat) */
ScrDyn *scr_stream_done_dyn_w(ScrClosure *clo, ScrDyn *const *args, size_t argc);
ScrDyn *scr_stream_done_dyn_f(ScrClosure *clo, ScrDyn *const *args, size_t argc);
ScrDyn *scr_stream_done_dyn_d(ScrClosure *clo, ScrDyn *const *args, size_t argc);
ScrDyn *scr_stream_done_dyn_t(ScrClosure *clo, ScrDyn *const *args, size_t argc);
ScrDyn *scr_stream_done_dyn_l(ScrClosure *clo, ScrDyn *const *args, size_t argc);
/* The readable surface. push/unshift borrow their chunk (strings convert
* utf8, Node's decodeStrings default); push answers the below-hwm bool. */
bool scr_stream_push(ScrStream *s, ScrBytes *chunk);
bool scr_stream_push_str(ScrStream *s, ScrStr *str);
/* push(chunk, enc): the per-call literal encoding (canonical); overrides
* the stream's defaultEncoding. Borrows both. */
bool scr_stream_push_str_enc(ScrStream *s, ScrStr *str, ScrStr *enc);
/* The defaultEncoding option's push side: how push(string) decodes chunks
* (Buffer.from(chunk, enc)). Canonical literal, never "utf8". Receiver
* answers +1 (the setEncoding chaining shape). */
ScrStream *scr_stream_set_push_encoding(ScrStream *s, ScrStr *enc);
bool scr_stream_push_null(ScrStream *s);
void scr_stream_unshift(ScrStream *s, ScrBytes *chunk);
void scr_stream_unshift_str(ScrStream *s, ScrStr *str);
ScrBytes *scr_stream_read(ScrStream *s, double size); /* +1 or NULL; size < 0 = all/first; throws once encoded (the static type is Buffer|null) */
/* setEncoding(enc): string-chunk mode — pushes decode through the
* StringDecoder, 'data' delivers strings (the emit ABI carries both
* payload slots; see scr_stream_emit_data), lengths count JS string
* units. Borrows the canonical encoding name; receiver +1. */
ScrStream *scr_stream_set_encoding(ScrStream *s, ScrStr *enc);
typedef struct ScrPromise ScrPromise; /* full declaration below (C11 repeat) */
/* for-await: a +1 promise of the next chunk — buffered content (one
* whole entry in Readable.from mode, the whole buffer otherwise —
* Node's iterator read()), the EOF sentinel (an empty Buffer / dyn
* undefined), or a rejection with the stream's error. The _dyn twin
* boxes by runtime tag (the JS lane; encoded chunks arrive as dyn
* strings); the typed form throws on encoded streams (chunks are
* Buffers in typed code). At most one parked waiter (the loop awaits
* each chunk before asking again). */
bool scr_stream_push_dyn(ScrStream *s, const ScrDyn *d); /* borrows d; tag dispatch */
bool scr_stream_write_dyn(ScrStream *s, const ScrDyn *d, ScrClosure *cb); /* cb moves */
ScrPromise *scr_stream_next_chunk(ScrStream *s);
ScrPromise *scr_stream_next_chunk_dyn(ScrStream *s);
/* node:stream/promises — the promise forms over the finished/pipeline
* machinery above: a pending void promise the terminal watcher settles
* (fulfilled on a clean finish, rejected with the finish status
* otherwise — the stream's error or ERR_STREAM_PREMATURE_CLOSE).
* Streams borrowed; +1 promises. */
ScrPromise *scr_sp_finished(ScrStream *s);
ScrPromise *scr_sp_pipeline(double n, ScrStream **streams);
/* node:stream/consumers — the promise consumers over the readable
* machinery: accumulate every chunk (string chunks as their utf8 bytes)
* and settle at the terminal point (right after 'close', the eos timing
* Node's consumers share) — text answers the utf8 decode, json parses
* the text (malformed input rejects with the parse's SyntaxError; the
* result is a +1 dyn tree), buffer the concatenated bytes. Stream
* errors reject; an early close rejects ERR_STREAM_PREMATURE_CLOSE; a
* stream with no readable side rejects Node's async-iterable TypeError.
* Streams borrowed; +1 promises. */
ScrPromise *scr_sc_text(ScrStream *s);
ScrPromise *scr_sc_json(ScrStream *s);
ScrPromise *scr_sc_buffer(ScrStream *s);
/* Readable.from(array): +1 fully-seeded object-entry stream (one WHOLE
* chunk per element — strings or Buffers per the flag; hwm 1, already
* EOF'd). Borrows arr. */
ScrStream *scr_stream_from_arr(ScrArr *arr, bool strings);
ScrStream *scr_stream_pause(ScrStream *s); /* recv +1 */
ScrStream *scr_stream_resume(ScrStream *s); /* recv +1 */
bool scr_stream_is_paused(ScrStream *s);
double scr_stream_flowing(ScrStream *s); /* -1 = null, 0 = false, 1 = true */
ScrStream *scr_stream_pipe(ScrStream *src, ScrStream *dst, bool end); /* dst +1 */
ScrStream *scr_stream_unpipe(ScrStream *src, ScrStream *dst /*NULL = all*/); /* recv +1 */
/* The writable surface. write borrows its chunk; cb (when non-NULL)
* MOVES and fires after the chunk's user write completes. */
bool scr_stream_write(ScrStream *s, ScrBytes *chunk, ScrClosure *cb /*moves*/);
bool scr_stream_write_str(ScrStream *s, ScrStr *str, ScrClosure *cb /*moves*/);
bool scr_stream_write_null(ScrStream *s); /* throws ERR_STREAM_NULL_VALUES */
ScrStream *scr_stream_end(ScrStream *s, ScrBytes *chunk_b, ScrStr *chunk_s,
ScrClosure *cb /*moves*/); /* recv +1 */
void scr_stream_cork(ScrStream *s);
void scr_stream_uncork(ScrStream *s);
/* Shared lifecycle. destroy's err is BORROWED (may be NULL). */
ScrStream *scr_stream_destroy(ScrStream *s, ScrError *err); /* recv +1 */
/* Readable async-iterator return(): destroy with Node's AbortError payload;
* the iterator's internal error consumer suppresses an unhandled crash when
* no user error listener exists. Receiver +1. */
ScrStream *scr_stream_iterator_close(ScrStream *s);
double scr_stream_prop(ScrStream *s, const char *name);
ScrError *scr_stream_errored(ScrStream *s); /* +1 or NULL */
/* The underscore-method assignment surface (`r._read = fn` after
* construction — Node's own-property shadow of the prototype method):
* the option-callback slot swaps its closure (+1 moves in, the old one
* releases) and its compiler-emitted invoke thunk; the next dispatch
* uses the new callback, Node's timing. */
void scr_stream_set_read(ScrStream *s, ScrClosure *cb, ScrStreamReadInv inv);
void scr_stream_set_write(ScrStream *s, ScrClosure *cb, ScrStreamChunkInv inv);
void scr_stream_set_final(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv);
void scr_stream_set_destroy(ScrStream *s, ScrClosure *cb, ScrStreamErrInv inv);
void scr_stream_set_transform(ScrStream *s, ScrClosure *cb, ScrStreamChunkInv inv);
void scr_stream_set_flush(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv);
/* Completion entries for the compiler-emitted done closures (err/data
* MOVE; NULLs = absent). */
void scr_stream_write_done(ScrStream *s, ScrError *err);
void scr_stream_final_done(ScrStream *s, ScrError *err);
void scr_stream_destroy_done(ScrStream *s, ScrError *err);
void scr_stream_transform_done(ScrStream *s, ScrError *err, ScrBytes *data, ScrStr *data_str);
void scr_stream_flush_done(ScrStream *s, ScrError *err, ScrBytes *data, ScrStr *data_str);
/* RC / trace / install (the emitter-unit shapes). */
ScrStream *scr_stream_retain(ScrStream *s);
void scr_stream_release(ScrStream *s);
void *scr_stream_retain_v(void *s);
void scr_stream_release_v(void *s);
void scr_stream_trace(void *obj, ScrTraceVisit visit, void *ctx);
void scr_stream_install(void);
/* The loop's stream hook (scr_async.c): `pending` keeps the loop alive
* while deferred ticks exist; `dispatch` drains them at every turn top —
* the FIRST station (nextTick-most). Stream-free builds keep both NULL
* and the loop is byte-identical. */
void scr_loop_set_stream(bool (*pending)(void), void (*dispatch)(void));
void scr_union_trace_v(void *u, ScrTraceVisit visit, void *ctx);
void scr_promise_trace_v(void *p, ScrTraceVisit visit, void *ctx);
/* ── exceptions ─────────────────────────────────────────────────────
* One in-flight exception cell per execution context — the main stack plus
* one per fiber (scr_async.c swaps the active cell on fiber switches).
* `throw` stores the thrown value (taking ownership of refcounted payloads)
* and sets the pending flag; the COMPILER emits a pending check after every call that can throw
* and unwinds by releasing its frames/scopes and returning a dummy value —
* no setjmp/longjmp (longjmp would skip the emitted RC releases). A `catch`
* takes the exception by clearing the cell (the supported catch form is
* bindingless, so the payload is released, not read); an exception still
* pending when the entry function returns is uncaught: main prints it to
* stderr and exits 1 (Node's uncaught exit code).
*
* Ref payloads carry their RC entry points as function pointers, exactly
* like ScrUnion/SCR_BOX_OBJ: the runtime cannot know per-class/per-record
* struct layouts. A throw while an exception is already pending REPLACES it
* (the old payload is released) — that is JS's behavior for a throw inside
* a `finally` running on the exception path.
*
* Ownership: scr_throw_str/_ref take ownership (+1 moves into the cell);
* scr_exc_clear and scr_exc_print_uncaught release the payload, so a
* pending exception never survives them (the RC audit stays meaningful).
*/
typedef enum {
SCR_EXC_NONE = 0,
SCR_EXC_F64,
SCR_EXC_BOOL,
SCR_EXC_STR,
SCR_EXC_REF, /* array/closure/standalone object/record/union: RC via the fn ptrs */
/* A HIERARCHY-class instance (the payload carries a vtable word after
* rc): RC exactly like REF, but the dynamic class is inspectable — catch
* bindings run instanceof against the payload's preorder interval, and
* the uncaught/unhandled printers render Error instances as
* "name: message" instead of "[object]". */
SCR_EXC_OBJ,
/* The generator-return sentinel (`gen.return(v)` injected at a suspended
* yield): PENDING like an exception — the emitted pending checks unwind
* through finally blocks — but it is a RETURN completion, not a throw:
* catch handlers in generator bodies re-unwind on it (the emitted
* sentinel prologue), and the generator trampoline consumes it (clears
* the cell, promotes the parked return value to the completion value).
* Carries NO payload (the value rides the generator's ret slot), so
* scr_exc_clear/reset need no arm. Never escapes a generator fiber. */
SCR_EXC_GENRET,
/* Refcounted values whose JS typeof is not object (Symbol, BigInt,
* functions, and checked-dynamic primitives). The payload uses the
* same ownership as REF. */
SCR_EXC_PRIMITIVE_REF,
} ScrExcKind;
/* One cell per fiber (JS has one exception in flight per execution
* context); scr_async.c swaps the active cell on fiber switches. */
typedef struct ScrExcCell {
ScrExcKind kind;
double f64;
bool b;
void *payload;
void *(*retain_fn)(void *);
void (*release_fn)(void *);
/* Non-NULL iff the REF payload type carries a cycle header — carried
* along so a rejection moving the payload into a promise keeps the
* promise's payload edge traceable. */
ScrTraceFn trace_fn;
} ScrExcCell;
/* Runtime-internal (fiber machinery in scr_async.c) — never emitted. */
ScrExcCell *scr_exc_swap_cell(ScrExcCell *cell); /* NULL = main's cell */
ScrExcCell *scr_exc_current_cell(void); /* the ACTIVE cell */
bool scr_exc_pending(void);
void scr_throw_f64(double v);
void scr_throw_bool(bool v);
void scr_throw_str(ScrStr *v); /* takes ownership */
/* Takes ownership of v; retain/release must be non-NULL (the compiler
* passes the payload type's `_v` adapters, like scr_union_new_ref); trace
* is non-NULL iff the payload type carries a cycle header. */
void scr_throw_ref(void *v, void *(*retain)(void *), void (*release)(void *),
ScrTraceFn trace);
/* Store a reference whose JS typeof was checked at the throw site. */
void scr_throw_ref_classified(void *v, void *(*retain)(void *), void (*release)(void *),
ScrTraceFn trace, bool object);
void scr_throw_primitive_ref(void *v, void *(*retain)(void *), void (*release)(void *),
ScrTraceFn trace);
/* Same ownership contract as scr_throw_ref; the payload must be a
* hierarchy-class instance (vtable word present) — see SCR_EXC_OBJ. */
void scr_throw_obj(void *v, void *(*retain)(void *), void (*release)(void *),
ScrTraceFn trace);
/* Catch: discard the in-flight exception (releases a refcounted payload). */
void scr_exc_clear(void);
/* ── catch bindings ───────────────────────────────────────────────────
* `catch (e)` binds the in-flight exception as a refcounted SNAPSHOT box:
* entering the catch MOVES the cell's payload into a fresh ScrCaught (the
* pending flag clears), the binding releases with its scope like any
* refcounted local, and every runtime question a catch body may ask —
* typeof-style kind tests (emitted as direct kind reads), instanceof over
* hierarchy payloads, payload extraction, rethrow — reads the box. The box
* itself carries no collector header: bindings live only in frames (the
* frontend rejects captures), so no cycle can pass through it, and its
* payload reference simply counts as an external edge — trial deletion
* restores it on scan. */
typedef struct ScrCaught {
size_t rc;
ScrExcKind kind; /* never SCR_EXC_NONE */
double f64;
bool b;
void *payload;
void *(*retain_fn)(void *);
void (*release_fn)(void *);
ScrTraceFn trace_fn;
} ScrCaught;
ScrCaught *scr_exc_take(void); /* moves the pending cell into a fresh box (+1) */
ScrCaught *scr_caught_retain(ScrCaught *c);
void scr_caught_release(ScrCaught *c); /* NULL-tolerant */
/* Replace the pending disposal error with a SuppressedError and consume the
* original body-error snapshot. The current exception cell must be pending. */
void scr_exc_suppress(ScrCaught *suppressed);
/* Borrows the box; re-raises the saved exception exactly (payload RETAINED
* — the binding stays live until its scope exits). */
void scr_rethrow(const ScrCaught *c);
/* `e instanceof C` on a catch binding: an OBJ payload whose vtable preorder
* lies inside C's interval. False for every other payload kind. */
bool scr_caught_instanceof(const ScrCaught *c, size_t pre, size_t post);
/* `typeof e === "object"` on a catch binding. Throw sites choose REF or
* PRIMITIVE_REF from the value's JS typeof before storing the payload. */
bool scr_caught_is_object(const ScrCaught *c);
/* `String(e)` / `${e}` on a catch binding: JS's String() over the snapshot
* — numbers/booleans/strings by value, Error payloads via
* scr_error_to_string ("name: message" — String(e) carries no stack in
* Node either), every other ref payload "[object Object]" (exact for
* records and toString-less class instances; a documented approximation
* for thrown arrays/closures/union boxes). Borrows the box; returns +1. */
ScrStr *scr_caught_to_string(const ScrCaught *c);
/* `e as C` on a catch binding (scr_lib.c): an OBJ payload inside C's
* preorder interval extracts (retained, +1); every other payload THROWS a
* catchable TypeError naming `cls`. Callers are compiler-emitted pending
* checks. */
void *scr_caught_check_obj(const ScrCaught *c, size_t pre, size_t post,
const char *cls);
/* The snapshot as a dyn value (scr_json.c): identity-preserving for dyn
* payloads and %Error instances, scalars by value, the type-erased empty
* object for the rest (SEMANTICS.md 67). Borrows the box; +1. */
typedef struct ScrDyn ScrDyn; /* full definition below (C11 repeat) */
ScrDyn *scr_caught_to_dyn(const ScrCaught *c);
/* Uncaught path (called by main before exiting 1): flushes stdout, prints
* "Uncaught <value>" to stderr (strings raw, numbers JS-exact, booleans
* true/false, ref payloads as "[object]"), and releases the payload. */
void scr_exc_print_uncaught(void);
/* Optional native process exception registry. Hook returns 1 when handled,
* 0 when unhandled, and -1 when a handler itself threw (Node exit 7). */
extern int (*scr_uncaught_exception_hook)(bool from_promise);
bool scr_exc_handle_uncaught(bool from_promise);
void scr_process_on_uncaught_exception(ScrDyn *fn, bool once, bool monitor);
void scr_process_off_uncaught_exception(ScrDyn *fn, bool monitor);
/* ── standard library: process + node:fs (scr_lib.c) ─────────────────
* Called once at the top of main, right after scr_init: stashes argc/argv
* (the interned process.argv array is built lazily on first read) and
* registers an atexit cleanup that releases the library's interned values
* BEFORE the RC audit runs (atexit is LIFO; scr_init registered the audit
* earlier). scriptc argv shape: ["scriptc", argv[0], argv[1], ...] so
* positions and length line up with Node's [node-path, script-path, ...args]
* — the argv[0]/argv[1] VALUES diverge (see SEMANTICS.md).
*/
void scr_lib_init(int argc, char **argv);
/* +1 on the ONE interned argv array (identity and mutation semantics match
* Node's stable process.argv). */
ScrArr *scr_process_argv(void);
/* CommonJS module graph introspection. Module values are process-lifetime
* numeric handles into a registry populated by the generated program before
* its entry initializer runs. Metadata arguments are borrowed; getters return
* owned (+1) strings/arrays. Negative parent handles distinguish null (-1)
* from undefined (-2); a cache miss is -1. */
void scr_module_registry_init(double count);
void scr_module_define(double module_id, ScrStr *filename, ScrStr *id,
ScrStr *path, ScrArr *paths, bool is_main);
void scr_module_enter(double module_id);
void scr_module_link(double parent_id, double child_id);
void scr_module_finish(double module_id);
void scr_module_fail(double module_id);
ScrStr *scr_module_filename(double module_id);
ScrStr *scr_module_id(double module_id);
ScrStr *scr_module_path(double module_id);
ScrArr *scr_module_paths(double module_id);
ScrArr *scr_module_children(double module_id);
double scr_module_parent(double module_id);
bool scr_module_loaded(double module_id);
double scr_module_cache_get(ScrStr *filename);
bool scr_module_cache_has(ScrStr *filename);
ScrArr *scr_module_cache_keys(void);
/* Raw argv accessors — the island's process shim builds the same
* ["scriptc", argv[0], ...] shape from the same stash. */
int scr_lib_arg_count(void);
const char *scr_lib_arg(int i);
/* Private self-reexec marker parsed and removed from process.argv by
* scr_lib_init. False in an ordinary parent invocation. */
bool scr_lib_fork_info(double *target, uintptr_t *read_handle,
uintptr_t *write_handle);
/* True only for a child_process call whose command and first argument both
* resolve to this executable — Node's process.execPath/process.argv[1]
* self-reexec shape. Child argv builders collapse that known script marker;
* direct startup never filters user arguments. */
bool scr_lib_should_collapse_reexec_arg(ScrStr *cmd, ScrArr *args);
ScrStr *scr_process_platform(void); /* +1 interned ("darwin", "linux", ...) */
ScrStr *scr_process_cwd(void); /* +1 fresh (getcwd) */
/* Submit one raw chunk to fd 1/2 and flush it before returning. Used by all
* JavaScript-visible console/process/readline/island output paths so the
* internal stdio formatting buffer never delays live output. */
bool scr_stdio_write(int fd, const void *data, size_t len);
int scr_stdio_write_raw(int fd, const void *data, size_t len);
extern bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len);
/* process.stdout/.stderr .write — raw bytes (no newline or formatting; data
* borrowed), promptly visible and ordered with console output. Constantly
* true (the synchronous runtime never queues backpressure). */
bool scr_process_stdout_write(const ScrStr *data);
bool scr_process_stderr_write(const ScrStr *data);
/* The first-class WritableStream write: fd is the stream value itself
* (1 = stdout, 2 = stderr — process.stdout/stderr reads mint it). */
bool scr_proc_stream_write(double fd, const ScrStr *data);
/* getenv(3) behind process.env.<NAME>: +1 fresh string when the variable is
* set, NULL when absent. The compiler wraps the result into the interned
* `string | undefined` union type-directedly (the runtime knows no tags);
* the name is borrowed. Never throws. */
ScrStr *scr_env_get(const ScrStr *name);
/* setenv(3) behind process.env.NAME = v: later env reads and spawned
* children observe the write, like Node. Both borrowed. Never throws. */
void scr_env_set(const ScrStr *name, const ScrStr *value);
void scr_env_unset(const ScrStr *name); /* delete process.env.NAME */
/* The whole environment as alternating [k, v, k, v, ...] strings in
* environ order — the compiler's process.env snapshot builds its record
* from this. +1 fresh array. Never throws. */
ScrArr *scr_env_pairs(void);
/* getpid(2) / getuid(2) — POSIX-only target, always answer. */
double scr_process_pid(void);
double scr_process_getuid(void);
double scr_process_getgid(void);
/* process.execPath: the running binary's own resolved absolute path
* (_NSGetExecutablePath/readlink(/proc/self/exe) + realpath), interned on
* first read; +1 per read. Node's value is the node executable's path —
* SEMANTICS.md divergence 12 documents the difference. */
ScrStr *scr_process_exec_path(void);
/* process.arch: the binary's OWN architecture ("arm64", "x64") — Node's
* answer for its own build on the same machine. +1 interned. */
ScrStr *scr_process_arch(void);
/* process.versions.node: the runtime's Node COMPATIBILITY TARGET — no
* Node exists under a compiled binary, so this reports the version whose
* semantics the runtime implements (SEMANTICS.md divergence 60). +1
* interned. */
ScrStr *scr_process_versions_node(void);
ScrStr *scr_process_versions_openssl(void);
/* Stable native version dictionary (+1); node/openssl are compatibility
* targets, matching the scalar getters above. */
ScrDyn *scr_process_versions(void);
/* process.kill with Node's exact semantics: int32 pid validation (the
* ERR_INVALID_ARG_TYPE TypeError text), Node's signal-name table for the
* named form (unknown names throw the ERR_UNKNOWN_SIGNAL TypeError),
* signal 0 probes, kill(2) failure throws Node's `kill ESRCH`/`kill
* EPERM` Error. Both return Node's constant true; the signal string is
* borrowed. */
bool scr_process_kill(double pid, double signum);
bool scr_process_kill_named(double pid, const ScrStr *signal);
/* fflush stdout, then _Exit((int)code): no atexit handlers run — the RC
* audit is deliberately skipped (exiting mid-program leaves live values). */
void scr_process_exit(double code);
void scr_process_set_exit_code(double code);
double scr_process_exit_code_or_zero(void);
/* process._exiting: true once the exit sequence began (process.exit or
* the exit-listener runner set the flag). Never throws. */
extern SCR_TL bool scr_process_in_exit;
bool scr_process_exiting(void);
/* umask(2): mask < 0 reads without setting; otherwise sets and answers
* the previous mask. Never throws. */
double scr_process_umask(double mask);
/* The process introspection statics (scr_lib.c; Node's units). uptime:
* fractional seconds since the binary's load-time anchor. The CPU clocks
* answer microseconds; the *_diff forms subtract a prior sample, and
* scr_cpu_prev_validate throws Node's ERR_INVALID_ARG_VALUE RangeError
* (user first, then system) for negative/non-finite prev fields.
* rusage(idx) answers one resourceUsage() field by canonical index.
* available/constrained memory are libuv's numbers (0 where the platform
* has no answer). scr_active_resources (scr_async.c) walks the loop's
* own bookkeeping — 'Timeout'/'Immediate' strings, +1. */
double scr_process_uptime(void);
/* perf_hooks performance.now(): fractional ms since process start (the
* uptime anchor — Node's timeOrigin for a compiled program). */
double scr_perf_now(void);
double scr_cpu_user(void);
double scr_cpu_system(void);
double scr_cpu_user_diff(double prev);
double scr_cpu_system_diff(double prev);
double scr_thread_cpu_user(void);
double scr_thread_cpu_system(void);
double scr_thread_cpu_user_diff(double prev);
double scr_thread_cpu_system_diff(double prev);
void scr_cpu_prev_validate(double user, double system);
double scr_process_rusage(double idx);
double scr_available_memory(void);
double scr_constrained_memory(void);
ScrArr *scr_active_resources(void);
/* chdir(2) — throws Node's fs-shaped error on failure (syscall "chdir"). */
void scr_process_chdir(ScrStr *dir);
/* net's process-wide happy-eyeballs attempt budget (default 250ms) — in
* the core unit so the knob never links scr_net.c. Never throw. */
double scr_net_get_autosel_timeout(void);
void scr_net_set_autosel_timeout(double ms);
/* Synchronous fs, utf8-only (paths and contents are ScrStr bytes). All
* ScrStr arguments are BORROWED; string/array results return +1. On
* failure every function except scr_fs_exists THROWS through the exception
* cell (scr_throw_str) a CATCHABLE string formatted like Node's common
* error messages ("ENOENT: no such file or directory, open 'x'") and
* returns a dummy (false/NULL) — callers are compiler-emitted pending
* checks. scr_fs_exists mirrors Node's existsSync: errors become false,
* never a throw. */
ScrStr *scr_fs_read_file(ScrStr *path);
/* realpath(3) (+1 fresh); failures throw with Node's realpathSync
* spelling (syscall "lstat"). */
ScrStr *scr_fs_realpath(ScrStr *path);
ScrStr *scr_fs_realpath_promise(ScrStr *path);
void scr_fs_write_file(ScrStr *path, ScrStr *data);
void scr_fs_append_file(ScrStr *path, ScrStr *data);
bool scr_fs_exists(ScrStr *path);
void scr_fs_mkdir(ScrStr *path);
/* Files only, like Node's rmSync without `recursive` (a directory throws an
* EISDIR-coded error; use scr_fs_rmdir for empty directories). */
void scr_fs_rm(ScrStr *path);
void scr_fs_rmdir(ScrStr *path);
ScrArr *scr_fs_readdir(ScrStr *path); /* names, OS order, no "."/".." */
/* The fs option forms (same throw discipline). mkdir_recursive is Node's
* recursive algorithm (existing-dir target is a no-op; errors report
* Node's errno at Node's path). rm_opts is rmSync with the recursive/
* force booleans: force swallows ENOENT, recursive removes trees
* post-order, a directory without recursive keeps the EISDIR wording
* divergence. mkdtemp appends the six X's and returns the created path
* (+1). access takes the F_OK/R_OK/W_OK/X_OK bits. read_fd is a read(2)
* loop to EOF (Node's readFileSync(fd) shape — the stdin pattern);
* failures throw Node's no-path message ("EBADF: bad file descriptor,
* read"). */
void scr_fs_mkdir_recursive(ScrStr *path);
void scr_fs_rm_opts(ScrStr *path, bool recursive, bool force);
/* The maxRetries/retryDelay form: retries EBUSY/EMFILE/ENFILE/ENOTEMPTY/
* EPERM failures up to maxRetries times with Node's linear backoff
* (retryDelay ms longer per try); everything else throws immediately. */
void scr_fs_rm_opts_retry(ScrStr *path, bool recursive, bool force, double max_retries, double retry_delay);
ScrStr *scr_fs_mkdtemp(ScrStr *prefix);
void scr_fs_access(ScrStr *path, double mode);
ScrStr *scr_fs_read_fd(double fd);
/* The wider sync fs slice (same throw discipline; `.code` stamped like
* the rest). unlink/chmod/chown wrap the syscalls 1:1. copyfile copies
* contents into a created-or-truncated destination carrying the SOURCE's
* mode; its errors quote both paths ("copyfile 'src' -> 'dest'", Node's
* shape). write_file_mode is writeFileSync(p, data, { mode }) — the mode
* applies at creation only. The mkdir *_mode twins take mkdirSync's
* explicit mode (the recursive walk passes it to every directory it
* creates). */
void scr_fs_unlink(ScrStr *path);
void scr_fs_chmod(ScrStr *path, double mode);
void scr_fs_chown(ScrStr *path, double uid, double gid);
void scr_fs_copyfile(ScrStr *src, ScrStr *dest);
void scr_fs_rename(ScrStr *oldpath, ScrStr *newpath);
/* The callback rename worker uses the non-throwing syscall seam off the
* runtime thread, then materializes any Node-shaped Error back on the main
* thread. raw returns 0 on success or a positive errno value on failure. */
int scr_fs_rename_raw(const ScrStr *oldpath, const ScrStr *newpath);
void scr_fs_rename_error(int error, const ScrStr *oldpath, const ScrStr *newpath);
void scr_fs_write_file_mode(ScrStr *path, ScrStr *data, double mode);
void scr_fs_mkdir_mode(ScrStr *path, double mode);
void scr_fs_mkdir_recursive_mode(ScrStr *path, double mode);
/* Atomics.wait(int32Array, idx, expected, timeoutMs): "not-equal" when
* the element differs from expected, else a real nanosleep for the
* timeout and "timed-out" (+1 string either way). scriptc has no
* threads, so nothing can ever notify — for every compilable program
* this IS the spec's behavior and "ok" is unreachable (the compiler
* requires the timeout argument; an infinite wait would be a certain
* deadlock). Out-of-range indices trap like every bytes access. */
typedef struct ScrBytes ScrBytes; /* full definition below (C11 repeat) */
ScrStr *scr_atomics_wait(ScrBytes *arr, double idx, double expected, double timeout_ms);
/* isatty(3) over an fd (0/1/2): a real boolean — false where Node's
* non-TTY streams expose undefined (SEMANTICS.md). stdin_destroy is a
* documented no-op. */
bool scr_process_is_tty(double fd);
void scr_process_stdin_destroy(void);
void scr_process_stdin_set_raw_mode(bool raw);
/* Terminal width for process.stdout/stderr.columns (fd 1/2):
* ioctl(TIOCGWINSZ), -1 when the fd is not a TTY or the ioctl refuses —
* the emitter wraps a non-negative width into the `number | undefined`
* union's number arm and -1 into its undefined arm (Node's non-TTY
* `.columns` is undefined). Never throws. */
double scr_process_columns(double fd);
/* Terminal height; same optional-number sentinel contract as columns. */
double scr_process_rows(double fd);
/* Stats values (statSync/lstatSync and their fs.promises twins): an immutable
* snapshot of stat(2). scr_fs_stat follows symlinks; scr_fs_lstat does not.
* Both THROW like the other sync calls. */
typedef struct ScrStats ScrStats;
typedef struct ScrFileHandle ScrFileHandle;
typedef struct ScrPromise ScrPromise; /* full section further down */
ScrStats *scr_fs_stat(ScrStr *path); /* +1, or throws */
ScrStats *scr_fs_lstat(ScrStr *path); /* +1, or throws; NO follow (lstat) */
ScrStats *scr_fs_fstat(double fd); /* +1, or throws */
void scr_fs_fchmod(double fd, double mode);
void scr_fs_fsync(double fd);
void scr_fs_link(ScrStr *source, ScrStr *dest);
ScrStats *scr_stats_retain(ScrStats *s);
void scr_stats_release(ScrStats *s);
void *scr_stats_retain_v(void *p);
void scr_stats_release_v(void *p);
bool scr_stats_is_file(ScrStats *s);
bool scr_stats_is_dir(ScrStats *s);
bool scr_stats_is_symlink(ScrStats *s); /* lstat snapshots only */
double scr_stats_dev(ScrStats *s);
double scr_stats_ino(ScrStats *s);
double scr_stats_size(ScrStats *s);
double scr_stats_blocks(ScrStats *s); /* allocated size in 512-byte units */
double scr_stats_nlink(ScrStats *s);
double scr_stats_atime_ms(ScrStats *s); /* ms with the sub-second fraction */
double scr_stats_mtime_ms(ScrStats *s); /* ms with the ns fraction */
double scr_stats_ctime_ms(ScrStats *s); /* ms with the ns fraction */
/* fs/promises: the SAME sync operations, minting an already-settled
* promise — success fulfills, failure REJECTS with the would-be thrown
* error (catchable at the await, like Node). The syscall blocks the loop:
* I/O never interleaves with timers/other fibers (SEMANTICS.md). Implemented in
* scr_async.c beside the promise machinery (the runtime unit tests link
* scr_lib.c without the fiber slice). */
ScrPromise *scr_fsp_read_file(ScrStr *path);
ScrPromise *scr_fsp_write_file(ScrStr *path, ScrStr *data);
ScrPromise *scr_fsp_write_file_mode(ScrStr *path, ScrStr *data, double mode);
ScrPromise *scr_fsp_mkdir(ScrStr *path);
ScrPromise *scr_fsp_mkdir_mode(ScrStr *path, double mode);
ScrPromise *scr_fsp_mkdir_recursive(ScrStr *path);
ScrPromise *scr_fsp_mkdir_recursive_mode(ScrStr *path, double mode);
ScrPromise *scr_fsp_unlink(ScrStr *path);
ScrPromise *scr_fsp_chmod(ScrStr *path, double mode);
ScrPromise *scr_fsp_readdir(ScrStr *path);
ScrPromise *scr_fsp_rm(ScrStr *path);
ScrPromise *scr_fsp_stat(ScrStr *path);
ScrPromise *scr_fsp_realpath(ScrStr *path);
ScrPromise *scr_fsp_lstat(ScrStr *path);
ScrPromise *scr_fsp_rename(ScrStr *oldpath, ScrStr *newpath);
ScrPromise *scr_fsp_open(ScrStr *path, ScrStr *flags, double mode);
ScrPromise *scr_file_handle_close_promise(ScrFileHandle *h);
ScrPromise *scr_file_handle_read_file_promise(ScrFileHandle *h, ScrStr *encoding);
ScrPromise *scr_file_handle_read_file_bytes_promise(ScrFileHandle *h, ScrStr *encoding);
ScrPromise *scr_file_handle_write_file_promise(ScrFileHandle *h, ScrStr *data, ScrStr *encoding);
ScrPromise *scr_file_handle_write_file_bytes_promise(ScrFileHandle *h, ScrBytes *data, ScrStr *encoding);
ScrPromise *scr_file_handle_stat_promise(ScrFileHandle *h);
/* Shared executable worker pool. `work` runs on one of four native worker
* threads and must not touch runtime RC/exception state; `after` and
* `destroy` run on the main runtime thread. WASI runs `work` on a later
* loop turn. The payload is owned by the submission until destroy. */
typedef void (*ScrWorkFn)(void *payload);
void scr_work_submit(void *payload, ScrWorkFn work, ScrWorkFn after,
ScrWorkFn destroy);
/* fs.rename: the syscall is submitted to a native worker immediately and
* its error-first callback fires on a later event-loop turn through an
* emitted, program-shaped adapter. Both paths and the callback are borrowed
* at entry; the callback MOVES into the operation. err is borrowed and NULL
* on success. */
typedef void (*ScrFsRenameFn)(ScrClosure *cb, ScrError *err);
void scr_fs_rename_async(ScrStr *oldpath, ScrStr *newpath,
ScrClosure *cb /*moves*/, ScrFsRenameFn fn);
void scr_fs_rename_thunk0(ScrClosure *cb, ScrError *err);
/* ── node:timers/promises (scr_async.c) ──────────────────────────────
* The promisified pair: a PENDING void promise a one-shot heap timer /
* the immediate queue fulfills (the armed entry keeps the loop alive
* until it fires, like Node). Results +1; neither throws. */
ScrPromise *scr_tp_set_timeout(double ms);
ScrPromise *scr_tp_set_immediate(void);
/* ── node:diagnostics_channel (scr_dc.c — linked when the IR carries
* dc.* libCalls) ─────────────────────────────────────────────────────
* A process-global name→channel registry; channel handles are 1-based
* f64 indexes into it (Node's channels are process-lived too, its
* WeakRef machinery aside — the registry tears down atexit before the
* RC audit). Subscribers are dyn FUNCTION values, identity-matched by
* unsubscribe (the underlying closure, so re-boxings of one function
* still match — JS has ONE function object). publish calls each
* subscriber of a SNAPSHOT with (message, name); a subscriber's throw
* propagates out (MAY THROW — the documented divergence from Node's
* triggerUncaughtException). subscribe/unsubscribe throw Node's
* ERR_INVALID_ARG_TYPE TypeError for non-function subscribers (MAY
* THROW). Names and subscriber args are BORROWED; chanName's result +1. */
double scr_dc_channel(ScrStr *name);
void scr_dc_subscribe(ScrStr *name, ScrDyn *cb);
bool scr_dc_unsubscribe(ScrStr *name, ScrDyn *cb);
bool scr_dc_has_subscribers(ScrStr *name);
void scr_dc_publish(double handle, ScrDyn *message);
void scr_dc_chan_subscribe(double handle, ScrDyn *cb);
bool scr_dc_chan_unsubscribe(double handle, ScrDyn *cb);
bool scr_dc_chan_has_subscribers(double handle);
ScrStr *scr_dc_chan_name(double handle);
/* TracingChannel (dc.tracingChannel): a registry entry of the five event
* channels (tracing:<name>:start|end|asyncStart|asyncEnd|error), handled
* as a 1-based f64 exactly like Channel. subscribe/unsubscribe walk the
* handlers object's five event keys (truthy slots route to the per-channel
* add/remove — a non-function slot throws there, Node's order);
* unsubscribe answers Node's all-found conjunction. traceSync/traceCallback
* mirror Node's publish choreography over the checked-dynamic tree: ctx must be an OBJ (the
* lowering builds the default `{}`), fn/thisArg/args are dyn values (args
* an ARR of the call arguments); the traced call binds thisArg as the
* ambient receiver; result/error stamp onto ctx before end/error publish;
* a traced throw and a subscriber throw both propagate (MAY THROW). All
* arguments BORROWED; trace results +1 (or NULL with the exception
* pending). traceCallback wraps args[position] (JS-style negative
* indexing) in a native wrapper publishing error/result + asyncStart/
* asyncEnd around the original callback, throwing Node's
* ERR_INVALID_ARG_TYPE when the slot is not a function. */
double scr_dc_tracing_channel(ScrStr *name);
double scr_dc_tracing_channel_of(double start, double end, double async_start,
double async_end, double error);
double scr_dc_tc_channel(double h, double idx); /* 0=start..4=error */
bool scr_dc_tc_has_subscribers(double h);
void scr_dc_tc_subscribe(double h, ScrDyn *handlers);
bool scr_dc_tc_unsubscribe(double h, ScrDyn *handlers);
ScrDyn *scr_dc_tc_trace_sync(double h, ScrDyn *fn, ScrDyn *ctx, ScrDyn *this_arg, ScrDyn *args);
ScrDyn *scr_dc_tc_trace_callback(double h, ScrDyn *fn, double position, ScrDyn *ctx,
ScrDyn *this_arg, ScrDyn *args);
/* bindStore / runStores (the AsyncLocalStorage integration): per-store
* SET semantics (a re-bind replaces the transform; a non-function
* transform means identity); runStores enters every bound store with
* transform(data), publishes inside the entered contexts, runs fn with
* thisArg bound and arguments forwarded, and restores in reverse. The
* tracing choreography enters the start channel's stores around
* traceSync/traceCallback/tracePromise bodies and the asyncStart
* channel's around traceCallback's wrapped callback. */
void scr_dc_chan_bind_store(double handle, double als_id, ScrDyn *transform);
bool scr_dc_chan_unbind_store(double handle, double als_id);
ScrDyn *scr_dc_chan_run_stores(double handle, ScrDyn *data, ScrDyn *fn, ScrDyn *this_arg,
ScrDyn *args);
/* tracePromise: publishes start, runs fn (throw → error + end + rethrow,
* NULL result with the exception pending), wraps a non-promise result,
* publishes end, and returns the REACTION promise (+1) — a fiber that
* awaits the traced promise, stamps ctx.result/ctx.error, publishes
* asyncStart/asyncEnd (plus error first on rejection), and settles the
* result with the passed-through outcome. No-subscriber calls skip the
* choreography (Node's early exit) but still answer a promise. */
ScrPromise *scr_dc_tc_trace_promise(double h, ScrDyn *fn, ScrDyn *ctx, ScrDyn *this_arg,
ScrDyn *args);
/* ── child_process (scr_child.c) ─────────────────────────────────────
* The synchronous slice: spawnSync as posix_spawnp + piped utf8 capture +
* waitpid — blocks until the child is reaped (no zombie can outlive the
* call). NEVER throws: spawn failure (nonexistent binary, EACCES) is data
* — has_status false and empty outputs — like Node's `error` property
* (Node types stdout/stderr null there; divergence in SEMANTICS.md).
* A signal-killed child also has no status (Node's status: null). */
typedef struct ScrSpawnRes ScrSpawnRes;
ScrSpawnRes *scr_spawn_sync(ScrStr *cmd, ScrArr *args); /* +1, never throws */
/* The options form (cp.spawnSyncOpts): timeout_ms > 0 sends killsignal
* (a signal NAME; "" = SIGTERM) at the deadline and the result carries
* error: ETIMEDOUT + the signal — never a throw. stdio modes: stdin 0 =
* /dev/null, 2 = inherit; stdout/stderr 0 = capture, 1 = ignore, 2 =
* inherit (non-captured outputs read "" — the documented stance). */
ScrSpawnRes *scr_spawn_sync_opts(ScrStr *cmd, ScrArr *args, double timeout_ms,
ScrStr *killsignal, double in_mode,
double out_mode, double err_mode);
/* The runtime-string stdio entry: the compiler proved the value is
* "pipe" | "ignore" | "inherit" by type; the mode mapping happens here. */
ScrSpawnRes *scr_spawn_sync_stdio_str(ScrStr *cmd, ScrArr *args, double timeout_ms,
ScrStr *killsignal, ScrStr *stdio);
/* execFileSync/execSync behind one entry: posix_spawn(p) + piped utf8
* capture + waitpid with Node's exact error shapes — non-zero exit or
* signal death throws Error("Command failed: <cmd>[\n<stderr>]"), spawn
* failure throws "spawnSync <file> ENOENT", a timeout SIGTERMs the child
* and throws "spawnSync <file> ETIMEDOUT" (messages only — Node's
* status/stdout/stderr error properties don't exist, SEMANTICS.md).
* shell=true runs /bin/sh -c (args must be ["-c", cmd]); has_input false
* means the input option is ABSENT (stdin from /dev/null — Node's model
* for an undefined member), true feeds the bytes to a stdin pipe ("" is
* the pipe with immediate EOF); cwd "" inherits; has_env false inherits
* environ, true REPLACES it with the [k,v,...] pairs; timeout_ms <= 0
* disables; stdout_mode 1 captures / 0 ignores / 2 inherits the parent's
* fd, plus bit 4 = the CHILD'S STDIN inherits (stdio "inherit"; the
* flag rides stdout_mode because the signature predates it); stderr_mode
* 0 captures AND echoes to the parent stderr after completion (Node's
* no-stdio-option default), 1 captures only, 2 ignores, 3 inherits.
* Result is the captured utf8 stdout (+1) — "" when nothing captures
* (Node answers null there; discarded results cannot tell). All args
* borrowed. */
ScrStr *scr_exec_sync(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *input,
bool has_input, ScrStr *cwd, bool has_env, ScrArr *env_pairs,
double timeout_ms, double stdout_mode, double stderr_mode);
/* The promisified-execFile capture (cp.execCapture): the same core in the
* async shape — both streams captured (no echo), Node's ASYNC error
* messages on the throw paths ("Command failed: <cmd>\n<stderr>" with the
* unconditional newline, "spawn <file> <ERR>" with .code, timeouts as
* ordinary SIGTERM command failures). +1 ScrSpawnRes on success (stdout +
* stderr; status unused). All args borrowed. */
ScrSpawnRes *scr_exec_capture(ScrStr *cmd, ScrArr *args, ScrStr *cwd,
bool has_env, ScrArr *env_pairs, double timeout_ms);
ScrSpawnRes *scr_spawn_res_retain(ScrSpawnRes *r);
void scr_spawn_res_release(ScrSpawnRes *r);
void *scr_spawn_res_retain_v(void *p);
void scr_spawn_res_release_v(void *p);
bool scr_spawn_res_has_status(ScrSpawnRes *r);
double scr_spawn_res_status(ScrSpawnRes *r);
ScrStr *scr_spawn_res_stdout(ScrSpawnRes *r); /* +1 */
ScrStr *scr_spawn_res_stderr(ScrSpawnRes *r); /* +1 */
/* result.signal — the termination signal's name when a signal killed the
* child (a timeout's killSignal included), the null arm otherwise; the
* emitter wraps type-directedly over the has/get pair like status. */
bool scr_spawn_res_has_signal(ScrSpawnRes *r);
ScrStr *scr_spawn_res_signal(ScrSpawnRes *r); /* +1; has_signal only */
/* Node's `error` property: +1 %Error ("spawnSync <file> ENOENT", `code`
* stamped) when the spawn itself failed, NULL otherwise (the undefined
* arm — the emitter wraps type-directedly, the envGet convention). */
ScrError *scr_spawn_res_error(ScrSpawnRes *r);
/* argv construction shared by spawnSync and spawn: a NULL-terminated
* vector borrowing the strings (alive while cmd/args are); free() the
* vector only. */
char **scr_child_argv(ScrStr *cmd, ScrArr *args);
/* The asynchronous child: spawn starts it immediately (stdio all on
* /dev/null — the compiler fences everything but { stdio: "ignore" })
* and registers it with the event loop, which sweeps waitpid(WNOHANG) at
* quiescence (scr_children_poll) and fires listeners at reap; between
* turns the loop's sleep waits on a kqueue PROC filter, so an exit wakes
* it immediately (scr_children_wait; ~1ms polling is the fallback). "exit"
* fires once with the code (or none — signal death → the null arm);
* "error" fires ONLY for spawn failure, and with no listener registered
* it prints the error and exits 1 (Node's unhandled-'error' behavior).
* Listener callbacks MOVE in and are released when the child settles; a
* listener registered after settling is released and never fires. The
* adapters bridge the runtime's fixed firing ABI to the callback's
* compiled shape: exit_thunk0/err_thunk0 ignore the payload (zero-param
* listeners), err_thunk_error constructs the %Error instance, and the
* `(code: number | null)` shape is compiler-emitted (union tags are
* program data). */
typedef struct ScrChild ScrChild;
/* A piped child-output stream (child.stdout / child.stderr — stdio mode
* 3): refcounted like the child; 'data' fires one Buffer chunk per
* read(2) (≤64KB, consumer-driven — no listener, no read: the pipe is
* the buffer), 'end' fires once at EOF and always BEFORE the child's
* own 'exit' (Node's pinned ordering; the settle path drains first). A
* flowing stream (data consumer, not yet EOF) keeps the loop alive. */
typedef struct ScrChildStream ScrChildStream;
/* A piped child-input writer (child.stdin — stdio mode 3 on fd 0).
* write copies borrowed string/bytes data into a nonblocking queue and
* returns false once buffered data reaches the stream high-water mark;
* drain fires when that queue empties. end closes after queued data and
* then fires finish; destroy closes immediately without finish. Errors
* are asynchronous and use the child error adapters. */
typedef struct ScrChildWriter ScrChildWriter;
/* The data adapter: the chunk arrives BORROWED (multiple listeners see
* the same chunk); adapters retain what they keep. */
typedef void (*ScrChildStreamDataFn)(ScrClosure *cb, ScrBytes *chunk);
typedef void (*ScrChildStreamDataStrFn)(ScrClosure *cb, ScrStr *chunk);
/* signal_name: the termination signal's name (static storage) when a
* signal killed the child, NULL otherwise — Node's second exit-listener
* parameter; zero/one-param adapters ignore it. */
typedef void (*ScrChildExitFn)(ScrClosure *cb, bool has_code, double code,
const char *signal_name);
typedef void (*ScrChildErrFn)(ScrClosure *cb, ScrStr *msg);
/* execFile's fixed runtime firing ABI. error/stdout/stderr arrive as +1
* values and MOVE through the compiler-emitted adapter into the callback
* (or are released there when the callback omits a suffix). */
typedef void (*ScrExecFileFn)(ScrClosure *cb, ScrError *error,
ScrStr *stdout_value, ScrStr *stderr_value);
/* JSON IPC callback ABIs. Message values are borrowed checked-dynamic
* trees; send errors are +1 and move through the adapter (NULL = success). */
typedef void (*ScrIpcMessageFn)(ScrClosure *cb, ScrDyn *message);
typedef void (*ScrIpcSendFn)(ScrClosure *cb, ScrError *error);
ScrChild *scr_spawn(ScrStr *cmd, ScrArr *args); /* +1, never throws */
ScrChild *scr_fork(double target, ScrArr *args, double in_mode,
double out_mode, double err_mode, bool has_env,
ScrArr *env_pairs, ScrStr *cwd); /* +1 */
/* The options form (cp.spawnOpts): PER-SLOT stdio modes — 0 = ignore
* (/dev/null), 1 = inherit, 2 = fd (out/err only: out_fd/err_fd dup2
* into the child's slot, the openSync daemon-log idiom), 3 = pipe
* (stdin's write end becomes child.stdin; out/err read ends become the
* child.stdout/stderr streams);
* detached = POSIX_SPAWN_SETSID (own session/process group); shell runs
* the complete command through /bin/sh -c or cmd.exe /d /s /c; env
* REPLACES the child environment when has_env ([k,v,...] pairs); cwd ""
* = inherit. */
ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode,
double out_mode, double err_mode, double out_fd,
double err_fd, bool detached, bool shell, bool has_env,
ScrArr *env_pairs, ScrStr *cwd);
/* The callback slice: file + args, default options. The returned child has
* all three stdio slots piped; stdout/stderr are captured internally and
* the callback moves into the child registry until settlement. */
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb /*moves*/,
ScrExecFileFn fn);
ScrChild *scr_child_retain(ScrChild *c);
void scr_child_release(ScrChild *c);
void *scr_child_retain_v(void *p);
void scr_child_release_v(void *p);
void scr_child_on_exit(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildExitFn fn);
void scr_child_on_close(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildExitFn fn);
void scr_child_on_error(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildErrFn fn);
bool scr_child_ipc_connected(ScrChild *c);
bool scr_child_ipc_send(ScrChild *c, ScrStr *json);
bool scr_child_ipc_send_cb(ScrChild *c, ScrStr *json,
ScrClosure *cb /*moves*/, ScrIpcSendFn fn);
void scr_child_ipc_disconnect(ScrChild *c);
void scr_child_ipc_on_message(ScrChild *c, ScrClosure *cb /*moves*/,
ScrIpcMessageFn fn, bool once);
void scr_child_ipc_on_disconnect(ScrChild *c, ScrClosure *cb /*moves*/,
bool once);
double scr_process_fork_target(double target_count);
bool scr_process_ipc_connected(void);
bool scr_process_ipc_send(ScrStr *json);
bool scr_process_ipc_send_cb(ScrStr *json, ScrClosure *cb /*moves*/,
ScrIpcSendFn fn);
void scr_process_ipc_disconnect(void);
void scr_process_ipc_on_message(ScrClosure *cb /*moves*/,
ScrIpcMessageFn fn, bool once);
void scr_process_ipc_on_disconnect(ScrClosure *cb /*moves*/, bool once);
void scr_child_exit_thunk0(ScrClosure *cb, bool has_code, double code,
const char *signal_name);
void scr_child_err_thunk0(ScrClosure *cb, ScrStr *msg);
void scr_child_err_thunk_error(ScrClosure *cb, ScrStr *msg);
/* The stream surface: child.stdout/stderr answer +1 handles (NULL when
* the slot was not piped — Node's null); listeners MOVE in and release
* at EOF/exit-cleanup (post-'end' registrations release immediately and
* never fire, the stdin rule). The runtime-provided data adapters cover
* zero-param, Buffer, and string shapes; setEncoding uses the shared
* StringDecoder core so split multibyte sequences remain intact.
* Union-param listeners get compiler-emitted Buffer adapters. */
ScrChildStream *scr_child_stream_retain(ScrChildStream *s);
void scr_child_stream_release(ScrChildStream *s);
void *scr_child_stream_retain_v(void *p);
void scr_child_stream_release_v(void *p);
ScrChildStream *scr_child_stdout(ScrChild *c); /* +1, or NULL */
ScrChildStream *scr_child_stderr(ScrChild *c); /* +1, or NULL */
void scr_child_stream_on_data(ScrChildStream *s, ScrClosure *cb /*moves*/,
ScrChildStreamDataFn fn, bool once);
void scr_child_stream_on_data_str(ScrChildStream *s, ScrClosure *cb /*moves*/,
ScrChildStreamDataStrFn fn, bool once);
void scr_child_stream_on_end(ScrChildStream *s, ScrClosure *cb /*moves*/, bool once);
ScrChildStream *scr_child_stream_set_encoding(ScrChildStream *s, ScrStr *enc); /* +1 */
void scr_child_stream_thunk0(ScrClosure *cb, ScrBytes *chunk);
void scr_child_stream_thunk_bytes(ScrClosure *cb, ScrBytes *chunk);
void scr_child_stream_thunk_str(ScrClosure *cb, ScrStr *chunk);
ScrChildWriter *scr_child_writer_retain(ScrChildWriter *w);
void scr_child_writer_release(ScrChildWriter *w);
void *scr_child_writer_retain_v(void *p);
void scr_child_writer_release_v(void *p);
ScrChildWriter *scr_child_stdin(ScrChild *c); /* +1, or NULL */
bool scr_child_writer_write_string(ScrChildWriter *w, const ScrStr *data);
bool scr_child_writer_write_bytes(ScrChildWriter *w, const ScrBytes *data);
void scr_child_writer_end(ScrChildWriter *w);
void scr_child_writer_destroy(ScrChildWriter *w);
bool scr_child_writer_writable(ScrChildWriter *w);
void scr_child_writer_on_drain(ScrChildWriter *w, ScrClosure *cb /*moves*/, bool once);
void scr_child_writer_on_finish(ScrChildWriter *w, ScrClosure *cb /*moves*/, bool once);
void scr_child_writer_on_error(ScrChildWriter *w, ScrClosure *cb /*moves*/,
ScrChildErrFn fn, bool once);
/* The lifecycle members, Node's exact shapes (pinned by the corpus):
* pid is undefined (has_pid false) exactly on spawn failure; exitCode is
* null while running / after a signal death, the code after a normal
* exit, -errno once a spawn failure settled; killed flips on any
* successful kill() send (signal 0 included); kill answers false after
* the reap or on spawn failure and throws the Unknown-signal TypeError
* on bad names (may-throw); unref() drops the child from the loop's
* keep-alive set (reffed_pending is the loop's liveness half; teardown
* releases whatever the loop never reaped so the RC audit stays clean). */
bool scr_child_has_pid(ScrChild *c);
double scr_child_pid(ScrChild *c);
bool scr_child_has_exit_code(ScrChild *c);
double scr_child_exit_code(ScrChild *c);
bool scr_child_killed(ScrChild *c);
bool scr_child_kill(ScrChild *c, const ScrStr *signal);
bool scr_child_kill_num(ScrChild *c, double signum);
void scr_child_unref(ScrChild *c);
/* Installs ChildProcess's checked-dynamic handle identity bridge. Emitted
* programs call this only when the child_process surface is present. */
void scr_child_dyn_install(void);
bool scr_children_reffed_pending(void);
void scr_children_teardown(void);
/* Node's signal-name table (scr_lib.c), shared with child.kill: the
* resolved signal number, or -1 for names outside the table. */
int scr_signal_from_name(const ScrStr *signal);
/* The reverse walk (spawnSync's result.signal): Node's spelling for the
* host number, NULL outside the table. Static storage. */
const char *scr_signal_name(int sig);
/* The loop's half: pending() joins the exhaustion test (a live child
* keeps the process alive, like Node); poll() reaps and fires. wait()
* takes the loop's quiescent sleep while children are pending — it blocks
* on the child watch (kqueue on BSD, pidfd epoll on Linux) up to
* max_wait_ms (the next timer deadline), so a
* child's NOTE_EXIT wakes the loop immediately; it returns false (and the
* loop falls back to the ~1ms polling cap) when it can't wake for every
* pending child: non-kqueue platforms, spawn failures awaiting their
* first-pass settle, or a child whose exit filter could not be armed. */
bool scr_children_pending(void);
/* Work already queued in userspace: dispatch can progress without another
* pipe/exit notification. Connected channels and blocked writes are not ready. */
bool scr_children_ready(void);
bool scr_children_failed_pending(void);
void scr_children_poll(void);
bool scr_children_wait(double max_wait_ms);
/* ── node:crypto (scr_lib.c) ─────────────────────────────────────────
* Randomness plus the incremental MD5/SHA-1/SHA-256 Hash/Hmac and PBKDF2
* utility slice. The composed string-producing paths remain fast paths;
* first-class handles and Buffer results use the same digest core. */
ScrStr *scr_crypto_random_uuid(void);
ScrStr *scr_crypto_random_string(double n, ScrStr *enc); /* +1, or throws */
/* The fused createHash chain and crypto.hash. Strings hash their UTF-8
* bytes; byte values hash their raw storage. Unknown runtime algorithms
* preserve each Node API's distinct error message, class, and code. */
ScrStr *scr_crypto_hash_digest_str(ScrStr *alg, ScrStr *data, ScrStr *enc);
ScrStr *scr_crypto_hash_digest_bytes(ScrStr *alg, ScrBytes *data, ScrStr *enc);
typedef struct ScrCryptoHash ScrCryptoHash;
ScrCryptoHash *scr_crypto_hash_new(ScrStr *alg);
ScrCryptoHash *scr_crypto_hmac_new_str(ScrStr *alg, ScrStr *key);
ScrCryptoHash *scr_crypto_hmac_new_bytes(ScrStr *alg, ScrBytes *key);
ScrCryptoHash *scr_crypto_hash_retain(ScrCryptoHash *h);
void scr_crypto_hash_release(ScrCryptoHash *h);
void *scr_crypto_hash_retain_v(void *p);
void scr_crypto_hash_release_v(void *p);
ScrCryptoHash *scr_crypto_hash_update_str(ScrCryptoHash *h, ScrStr *data);
ScrCryptoHash *scr_crypto_hash_update_bytes(ScrCryptoHash *h, ScrBytes *data);
ScrCryptoHash *scr_crypto_hash_copy(ScrCryptoHash *h);
ScrStr *scr_crypto_hash_digest_string(ScrCryptoHash *h, ScrStr *enc);
ScrBytes *scr_crypto_hash_digest_buffer(ScrCryptoHash *h);
bool scr_crypto_timing_safe_equal(ScrBytes *left, ScrBytes *right);
ScrBytes *scr_crypto_random_fill(ScrBytes *bytes, double offset, double size);
ScrBytes *scr_crypto_random_fill_rest(ScrBytes *bytes, double offset);
double scr_crypto_random_int(double min, double max);
ScrBytes *scr_crypto_pbkdf2(ScrBytes *password, ScrBytes *salt,
double iterations, double keylen, ScrStr *digest);
typedef void (*ScrCryptoBytesFn)(ScrClosure *cb, ScrBytes *value /* moves */);
void scr_crypto_defer_bytes(ScrBytes *value /* moves */, ScrClosure *cb /* moves */,
ScrCryptoBytesFn fn);
void scr_crypto_random_bytes_async(double size, ScrClosure *cb /* moves */,
ScrCryptoBytesFn fn);
void scr_crypto_pbkdf2_async(ScrBytes *password, ScrBytes *salt,
double iterations, double keylen, ScrStr *digest,
ScrClosure *cb /* moves */, ScrCryptoBytesFn fn);
/* One-shot raw digest/HMAC by algorithm name ("md5" | "sha1" | "sha256")
* — the island crypto shim's bridge (scr_island.c host hooks). Digest
* bytes into out (≥32); returns the digest length, 0 for an unknown
* algorithm. */
size_t scr_crypto_digest_raw(const char *alg, const unsigned char *data, size_t len,
unsigned char out[32]);
size_t scr_crypto_hmac_raw(const char *alg, const unsigned char *key, size_t keylen,
const unsigned char *data, size_t len, unsigned char out[32]);
/* The composed `new crypto.X509Certificate(data).fingerprint` read (the
* handle never materializes): the SHA-1 of the DER, uppercase
* colon-separated — PEM or raw-DER input; anything else throws Node's
* ERR_OSSL_PEM_NO_START_LINE Error. Borrowed; +1 (may throw). */
ScrStr *scr_crypto_x509_fingerprint(ScrBytes *data);
ScrStr *scr_crypto_x509_fingerprint_str(ScrStr *pem);
/* The Validity window in Node's ASN1_TIME_print shape ("Jul 1 00:00:00
* 2026 GMT"); same PEM contract as the fingerprint pair. */
ScrStr *scr_crypto_x509_valid_from(ScrBytes *data);
ScrStr *scr_crypto_x509_valid_from_str(ScrStr *pem);
ScrStr *scr_crypto_x509_valid_to(ScrBytes *data);
ScrStr *scr_crypto_x509_valid_to_str(ScrStr *pem);
/* ── node:path (scr_path.c) ──────────────────────────────────────────
* BOTH of Node's implementations, ported exactly: the scr_path_* family
* is posix, the scr_path_win32_* family is Node v24's path.win32 (the
* compiler binds the bare module by TARGET; the path.posix/path.win32
* namespaces bind their family anywhere). All arguments are BORROWED;
* every string result is fresh (+1). Nothing throws. join/resolve take
* the call's variadic arguments packed into ONE string[] (the compiler
* builds the array literal); the resolves consult getcwd like Node
* (failure aborts, like process.cwd), the win32 one after the per-drive
* "=X:" env vars. basename always receives a suffix — "" (a Node no-op)
* when the call omitted it. */
ScrStr *scr_path_join(ScrArr *parts);
ScrStr *scr_path_resolve(ScrArr *parts);
ScrStr *scr_path_normalize(ScrStr *path);
ScrStr *scr_path_dirname(ScrStr *path);
ScrStr *scr_path_basename(ScrStr *path, ScrStr *suffix);
ScrStr *scr_path_extname(ScrStr *path);
bool scr_path_is_absolute(ScrStr *path);
ScrStr *scr_path_relative(ScrStr *from, ScrStr *to);
/* posix.toNamespacedPath — the identity (Node: "Non-op on posix
* systems"). +1 via retain. */
ScrStr *scr_path_to_namespaced_path(ScrStr *path);
/* path.win32.* — Node v24's win32 implementation ported byte-for-byte
* (backslash output, both slashes on input, UNC/device/drive-letter
* roots, the reserved device names, the CVE-2024-36139 normalize
* hardening); join/resolve keep the packed-string[] ABI. On win32
* TARGETS these back the bare `path` module (Node on Windows IS
* path.win32); everywhere they are the path.win32 namespace. resolve
* consults the per-drive "=X:" env vars then the cwd, like Node's.
* Same borrow/+1 contract as the posix set; nothing throws. */
ScrStr *scr_path_win32_join(ScrArr *parts);
ScrStr *scr_path_win32_resolve(ScrArr *parts);
ScrStr *scr_path_win32_normalize(ScrStr *path);
ScrStr *scr_path_win32_dirname(ScrStr *path);
ScrStr *scr_path_win32_basename(ScrStr *path, ScrStr *suffix);
ScrStr *scr_path_win32_extname(ScrStr *path);
bool scr_path_win32_is_absolute(ScrStr *path);
ScrStr *scr_path_win32_relative(ScrStr *from, ScrStr *to);
ScrStr *scr_path_win32_to_namespaced_path(ScrStr *path);
/* fs.openSync(path, flags) → the raw fd as f64 (Node's string flag
* grammar; unknown flags throw Node's ERR_INVALID_ARG_VALUE TypeError
* text, open(2) failures the Node fs error shape), and fs.closeSync(fd)
* (failure throws the path-less "EBADF: bad file descriptor, close").
* The pair behind spawn's fd-stdio form. */
double scr_fs_open(ScrStr *path, ScrStr *flags);
double scr_fs_open_numeric(ScrStr *path, double flags, double mode);
ScrFileHandle *scr_file_handle_open(ScrStr *path, ScrStr *flags, double mode);
ScrFileHandle *scr_file_handle_retain(ScrFileHandle *h);
void scr_file_handle_release(ScrFileHandle *h);
void *scr_file_handle_retain_v(void *p);
void scr_file_handle_release_v(void *p);
double scr_file_handle_fd(ScrFileHandle *h);
void scr_file_handle_close(ScrFileHandle *h);
double scr_file_handle_read(ScrFileHandle *h, ScrBytes *buf, double offset,
double length, double position, bool length_default);
double scr_file_handle_write_bytes(ScrFileHandle *h, ScrBytes *buf,
double offset, double length,
double position, bool length_default);
double scr_file_handle_write_str(ScrFileHandle *h, ScrStr *data,
double position, ScrStr *encoding);
ScrStr *scr_file_handle_read_file(ScrFileHandle *h);
ScrBytes *scr_file_handle_read_file_bytes(ScrFileHandle *h);
void scr_file_handle_write_file(ScrFileHandle *h, ScrStr *data);
void scr_file_handle_write_file_bytes(ScrFileHandle *h, ScrBytes *data);
ScrStats *scr_file_handle_stat(ScrFileHandle *h);
/* position == -1 reads from and advances the descriptor's current offset;
* nonnegative positions leave that offset unchanged (pread/ReadFile seam). */
double scr_fs_read_sync(double fd, ScrBytes *buf, double offset, double length,
double position);
/* writeSync's classic Buffer window and utf8 string forms. position -1 (or
* any other non-safe/nonnegative-integer value) writes at and advances the
* current descriptor offset; safe nonnegative positions leave it unchanged. */
double scr_fs_write_sync(double fd, ScrBytes *buf, double offset, double length,
double position);
double scr_fs_write_str_sync(double fd, ScrStr *data, double position,
ScrStr *encoding);
void scr_fs_close(double fd);
/* ── WHATWG URL (scr_url.c) ──────────────────────────────────────────
* An immutable, refcounted URL value, parsed once at construction. The
* parser covers the WHATWG algorithm's common ground exactly (see
* scr_url.c's header comment for the covered surface and the documented
* divergences: no IDNA, no IPv6, opaque paths verbatim).
*
* scr_url_new THROWS a catchable TypeError ("Invalid URL") through the
* exception cell on unparsable input and returns NULL; the getters never
* throw (borrowed receiver, +1 string). scr_url_to_path / _str_to_path
* throw Node's fileURLToPath TypeErrors (non-file scheme, encoded '/',
* non-empty host); scr_url_from_path resolves against the cwd and never
* throws. Compiler-emitted pending checks follow every throwing call. */
typedef struct ScrUrl ScrUrl;
ScrUrl *scr_url_new(ScrStr *input); /* +1, or throws */
ScrUrl *scr_url_new_base(ScrStr *input, ScrStr *base); /* +1, or throws */
ScrUrl *scr_url_retain(ScrUrl *u);
void scr_url_release(ScrUrl *u);
void *scr_url_retain_v(void *p);
void scr_url_release_v(void *p);
ScrStr *scr_url_protocol(ScrUrl *u); /* +1 "https:" */
ScrStr *scr_url_origin(ScrUrl *u); /* +1 "https://host" or "null" */
ScrStr *scr_url_username(ScrUrl *u); /* +1 encoded username, possibly empty */
ScrStr *scr_url_password(ScrUrl *u); /* +1 encoded password, possibly empty */
ScrStr *scr_url_host(ScrUrl *u); /* +1 "host[:port]" (defaults stripped) */
ScrStr *scr_url_hostname(ScrUrl *u); /* +1 port-less host ("" when none) */
ScrStr *scr_url_port(ScrUrl *u); /* +1 "" or normalized non-default port */
ScrStr *scr_url_pathname(ScrUrl *u); /* +1 */
ScrStr *scr_url_href(ScrUrl *u); /* +1; also toString() */
ScrStr *scr_url_to_path(ScrUrl *u); /* +1, or throws */
ScrStr *scr_url_str_to_path(ScrStr *s); /* +1, or throws */
ScrUrl *scr_url_from_path(ScrStr *path); /* +1; throws on win32 UNC malformations only */
ScrUrl *scr_url_from_path_platform(ScrStr *path, bool windows); /* +1; explicit options.windows */
/* The bridge pair's win32 arms as direct entry points (the public pair
* selects by TARGET at compile time — Node's isWindows): the host-side
* differential tests exercise the Windows behavior from any platform,
* like Node's { windows: true } options on fileURLToPath/pathToFileURL. */
ScrStr *scr_url_to_path_w32(ScrUrl *u); /* +1, or throws */
ScrUrl *scr_url_from_path_w32(ScrStr *path); /* +1, or throws */
/* ── URLSearchParams (scr_url.c) ─────────────────────────────────────
* A refcounted, MUTABLE ordered list of decoded (name, value) pairs —
* the WHATWG application/x-www-form-urlencoded surface. Standalone
* (scr_sp_new/parse/copy/from_pairs) or the LIVE view of a URL's query
* (scr_url_search_params — cached on the URL so every read answers the
* same identity, and every list mutation re-serializes into the URL's
* query: href reflects immediately; an emptied list drops the '?').
* Parsing/serialization match Node byte-for-byte: '+' is space, %XX
* decodes (malformed escapes verbatim, decoded bytes UTF-8-scrubbed to
* U+FFFD), serialization keeps [A-Za-z0-9*\-._], space → '+', uppercase
* %XX otherwise. sort() is stable, by UTF-16 code units. Only
* scr_sp_from_pairs throws (ERR_INVALID_TUPLE on a non-[name, value]
* row); everything else never does. */
typedef struct ScrSearchParams ScrSearchParams;
ScrSearchParams *scr_sp_new(void); /* +1 empty list */
ScrSearchParams *scr_sp_parse(ScrStr *init); /* +1; strips ONE leading '?' */
ScrSearchParams *scr_sp_copy(ScrSearchParams *src); /* +1 snapshot */
ScrSearchParams *scr_sp_from_pairs(ScrArr *pairs); /* +1, or throws (string[][] rows) */
/* The record-literal init desugar: append (name, value), answer sp +1. */
ScrSearchParams *scr_sp_with(ScrSearchParams *sp, ScrStr *name, ScrStr *value);
ScrSearchParams *scr_sp_retain(ScrSearchParams *sp);
void scr_sp_release(ScrSearchParams *sp);
void *scr_sp_retain_v(void *p);
void scr_sp_release_v(void *p);
ScrSearchParams *scr_url_search_params(ScrUrl *u); /* +1 live cached view */
ScrStr *scr_url_search(ScrUrl *u); /* +1 "?..." or "" */
ScrStr *scr_url_hash(ScrUrl *u); /* +1 "#..." or "" */
void scr_sp_append(ScrSearchParams *sp, ScrStr *name, ScrStr *value);
void scr_sp_set(ScrSearchParams *sp, ScrStr *name, ScrStr *value);
void scr_sp_delete(ScrSearchParams *sp, ScrStr *name);
void scr_sp_delete_value(ScrSearchParams *sp, ScrStr *name, ScrStr *value);
ScrStr *scr_sp_get(ScrSearchParams *sp, ScrStr *name); /* +1 first value, or NULL */
ScrArr *scr_sp_get_all(ScrSearchParams *sp, ScrStr *name); /* +1 string[] */
bool scr_sp_has(ScrSearchParams *sp, ScrStr *name);
bool scr_sp_has_value(ScrSearchParams *sp, ScrStr *name, ScrStr *value);
void scr_sp_sort(ScrSearchParams *sp);
double scr_sp_size(ScrSearchParams *sp);
ScrStr *scr_sp_to_string(ScrSearchParams *sp); /* +1 urlencoded serialization */
/* Index-walk iteration (the compiler's for-of/forEach desugar re-reads
* scr_sp_size each pass — live, like the spec's index-based iterator). */
ScrStr *scr_sp_key_at(ScrSearchParams *sp, double i); /* +1; "" out of range */
ScrStr *scr_sp_val_at(ScrSearchParams *sp, double i); /* +1; "" out of range */
/* ── ES Symbol values (scr_symbol.c — linked only when the IR uses the
* symbol surface) ─────────────────────────────────────────────────────
* A symbol is a runtime-unique IDENTITY: the pointer IS the identity
* (`===` is a pointer compare; every scr_sym_new allocation is a distinct
* symbol even for equal descriptions — JS exactly). Heap, refcounted,
* IMMUTABLE; holds at most two strings (description + registry key), so
* never part of a cycle, no trace. Symbol.for's global registry interns
* per KEY (byte equality) and is spec-permanent — entries are never
* evicted; an atexit cleanup (registered at first use, so LIFO runs it
* BEFORE scr_init's RC audit) releases the chain so audited string counts
* stay exact. None of these throw. */
typedef struct ScrSym ScrSym;
ScrSym *scr_sym_new(ScrStr *desc); /* borrows desc (NULL = no description); +1 fresh identity */
ScrSym *scr_sym_for(ScrStr *key); /* borrows key; +1 on the interned per-key symbol */
ScrSym *scr_sym_retain(ScrSym *s);
void scr_sym_release(ScrSym *s);
void *scr_sym_retain_v(void *p);
void scr_sym_release_v(void *p);
ScrStr *scr_sym_desc(ScrSym *s); /* +1 description, or NULL when absent */
ScrStr *scr_sym_key_for(ScrSym *s); /* +1 registry key, or NULL (not registered) */
ScrStr *scr_sym_to_string(ScrSym *s); /* +1 "Symbol(desc)" ("Symbol()" when absent) */
/* ── ECMAScript bigint values (scr_bigint.c, linked on demand) ─────────
* Immutable arbitrary-precision signed integers. The implementation uses
* little-endian base-2^32 limbs, but callers see only value semantics:
* equality and ordering compare numbers, never pointers. */
typedef struct ScrBigInt ScrBigInt;
ScrBigInt *scr_bigint_retain(ScrBigInt *v);
void scr_bigint_release(ScrBigInt *v);
void *scr_bigint_retain_v(void *v);
void scr_bigint_release_v(void *v);
ScrBigInt *scr_bigint_parse(ScrStr *text); /* borrowed text; +1 or throws */
ScrBigInt *scr_bigint_from_f64(double value); /* +1 or throws */
ScrBigInt *scr_bigint_from_u64(uint64_t value);
ScrBigInt *scr_bigint_from_i64(int64_t value);
ScrBigInt *scr_bigint_from_pointer(void *value);
uint64_t scr_bigint_to_u64(const ScrBigInt *value); /* low 64 bits, modulo 2^64 */
int64_t scr_bigint_to_i64(const ScrBigInt *value);
bool scr_bigint_pointer_fits(const ScrBigInt *value);
bool scr_bigint_u64_fits(const ScrBigInt *value);
bool scr_bigint_i64_fits(const ScrBigInt *value);
void *scr_bigint_to_pointer(const ScrBigInt *value); /* low pointer-width bits; raw address, borrowed */
ScrBigInt *scr_bigint_neg(ScrBigInt *value);
ScrBigInt *scr_bigint_not(ScrBigInt *value);
ScrBigInt *scr_bigint_add(ScrBigInt *a, ScrBigInt *b);
ScrBigInt *scr_bigint_sub(ScrBigInt *a, ScrBigInt *b);
ScrBigInt *scr_bigint_mul(ScrBigInt *a, ScrBigInt *b);
ScrBigInt *scr_bigint_div(ScrBigInt *a, ScrBigInt *b); /* throws on zero */
ScrBigInt *scr_bigint_mod(ScrBigInt *a, ScrBigInt *b); /* throws on zero */
ScrBigInt *scr_bigint_pow(ScrBigInt *a, ScrBigInt *b); /* throws on negative exponent */
ScrBigInt *scr_bigint_and(ScrBigInt *a, ScrBigInt *b);
ScrBigInt *scr_bigint_or(ScrBigInt *a, ScrBigInt *b);
ScrBigInt *scr_bigint_xor(ScrBigInt *a, ScrBigInt *b);
ScrBigInt *scr_bigint_shl(ScrBigInt *a, ScrBigInt *count);
ScrBigInt *scr_bigint_shr(ScrBigInt *a, ScrBigInt *count);
bool scr_bigint_eq(ScrBigInt *a, ScrBigInt *b);
bool scr_bigint_eq_string(ScrBigInt *a, ScrStr *b); /* StringToBigInt failure is false */
double scr_bigint_cmp_f64(ScrBigInt *a, ScrBigInt *b); /* -1, 0, 1 */
double scr_bigint_cmp_number(ScrBigInt *a, double b); /* -1, 0, 1; 2 = NaN */
bool scr_bigint_truthy(ScrBigInt *value);
ScrStr *scr_bigint_to_string(ScrBigInt *value, double radix); /* +1 or throws */
ScrStr *scr_bigint_inspect(ScrBigInt *value); /* decimal plus trailing n; +1 */
double scr_bigint_to_f64(ScrBigInt *value);
ScrBigInt *scr_bigint_as_uint_n(double bits, ScrBigInt *value);
ScrBigInt *scr_bigint_as_int_n(double bits, ScrBigInt *value);
ScrBigInt *scr_bigint_buffer_read(ScrBytes *bytes, double offset, bool sign, bool le);
double scr_bigint_buffer_write(ScrBytes *bytes, ScrBigInt *value, double offset, bool sign, bool le);
ScrBigInt *scr_bigint_dataview_get(ScrBytes *bytes, double offset, bool sign, bool le);
void scr_bigint_dataview_set(ScrBytes *bytes, double offset, ScrBigInt *value, bool le);
/* ── node:os (scr_lib.c) ─────────────────────────────────────────────
* os.platform() lowers to scr_process_platform. homedir is $HOME else
* getpwuid(3) (failure aborts); tmpdir is Node's $TMPDIR/$TMP/$TEMP
* cascade else /tmp, one trailing slash trimmed. Both +1 fresh. */
ScrStr *scr_os_homedir(void);
ScrStr *scr_os_tmpdir(void);
ScrStr *scr_os_release(void); /* uname(2) release — +1 fresh */
ScrStr *scr_os_type(void); /* uname(2) sysname — +1 fresh */
double scr_os_totalmem(void); /* total physical memory, bytes */
/* os.userInfo()'s field trio (pw_name / pw_shell / pw_dir — the passwd
* home, not the $HOME cascade). +1 fresh; abort on lookup failure. */
ScrStr *scr_os_user_name(void);
ScrStr *scr_os_user_shell(void);
ScrStr *scr_os_user_homedir(void);
/* fs.readdirSync(path, { withFileTypes: true }): one readdir pass
* snapshotting name + entry kind (scr_lib.c) — the emitter walks it to
* assemble the typed Dirent record rows inline. Entry kinds use libuv's
* UV_DIRENT encoding (1 file, 2 dir, 3 link, 4 fifo, 5 socket, 6 char,
* 7 block, 0 unknown); a DT_UNKNOWN d_type falls back to lstat(2) —
* Node's own getDirents rule. OS order, no "."/"..". scr_fs_scandir
* throws Node's scandir errno error and answers NULL then; count/free are
* NULL-tolerant so the promise emitter can turn that pending error into a
* rejection, while the name accessor returns +1. */
typedef struct ScrScandir ScrScandir;
ScrScandir *scr_fs_scandir(ScrStr *path); /* or throws (NULL) */
size_t scr_fs_scandir_count(const ScrScandir *s); /* NULL -> 0 */
ScrStr *scr_fs_scandir_name(const ScrScandir *s, size_t i); /* +1 */
double scr_fs_scandir_type(const ScrScandir *s, size_t i);
void scr_fs_scandir_free(ScrScandir *s); /* NULL-tolerant */
/* os.networkInterfaces(): a getifaddrs(3) snapshot (scr_lib.c) the emitter
* walks to build the typed Dict<NetworkInterfaceInfo[]> record inline. Row
* selection and fields follow libuv/Node (IFF_UP && IFF_RUNNING,
* AF_INET/AF_INET6; internal = IFF_LOOPBACK; MAC from the link-level
* sibling entry, zeros when absent; cidr = address/<netmask prefix>, NULL
* for a split netmask — Node's null). String accessors return +1; a
* getifaddrs failure yields an empty snapshot. */
typedef struct ScrIfaddrs ScrIfaddrs;
ScrIfaddrs *scr_os_ifaddrs(void);
size_t scr_os_ifaddrs_count(const ScrIfaddrs *s);
ScrStr *scr_os_ifaddrs_name(const ScrIfaddrs *s, size_t i);
ScrStr *scr_os_ifaddrs_address(const ScrIfaddrs *s, size_t i);
ScrStr *scr_os_ifaddrs_netmask(const ScrIfaddrs *s, size_t i);
ScrStr *scr_os_ifaddrs_family(const ScrIfaddrs *s, size_t i); /* "IPv4"/"IPv6" */
ScrStr *scr_os_ifaddrs_mac(const ScrIfaddrs *s, size_t i);
bool scr_os_ifaddrs_internal(const ScrIfaddrs *s, size_t i);
bool scr_os_ifaddrs_ipv6(const ScrIfaddrs *s, size_t i);
ScrStr *scr_os_ifaddrs_cidr(const ScrIfaddrs *s, size_t i); /* +1 or NULL */
double scr_os_ifaddrs_scopeid(const ScrIfaddrs *s, size_t i);
void scr_os_ifaddrs_free(ScrIfaddrs *s);
/* ── JSON + dynamic values (scr_json.c) ─────────────────────────────
* A dyn value — the compiled form of `unknown` — is a refcounted JSON dyn
* tree: the result of JSON.parse, inert until a CHECKED CAST validates it
* against a static type. The compiler emits the validation walkers
* (sc_dc_* builders / sc_dm_* match predicates) and the type-directed
* stringify serializers (sc_jw_*) per program; this runtime slice provides
* the checked-dynamic tree itself, the RFC 8259 parser, the shared failure path
* (scr_dyn_check_fail), and the output buffer the serializers append to.
*
* Ownership: the checked-dynamic tree owns its children (strings, items, entry keys+values);
* releasing the root releases the tree recursively. scr_json_parse borrows
* the text and returns +1 — or THROWS a catchable string shaped like Node's
* V8 messages ("Unexpected end of JSON input"; approximate fidelity, see
* SEMANTICS.md) and returns NULL. scr_dyn_obj_get returns a BORROWED
* member (the compiler's builders retain what they extract themselves).
*/
typedef enum {
SCR_DYN_NULL,
SCR_DYN_BOOL,
SCR_DYN_NUM,
SCR_DYN_STR,
SCR_DYN_ARR,
SCR_DYN_OBJ,
/* The undefined VALUE. Never produced by the parser (JSON text has no
* undefined) — it enters the checked-dynamic tree through index-signature overflow reads
* (a missing key), optional-chain unit paths on dyn results, and the
* compiler's static→dyn converters (an undefined-armed union's unit
* arm). One immortal instance (scr_dyn_undefined); JSON serialization
* drops object members holding it and prints null for array slots,
* exactly Node. dynCheck matches it against exactly the undefined arm. */
SCR_DYN_UNDEF,
/* A numeric typed-array/Buffer value retained by reference. Boxing and
* checked extraction preserve the view, exact element kind, and backing
* allocation. typeof answers "object"; String() joins numeric elements
* (Buffer keeps its encoding behavior); JSON writes an index-keyed object.
* Exact-brand tests accept Buffer as Uint8Array. */
SCR_DYN_BYTES,
/* A FUNCTION value. Never produced by the parser — it enters the checked-dynamic tree
* through the compiler's static→dyn converters (a typed closure flowing
* into an `unknown` slot: `mustCall(fn)`-style untyped JS helpers). Owns
* a ScrClosure plus a compiler-emitted CALL THUNK that validates dyn
* arguments into the closure's declared parameter types (per-arg
* dynCheck — JS arity semantics: extra args ignored, missing args are
* the undefined dyn value) and converts the result back to a dyn value.
* `sig` is the compiler's interned signature key: dynCheck against an
* IDENTICAL function type unwraps the closure directly; any other
* (adaptable) function target wraps the box in a per-target adapter
* closure. typeof answers "function", truthiness is true, String()
* renders the native-code form (source text is gone — SEMANTICS.md),
* JSON serialization treats it like undefined (dropped from objects,
* null in arrays), and util.inspect prints [Function: name]. */
SCR_DYN_FUNC,
/* A NATIVE HANDLE value (httpReq/httpRes/netSocket crossing the checked-
* dynamic boundary — `server.on('request', mustCall((req, res) => ...))`
* makes the listener dyn, so the event tuple must box). Never produced
* by the parser — it enters the checked-dynamic tree through the compiler's static→dyn
* converters and the per-target function adapters. Boxes by REFERENCE
* (a retained pointer + a handle-type tag): handles are stateful I/O
* objects, so identity is the HANDLE, not the box — strict equality
* compares (tag, pointer), and dynCheck against the same handle type
* unwraps the pointer (+1) instead of copying. Member CALLS and the
* property reads/writes with static equivalents dispatch through a
* per-tag ops table (scr_dyn_handle_install — scr_http.c/scr_net.c
* register at main(), the vtable-stamp story) onto the SAME entry
* points the static lowerings use; unknown members meet the loud
* "not supported yet" ladder, never a silent wrong answer. typeof
* answers "object", truthiness is true, String() renders
* "[object Object]" (Object.prototype.toString — Node's answer for
* these classes), JSON serialization and util.inspect fence loudly. */
SCR_DYN_HANDLE,
/* A PROMISE value. Never produced by the parser — it enters the checked-dynamic tree
* through the compiler's static→dyn converters (a promise flowing into
* an `any`/`unknown` slot: the traced-function boundary of
* dc.tracingChannel.tracePromise, a dyn-boxed async closure's return).
* Boxes by REFERENCE (a retained ScrPromise + the boundary contract
* that a dyn-crossing promise settles with a dyn payload: promise<dyn>
* boxes its ScrPromise directly, other inner types box an ADAPTER
* promise whose emitted settle callback converts the payload — see
* scr_dyn_new_promise_adapting). Identity is the PROMISE, not the box
* (strict equality compares the pointers; Promise.resolve identity
* survives one crossing, a re-boxed typed promise mints a fresh
* adapter — SEMANTICS.md). typeof answers "object", truthiness is
* true, String() renders "[object Promise]" (Object.prototype.toString
* — promises have no own toString), JSON serialization writes {} (no
* own enumerable properties, exactly Node), and util.inspect fences
* loudly. The dyn→promise edge is NOT visible to the cycle collector
* (the dyn→closure stance): a cycle THROUGH a dyn-boxed promise is
* merely never collected. */
SCR_DYN_PROMISE,
/* An ISLAND (engine-held) value — the jsval→dyn crossing. Never
* produced by the parser — it enters the checked-dynamic tree through the gated
* constructor scr_dyn_from_jsval (scr_island.c): an 'any'-typed value
* flowing into an 'unknown'/'object'/JS-residue slot. Boxes by
* REFERENCE (a retained ScrJsval cell). The constructor SCALAR-
* NORMALIZES: engine numbers/strings/booleans/null/undefined convert
* to the native dyn kinds at wrap time, so JSVAL nodes only ever hold
* engine objects, arrays, and functions (plus the symbol/bigint edge —
* kinds the checked-dynamic tree cannot represent at all). Identity is the CELL's
* engine value: strict equality routes to the engine's === (two wraps
* of one engine value compare equal), and scr_jsval_from_dyn unwraps
* the SAME cell back (+1) — the boundary is identity-preserving for
* engine-born values. typeof/truthiness/String() route to the engine
* per use (scr_dyn_jsval_ops); every dyn walk without an armed route
* (JSON, structuredClone, deepStrictEqual, inspect, keyed access,
* calls, iteration) throws the LOUD "not supported yet" ladder — never
* a silent wrong answer. The dyn→cell edge is NOT visible to the cycle
* collector (the dyn→closure stance): a cycle dyn → cell → engine
* object → host closure → dyn is merely never collected (the
* documented cross-boundary-cycle divergence). */
SCR_DYN_JSVAL,
/* A compiler-owned typed reference. Program class instances use this for
* ordinary `unknown` storage; native Web streams also use it when their
* contract must expose the exact input reference again. The capsule
* retains the original value so an exact checked cast preserves identity;
* `materialize` supplies the ordinary own-field/data view to generic dyn
* consumers. LLVM hardcodes these kind numbers; append new kinds. */
SCR_DYN_TYPED_REF,
/* Native Proxy over a checked plain object; keep new tags at the end. */
SCR_DYN_PROXY,
SCR_DYN_BIGINT, /* exact native integer, retained by the box */
} ScrDynKind;
/* The handle-type tags the checked-dynamic tree can carry. The set is deliberately the
* HANDLE kinds whose member surfaces already have complete static
* lowerings (the http/net receiver surface); new kinds join by adding a
* tag + an ops registration in their owning unit. */
typedef enum {
SCR_DYNH_HTTP_REQ, /* ScrHttpReq — IncomingMessage (server req & client res) */
SCR_DYNH_HTTP_RES, /* ScrHttpRes — ServerResponse */
SCR_DYNH_NET_SOCKET, /* ScrNetSocket — net.Socket */
SCR_DYNH_NET_SERVER, /* ScrNetServer — net.Server (http.Server rides the same handle) */
SCR_DYNH_H2_SESSION, /* ScrH2Session — Http2Session (client & server) */
SCR_DYNH_H2_STREAM, /* ScrH2Stream — Http2Stream (client & server) */
SCR_DYNH_HTTP_CLIENT, /* ScrHttpClientReq — http.ClientRequest */
SCR_DYNH_HTTP_AGENT, /* ScrHttpAgent — http.Agent / https.Agent */
SCR_DYNH_ABORT_SIGNAL, /* native static-fetch AbortSignal */
SCR_DYNH_WEB_STREAM, /* native WHATWG ReadableStream */
SCR_DYNH_WEB_READER, /* native ReadableStreamDefaultReader */
SCR_DYNH_WEB_CONTROLLER, /* native ReadableStreamDefaultController */
SCR_DYNH_FETCH_RESPONSE, /* native static-fetch Response */
SCR_DYNH_FETCH_HEADERS, /* native static-fetch response Headers */
SCR_DYNH_EVENT, /* native static-fetch abort Event */
SCR_DYNH_ABORT_CONTROLLER, /* native static-fetch AbortController */
SCR_DYNH_CHILD, /* child_process.ChildProcess */
SCR_DYNH_FILE_HANDLE, /* fs/promises.FileHandle */
SCR_DYNH_ARRAY_BUFFER, /* fixed-length ArrayBuffer storage */
SCR_DYNH_SEGMENTER, /* default Unicode grapheme segmentation */
SCR_DYNH_SEGMENTS, /* iterable immutable segmentation result */
SCR_DYNH_BUILTIN_MODULE, /* native builtin export object */
SCR_DYNH_GLOBAL, /* native global object identity */
SCR_DYNH_WEAK_MAP, /* native weak-key metadata */
SCR_DYNH_WEAK_SET, /* native weak membership */
SCR_DYNH_SET, /* native Set<unknown>, shared backing map */
SCR_DYNH_REGEXP, /* native RegExp, shared compiled pattern */
SCR_DYNH_STDIO, /* stable process stdin/stdout/stderr values */
SCR_DYNH_URL, /* native WHATWG URL */
SCR_DYNH_COUNT,
} ScrDynHandleTag;
typedef struct ScrDyn ScrDyn;
typedef struct ScrBytes ScrBytes; /* full definition below (C11 repeat) */
typedef struct ScrClosure ScrClosure; /* full definition below (C11 repeat) */
typedef struct ScrDynTypedCast {
const char *type_key;
size_t type_key_len;
void *ptr;
void *(*retain)(void *);
void (*release)(void *);
struct ScrDynTypedCast *next;
} ScrDynTypedCast;
typedef struct ScrJsval ScrJsval; /* opaque island cell (C11 repeat; the
* always-linked dyn core never touches
* its engine value — only the gated ops
* installed by scr_dyn_from_jsval do) */
/* The compiler-emitted call glue carried by a SCR_DYN_FUNC box: checks the
* dyn arguments against the boxed closure's declared parameter types (a
* mismatch throws the catchable path-annotated TypeError and returns NULL
* with the exception pending), calls through the closure, and returns the
* result as an owned (+1) dyn value. `args` entries are BORROWED. */
typedef ScrDyn *(*ScrDynThunk)(ScrClosure *clo, ScrDyn *const *args, size_t argc);
/* Object member. Keys are malloc'd UTF-8 bytes (NUL-terminated for
* convenience; key_len excludes the NUL) — duplicate keys were already
* collapsed at parse time (later wins, like JS JSON.parse). */
typedef struct {
char *key;
size_t key_len;
ScrDyn *value; /* owned */
ScrDyn *getter; /* owned when this is an accessor property */
ScrDyn *setter; /* owned when this is an accessor property */
bool accessor;
/* Ordinary object insertion sets all three attributes true. */
bool writable;
bool enumerable;
bool configurable;
} ScrDynEntry;
struct ScrDyn {
size_t rc; /* SIZE_MAX = immortal (unused for dyn; kept per convention) */
ScrDynKind kind;
/* SCR_DYN_BYTES flavor: true when the value REPRESENTS a Node Buffer
* (stream chunks, buffer-typed boundary crossings) — Buffer's string
* coercion and toString() decode utf8, where a plain Uint8Array joins
* its elements ("1,2,3"). Everything else ignores it. */
bool buffer;
/* SCR_DYN_OBJ flavor: true for Object.create(null)'s dictionary — an
* object with NO prototype. Method dispatch needs nothing (the checked-dynamic tree's
* OBJ dispatch is already own-member-only, which IS Node's null-proto
* answer); util.inspect prefixes "[Object: null prototype]", and
* deepStrictEqual separates it from plain objects (Node compares
* prototypes — the bytes `buffer` gate's stance). Keyed reads/writes,
* Object.keys/entries/assign, JSON, and typeof are flag-blind, and
* fresh copies (structuredClone) DROP the flag — Node's serialization
* answers a plain object too. */
bool null_proto;
/* A deep typed-to-dyn copy has no stable native reference identity.
* Weak collections must refuse it instead of weakly tracking its box. */
bool copied_from_native;
bool non_extensible; /* native objects and arrays; fits the header padding */
union {
bool b;
double num;
ScrStr *str; /* owned */
ScrBytes *bytes; /* owned (SCR_DYN_BYTES) */
ScrBigInt *bigint; /* owned (SCR_DYN_BIGINT) */
struct { size_t len; size_t cap; ScrDyn **items; struct ScrDyn *properties; bool sealed; bool frozen; } arr; /* owned */
struct {
size_t len;
size_t cap;
ScrDynEntry *entries;
/* Optional identity of the typed record that produced this ordinary
* snapshot. Empty records and callback interfaces can retain their
* source across repeated static-to-dynamic crossings. */
/* Owned through source_access(source_identity, false). Callback
* interfaces may request a fresh snapshot with the true arm. */
void *source_identity;
ScrDyn *(*source_access)(void *, bool materialize);
} obj; /* owned */
/* SCR_DYN_FUNC: the boxed closure (owned) + its call descriptor. `sig`
* and `name` are static compiler-emitted literals (never freed); name
* may be NULL (anonymous — inspect prints [Function (anonymous)]).
* The dyn→closure edge is NOT visible to the cycle collector (ScrDyn
* has no trace header): trial deletion treats it as an external root,
* so nothing dangles — a cycle THROUGH a dyn-boxed function is merely
* never collected (documented divergence). */
struct { ScrClosure *clo; ScrDynThunk thunk; const char *sig; const char *name; uint32_t arity; ScrClassObj *class_obj; } fn;
/* Native handles normally keep conservative ownership edges. Sets
* backed by cycle-capable maps expose their edge to the collector.
* Cache that allocation property: during collection the map may be
* freed before its box, so disposal must not inspect the map again. */
struct { void *ptr; ScrDynHandleTag tag; bool traced; } handle;
/* SCR_DYN_TYPED_REF: original static value + its compiler-emitted
* identity tag, RC adapters, and ordinary dyn snapshot adapter. */
struct {
void *ptr;
void *(*retain)(void *);
void (*release)(void *);
const char *type_key;
size_t type_key_len;
ScrDyn *(*materialize)(void *);
void (*commit)(void *, const ScrDyn *);
/* Both caches belong to this capsule. ReadableStream canonicalizes
* capsules for repeated source references, so the refreshed dyn view
* and every structurally converted static view keep JS reference
* identity while the source reference remains observable. */
ScrDyn *materialized;
ScrDynTypedCast *casts;
bool traced; /* native referent has a cycle header */
} typed_ref;
/* SCR_DYN_PROMISE: the retained promise. The boundary contract: it
* settles with a dyn payload (SCR_EXC_REF ScrDyn fulfillment or a
* void fulfillment awaiters read as the undefined value) — the
* emitted converters guarantee it (direct box for promise<dyn>,
* adapter promise otherwise). */
ScrPromise *promise;
/* SCR_DYN_JSVAL: the retained island cell (engine objects/arrays/
* functions only — the constructor scalar-normalizes; see the kind's
* comment). Released through the installed ops so this always-linked
* core never references the gated island unit. */
struct { ScrJsval *cell; } jsval;
struct { ScrDyn *target; ScrDyn *handler; } proxy; /* both owned */
} v;
/* Keep the checked value payload at its established offset: generated
* native converters read array and object fields directly. */
struct ScrDyn *prototype;
};
static inline ScrDyn *scr_dyn_retain(ScrDyn *d) {
if (d->rc != SIZE_MAX) {
d->rc++;
scr_cyc_mark_live(d);
}
return d;
}
void scr_dyn_release(ScrDyn *d); /* releases the tree recursively; NULL-tolerant */
/* RFC 8259 parse of a UTF-8 string. Borrows text; returns +1, or throws a
* catchable Node-flavored SyntaxError string and returns NULL (callers are
* compiler-emitted pending checks — json.parse is in the may-throw seed). */
ScrDyn *scr_json_parse(ScrStr *text);
/* Native JSON callbacks. All inputs borrowed, result owned (+1), NULL on
* pending exception. Stringify returns a dyn string OR actual undefined
* when the replacer omits the root. gap has already applied space rules. */
ScrDyn *scr_json_parse_reviver(ScrStr *text, const ScrDyn *reviver);
ScrDyn *scr_json_stringify_replacer(const ScrDyn *value, const ScrDyn *replacer, const ScrStr *gap);
/* BORROWED member lookup on a SCR_DYN_OBJ; NULL when the key is absent. */
ScrDyn *scr_dyn_obj_get(const ScrDyn *d, const char *key, size_t key_len);
/* Own-enumerable check on a SCR_DYN_OBJ. */
bool scr_dyn_obj_enumerable(const ScrDyn *d, const char *key, size_t key_len);
/* Literal discriminator tests used before selecting a typed record layout.
* Missing fields and different primitive kinds never match. Borrowed args. */
bool scr_dyn_field_eq_str(const ScrDyn *d, const ScrStr *key, const ScrStr *value);
bool scr_dyn_field_eq_num(const ScrDyn *d, const ScrStr *key, double value);
bool scr_dyn_field_eq_bool(const ScrDyn *d, const ScrStr *key, bool value);
/* Object.keys/values/entries over the checked-dynamic tree: JS own-key order (array-index
* keys ascending first), dyn-array results (+1); values/entries RETAIN
* member nodes. null/undefined receivers throw Node's catchable
* TypeError. */
ScrDyn *scr_dyn_obj_keys(const ScrDyn *v);
ScrDyn *scr_dyn_for_in_keys(const ScrDyn *v);
/* Snapshot all own string keys of a SCR_DYN_OBJ in JS order. Returns +1. */
ScrDyn *scr_dyn_obj_own_keys(const ScrDyn *v);
ScrDyn *scr_dyn_get_own_property_names(const ScrDyn *value);
/* Object.hasOwn over a dyn receiver: OBJ member presence, ARR index
* bounds ("length" included); nullish receivers throw Node's ToObject
* TypeError; every other kind answers false. */
bool scr_dyn_has_own(const ScrDyn *v, const ScrStr *key);
bool scr_dyn_property_is_enumerable(const ScrDyn *value, const ScrStr *key);
/* Object.assign over dyn values (+1 target back; ToObject TypeError on a
* nullish target). Sources visit own keys in order and copy entries that
* are enumerable when reached; ARR/STR/BYTES expose index keys. Nullish
* and scalar/function/handle sources copy nothing. */
ScrDyn *scr_dyn_assign(ScrDyn *target, const ScrDyn *src);
ScrDyn *scr_dyn_copy_data_properties(ScrDyn *target, const ScrDyn *src);
/* Variadic Object.assign (the spread-source form): the compiler packs
* every source into one fresh dyn array — pack_push retains a plain
* source in (BORROWED), pack_push_spread flattens a spread source through
* the spread-call walk (V8's exact TypeError texts, `what` spelling the
* spread expression; MAY THROW pending) — then assign_all copies each
* pack element's own members onto the target left to right and answers
* the target retained (+1; ToObject TypeError on a nullish target). */
void scr_dyn_pack_push(ScrDyn *pack, ScrDyn *v);
void scr_dyn_pack_push_spread(ScrDyn *pack, const ScrDyn *src, const ScrStr *what);
/* The iterated-path twin — a spread that is NOT the single last argument
* takes V8's iterator-protocol failure texts, which describe the VALUE
* ("object null", "number 5", ...) instead of spelling the expression. */
void scr_dyn_pack_push_spread_iter(ScrDyn *pack, const ScrDyn *src);
ScrDyn *scr_dyn_assign_all(ScrDyn *target, const ScrDyn *sources);
ScrDyn *scr_dyn_obj_values(const ScrDyn *v);
ScrDyn *scr_dyn_obj_entries(const ScrDyn *v);
/* dyn construction — the compiler-emitted static→dyn converters (sc_td_*)
* and index-signature overflow machinery build dyn values directly.
* Constructors return +1; _new_str RETAINS the string into the node.
* arr_push and obj_set take OWNERSHIP of the value (+1 moves in); obj_set
* COPIES the key bytes and replaces a duplicate key's value (later wins,
* like JS). scr_dyn_undefined returns THE immortal undefined value
* (rc == SIZE_MAX — retains/releases are no-ops). */
ScrDyn *scr_dyn_undefined(void);
ScrDyn *scr_dyn_new_null(void);
ScrDyn *scr_dyn_new_bool(bool b);
ScrDyn *scr_dyn_new_num(double n);
ScrDyn *scr_dyn_native_set_new(const ScrDyn *seed);
ScrDyn *scr_dyn_native_set(ScrMap *value); /* borrowed backing, +1 box */
bool scr_dyn_native_set_is(const ScrDyn *value);
ScrRegex *scr_regex_new_checked(const ScrDyn *pattern, const ScrDyn *flags);
ScrDyn *scr_dyn_native_url(ScrUrl *value);
bool scr_dyn_native_url_is(const ScrDyn *value);
ScrDyn *scr_dyn_native_regex(ScrRegex *value);
bool scr_dyn_native_regex_is(const ScrDyn *value);
ScrDyn *scr_dyn_freeze(ScrDyn *value);
bool scr_dyn_is_frozen(const ScrDyn *value);
ScrDyn *scr_dyn_new_bigint(ScrBigInt *value); /* borrows and retains value */
ScrDyn *scr_dyn_new_str(ScrStr *s);
ScrDyn *scr_dyn_new_arr(void);
ScrDyn *scr_dyn_new_obj(void);
/* The ordinary object snapshot plus a retained typed source. source_access
* releases that source when materialize=false and returns a fresh snapshot
* when true. */
ScrDyn *scr_dyn_new_obj_with_identity(
void *source, void *(*source_retain)(void *),
ScrDyn *(*source_access)(void *, bool materialize));
bool scr_dyn_obj_same_source(const ScrDyn *a, const ScrDyn *b);
void *scr_dyn_obj_source_cast(const ScrDyn *d,
ScrDyn *(*source_access)(void *, bool), void *(*source_retain)(void *));
/* Object.create(null): the fresh null-prototype dictionary (see the
* null_proto flavor flag above). */
ScrDyn *scr_dyn_new_obj_null_proto(void);
ScrDyn *scr_dyn_obj_create(ScrDyn *prototype);
ScrDyn *scr_dyn_obj_create_with_properties(ScrDyn *prototype, ScrDyn *descriptors);
ScrDyn *scr_dyn_get_prototype(ScrDyn *object);
ScrDyn *scr_dyn_get_own_property_descriptors(ScrDyn *object);
ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype);
ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object);
bool scr_dyn_is_extensible(ScrDyn *object);
ScrDyn *scr_dyn_seal(ScrDyn *object);
bool scr_dyn_is_sealed(const ScrDyn *object);
ScrDyn *scr_dyn_proxy_new(const ScrDyn *target, const ScrDyn *handler);
ScrDyn *scr_dyn_proxy_get(const ScrDyn *proxy, const ScrStr *key);
bool scr_dyn_proxy_has(const ScrDyn *proxy, const ScrStr *key);
ScrDyn *scr_dyn_own_descriptor(const ScrDyn *value, const ScrStr *key);
void scr_dyn_proxy_set(ScrDyn *proxy, ScrStr *key, ScrDyn *value);
void scr_dyn_proxy_delete(ScrDyn *proxy, const ScrStr *key);
void scr_dyn_proxy_unsupported(const char *operation);
/* Retains the native view, preserving identity and its shared backing
* allocation across the checked-dynamic boundary. Borrows b. */
ScrDyn *scr_dyn_new_bytes(const ScrBytes *b);
ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key);
/* The Buffer-flavored twin (stream chunks): string coercion/toString
* decode utf8 instead of joining elements. */
ScrDyn *scr_dyn_new_buffer(const ScrBytes *b);
/* Identity-preserving transit capsule used by ReadableStream.from over
* typed arrays. The constructor retains `ptr`; matching unbox returns +1.
* materialize lazily creates and then retains one detached dyn snapshot.
* The cast cache lets a non-exact dynCheck reuse one safe, compiler-built
* target view instead of raw-casting structurally different layouts. */
ScrDyn *scr_dyn_new_typed_ref(
void *ptr, void *(*retain)(void *), void (*release)(void *),
const char *type_key, size_t type_key_len,
ScrDyn *(*materialize)(void *),
void (*commit)(void *, const ScrDyn *));
ScrDyn *scr_dyn_new_typed_ref_traced(
void *ptr, void *(*retain)(void *), void (*release)(void *),
const char *type_key, size_t type_key_len,
ScrDyn *(*materialize)(void *),
void (*commit)(void *, const ScrDyn *));
bool scr_dyn_typed_ref_is(
const ScrDyn *d, const char *type_key, size_t type_key_len);
bool scr_dyn_typed_ref_is_key(const ScrDyn *d, const ScrStr *type_key);
void *scr_dyn_typed_ref_unbox(const ScrDyn *d); /* +1 */
ScrDyn *scr_dyn_class_view_unavailable(void *ptr);
ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d); /* +1 */
void scr_dyn_typed_ref_commit(ScrDyn *d);
void *scr_dyn_typed_ref_cached_cast(
const ScrDyn *d, const char *type_key, size_t type_key_len); /* +1/NULL */
void scr_dyn_typed_ref_cache_cast(
ScrDyn *d, const char *type_key, size_t type_key_len, void *ptr,
void *(*retain)(void *), void (*release)(void *));
/* Retains a SCR_DYN_BYTES view (+1) for checked extraction. */
ScrBytes *scr_dyn_bytes_unbox(const ScrDyn *d);
/* Exact numeric typed-array brand; elem is a ScrBytesElem tag. */
bool scr_dyn_bytes_is(const ScrDyn *d, int elem);
void scr_dyn_arr_push(ScrDyn *arr, ScrDyn *item);
/* Spread completion for a runtime-arity argument list (`f(...xs)` in the
* checked-dynamic tier): flattens `src` into `arr` per JS's spread over the
* dyn's iterable kinds — arrays element-by-element (retained), strings by
* code POINT (the string iterator), bytes by byte — and throws V8's exact
* SPREAD-CALL TypeError for every other kind (pending; callers check):
* nullish sources spell the spread expression (`what`), everything else is
* the generic "Spread syntax requires ..." text. Borrows src. */
void scr_dyn_arr_push_spread(ScrDyn *arr, const ScrDyn *src, const char *what);
/* Destructuring pack over a dyn source: iterable kinds (arrays, strings by
* code point, bytes) collect into a fresh array (+1); every other kind
* throws V8's destructuring TypeError — `msg` verbatim when non-empty (the
* compile-time source spelling), else the runtime kind wording. Borrows
* both; NULL with the exception pending on the throw. */
ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg);
/* Map constructor seeds: retain array inputs for live iteration, accept
* nullish inputs as empty, and validate each entry before reading 0/1. */
ScrDyn *scr_dyn_map_seed_entries(const ScrDyn *src);
ScrDyn *scr_dyn_map_seed_entry(const ScrDyn *entry);
bool scr_weak_map_is(const ScrDyn *value);
bool scr_weak_set_is(const ScrDyn *value);
ScrDyn *scr_weak_map_new(ScrDyn *entries);
ScrDyn *scr_weak_set_new(ScrDyn *values);
ScrDyn *scr_dyn_from_entries(ScrDyn *entries);
ScrDyn *scr_dyn_mark_snapshot(ScrDyn *value); /* consumes and returns +1 */
/* Optional non-owning observers, installed by weak collections. */
extern SCR_TL void (*scr_weak_dispose_hook)(void *);
void scr_weak_dispose(void *object);
/* The for-of-over-dyn pack accessors (the emitted index loop drives them
* over a scr_dyn_iter_pack result, which is ARR by construction).
* scr_dyn_arr_len answers 0 for non-ARR kinds; scr_dyn_arr_at answers the
* undefined singleton past the end (both never throw). scr_dyn_arr_at is
* +1. */
double scr_dyn_arr_len(const ScrDyn *d);
ScrDyn *scr_dyn_arr_at(const ScrDyn *d, double i);
ScrDyn *scr_dyn_arr_named_get(const ScrDyn *d, const ScrStr *key);
void scr_dyn_obj_set(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value);
ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor);
ScrDyn *scr_dyn_get_own_property_descriptor(ScrDyn *target, ScrDyn *key);
ScrDyn *scr_dyn_array_proto_call(ScrDyn *target, ScrStr *method, ScrDyn *args);
/* The checked-dynamic keyed WRITE (`h.k = v` on a dyn receiver): OBJ sets
* the member (JS: later writes win, insertion order); undefined/null and
* non-object kinds throw Node's catchable TypeErrors (strict-mode
* wording). All three operands BORROWED (the value is retained in). */
void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value);
void scr_dyn_key_set_computed(ScrDyn *recv, ScrDyn *key, ScrDyn *value);
void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict);
ScrDyn *scr_dyn_global_symbol_get(ScrSym *key); /* borrowed key; +1 value */
void scr_dyn_global_symbol_set(ScrSym *key, ScrDyn *value); /* both borrowed */
bool scr_dyn_global_symbol_has(ScrSym *key);
void scr_dyn_global_symbol_delete(ScrSym *key);
/* `key in v` with a runtime key (OBJ own members, ARR length/valid indices,
* Proxy has traps). Traps and unsupported representations may throw. */
bool scr_dyn_has_key(const ScrDyn *v, const ScrStr *key);
/* Bare `typeof v` on a dyn value: the dyn kind's JS answer (+1 string;
* null answers "object"). Never throws. */
ScrStr *scr_dyn_typeof(const ScrDyn *d);
/* Object.prototype.toString.call over the native checked-dynamic tree; +1. */
ScrStr *scr_dyn_object_tag(const ScrDyn *d);
/* Receiver-kind-dispatched toString() (Buffer-flavored bytes decode per
* enc — utf8 default; strings/numbers/booleans/arrays/objects answer
* JS-exactly; undefined/null throw the catchable TypeError). Borrows; +1. */
ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc);
/* The method-call spelling `d.toString(enc?)`: identical, except a
* null-prototype dictionary throws "<what> is not a function" — its
* prototype chain has no toString (Node's answer). */
ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what);
ScrStr *scr_dyn_to_string_argument(const ScrDyn *d, const ScrDyn *argument, const ScrStr *what);
/* JS String() over the dyn kind (units render "null"/"undefined" where
* scr_dyn_to_string throws) — the web globals' WebIDL ToString. +1. */
ScrStr *scr_dyn_string_coerce(const ScrDyn *d);
/* JS ToString WITH the object protocol (user toString/valueOf members
* called, their throws propagating) — the WHATWG USVString conversions.
* Borrows; +1 or NULL with the exception pending. */
ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d);
bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out);
/* Direct-return ABI wrapper for compiler libCalls: JS ToNumber, or NaN
* with the exception pending when an object hook throws/refuses. */
double scr_dyn_number_coerce(const ScrDyn *d);
double scr_dyn_number_constructor(const ScrDyn *d);
ScrDyn *scr_dyn_add(const ScrDyn *left, const ScrDyn *right); /* borrowed; +1 or NULL/pending */
/* `d instanceof TypeError` (and the other builtin error classes) on a
* checked-dynamic value: the from_error cache resolves the dyn encoding
* back to its runtime error and the class's stamped preorder interval
* answers. A dyn value that never came from an error answers false. */
bool scr_dyn_err_instanceof(const ScrDyn *d, double kind);
/* Property read on a checked-dynamic object. Own fields take precedence;
* runtime Error instances inherit their exact builtin constructor token.
* The result is owned. */
ScrDyn *scr_dyn_obj_read(const ScrDyn *d, const char *key, size_t key_len);
/* JS typeof comparison, including null's "object" result. */
bool scr_dyn_is_object(const ScrDyn *d);
void scr_dyn_throw(ScrDyn *d); /* moves; preserves boxed native Error classification */
/* structuredClone over the checked-dynamic tree: JSON-safe data + bytes deep-copy;
* functions/handles throw the spec's catchable DataCloneError; cycles
* throw the scriptc fence (the checked-dynamic tree cannot represent them). Option
* validation (shared with scr_domex_clone) throws Node's exact
* TypeErrors; any non-empty transfer list throws DataCloneError. The
* _missing form is the zero-argument call: always throws Node's
* ERR_MISSING_ARGS. Args borrowed (options NULL-tolerant); results +1. */
ScrDyn *scr_structured_clone(const ScrDyn *value, const ScrDyn *options);
ScrDyn *scr_structured_clone_missing(void);
ScrDyn *scr_structured_clone_transfer_fail(void); /* always throws DataCloneError */
/* Validates a structuredClone options dyn value (throws on failure —
* callers must check scr_exc_pending). Borrowed, NULL-tolerant. */
void scr_sc_validate_options(const ScrDyn *options);
/* An %Error as the boundary's dyn shape ({name, message[, code]}).
* Borrows; +1. */
ScrDyn *scr_dyn_from_error(const ScrError *e);
/* The reverse extraction, riding the same identity cache: a dyn error
* that came from a runtime ScrError answers THAT instance (+1;
* out-and-back crossings compare reference-equal); alien %error objects
* rebuild once and enter the cache. Borrows d. */
ScrError *scr_error_from_dyn(const ScrDyn *d); /* scr_async_dyn.c (gated) */
/* The cache's runtime-internal access pair (scr_json.c owns the storage;
* the gated extraction reads/writes through these). */
ScrError *scr_errdyn_err_of(const ScrDyn *d); /* +1 or NULL */
void scr_errdyn_put(ScrError *e, ScrDyn *d); /* retains both sides */
/* ToBoolean over the dyn kind (JS-exact); borrowed, never throws. */
bool scr_dyn_truthy(const ScrDyn *d);
/* JS String() over a dyn value (arrays join, [object Object], the error
* encoding renders "name: message"); borrowed, result +1, never throws. */
ScrStr *scr_dyn_display(const ScrDyn *d);
/* JS === over two dyn values: scalars by value, units by kind, everything
* reference-shaped by node identity. Borrowed; never throws. */
bool scr_dyn_strict_eq(const ScrDyn *a, const ScrDyn *b);
/* Prototype-method dispatch on a dyn receiver (`recv.m(...)` where `m` is
* a name a dyn-representable prototype declares — push/slice/forEach/
* apply/...): implemented (kind, name) pairs run JS-exact semantics; a
* real-but-unimplemented method throws a LOUD "not supported yet" Error;
* a name the kind's prototype lacks throws Node's "<what> is not a
* function"; OBJ receivers call the own member. recv/args borrowed,
* result owned (+1). MAY THROW (NULL with the exception pending). */
ScrDyn *scr_dyn_invoke(ScrDyn *recv, const char *method, ScrDyn *const *args, size_t argc, const char *what);
/* Keyed read on a FUNC node: the own-property table first (defineProperties
* writes land there), then "name" (the box's best-effort static name; ""
* for anonymous) and "length" (the boxed arity). Returns +1, or NULL when
* the key answers nothing (the caller's undefined). Never throws. */
ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len);
/* Object.defineProperties over dyn values (targets: OBJ and FUNC): each
* descriptor's `value` becomes an own data property. OBJ targets preserve
* writable/enumerable/configurable; FUNC targets still use plain properties.
* get/set throw the loud unsupported Error.
* Returns the target (+1, JS's return), or NULL with a pending catchable
* TypeError (non-object target/descriptor — Node's messages). */
ScrDyn *scr_dyn_define_props(ScrDyn *target, ScrDyn *descs);
/* Boxes a closure as a callable dyn value. Ownership of `clo` MOVES in
* (callers retain first when they keep their own reference); `sig`/`name`
* must be static literals (the box never frees them; name may be NULL). */
ScrDyn *scr_dyn_new_func(ScrClosure *clo, ScrDynThunk thunk, uint32_t arity, const char *sig, const char *name);
ScrDyn *scr_dyn_new_class(ScrClassObj *cls, const char *type_key);
bool scr_dyn_class_is(const ScrDyn *value, const char *type_key);
/* Calls a dyn value: a non-function kind throws the catchable TypeError
* "<what> is not a function" (Node's wording — `what` is the call site's
* callee spelling) and returns NULL; a function kind delegates to the
* boxed thunk (per-arg checks live there). `args` entries are BORROWED;
* the result is owned (+1), or NULL with the exception pending. */
ScrDyn *scr_dyn_call(const ScrDyn *d, ScrDyn *const *args, size_t argc, const char *what);
/* scr_dyn_call over a dyn ARRAY's elements (the spread-application form —
* `f(...args)` after the emitted argument array is built): argv IS the
* array's items. Borrows both; result owned (+1), or NULL pending. */
ScrDyn *scr_dyn_apply(const ScrDyn *d, const ScrDyn *args, const char *what);
/* Function.bind borrows its target and arguments, returns a fresh callable. */
ScrDyn *scr_dyn_bind(ScrDyn *target, ScrDyn *const *args, size_t argc);
/* Path spine for dynCheck error messages — a compile-time-shaped linked
* list the emitted builders stack-allocate per recursion level: `key`
* non-NULL for an object member segment, else `index` is an array index.
* Rendered root-first as "$", "$.items[2].price", ... */
typedef struct ScrDynPath {
const struct ScrDynPath *parent;
const char *key;
size_t index;
} ScrDynPath;
ScrMap *scr_dyn_native_set_check(const ScrDyn *value, const ScrDynPath *path);
ScrStr *scr_url_checked_to_path(const ScrDyn *value);
ScrUrl *scr_dyn_native_url_check(const ScrDyn *value, const ScrDynPath *path);
ScrRegex *scr_dyn_native_regex_check(const ScrDyn *value, const ScrDynPath *path);
ScrClassObj *scr_dyn_class_check(const ScrDyn *value, const char *type_key, const ScrDynPath *path);
/* The dynCheck failure path: builds "TypeError: expected <want> at <path>,
* got <kind>" (got == NULL renders as "undefined" — a missing object
* member) and THROWS it through the exception cell (catchable; uncaught it
* prints as "Uncaught TypeError: ..." and exits 1). This is
* scriptc-specific behavior — JS `as` never checks (SEMANTICS.md). */
void scr_dyn_check_fail(const ScrDynPath *path, const char *want, const ScrDyn *got);
/* ── native handles in the checked-dynamic tree (SCR_DYN_HANDLE) ────────────────────────
* Per-tag dispatch ops, registered by the handle's owning unit
* (scr_http_dyn_install / scr_net_dyn_install — emitted main() calls
* them exactly when the unit is linked, the scr_net_install story), so
* this always-linked core never references gated units. */
typedef struct ScrDynHandleOps {
const char *cls; /* Node's constructor name — error texts and dynCheck's "got ..." */
void *(*retain)(void *h);
void (*release)(void *h);
/* Member CALL on a dyn handle (`res.end(...)`): dispatch (tag, method)
* onto the static lowering's entry point with per-arg checks per the
* static signature. `self` is the receiving box (`return this` methods
* answer it +1). Result owned (+1) or NULL with the exception pending.
* Real-but-unimplemented members throw the loud "not supported yet"
* ladder; names the class's surface never had throw Node's
* "<what> is not a function". */
ScrDyn *(*invoke)(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what);
/* Property READ with a static equivalent (req.url, res.statusCode):
* +1 boxed value; NULL = no such modeled property (the caller answers
* the undefined singleton — the checked-dynamic tree's own-property stance); may throw
* the loud ladder for real-but-unmodeled names (NULL + pending). */
ScrDyn *(*get)(void *h, const char *key, size_t key_len);
/* Property WRITE with a static equivalent (res.statusCode = 200):
* true = handled (value borrowed); false = not a modeled property (the
* caller throws the loud ladder — Node would take an expando, but a
* silent per-box expando would break handle identity). May throw its
* own catchable error (returns true with the exception pending). */
bool (*set)(void *h, const char *key, size_t key_len, const ScrDyn *value);
/* Cross-unit pipe DESTINATION hook (`socket.pipe(res)` — the source's
* unit cannot name the destination's entry points without breaking
* link gating): the destination accepts a source handle box, or
* answers false for a pairing it does not model (the caller's loud
* fence). NULL when the tag accepts no pipes. */
bool (*pipe_from)(void *dst, const ScrDyn *src);
/* Immutable native iterables may drain into a fresh array. Mutable or
* effectful iterators need a separate step/close protocol. NULL = not iterable. */
ScrDyn *(*iter_pack)(void *h);
} ScrDynHandleOps;
ScrDyn *scr_intl_segmenter_new(void);
ScrStr *scr_process_builtin_id(ScrDyn *id, ScrArr *known);
ScrDyn *scr_process_builtin_module(ScrStr *id, ScrDyn *getter);
ScrDyn *scr_process_builtin_unsupported(ScrStr *id, ScrStr *member);
ScrDyn *scr_global_native(ScrArr *known);
ScrDyn *scr_process_stdio(double fd);
void scr_dyn_handle_install(ScrDynHandleTag tag, const ScrDynHandleOps *ops);
void scr_file_handle_dyn_install(void);
/* The tag's class display name ("IncomingMessage") — error texts across
* units; answers "object" for an uninstalled tag (error paths only). */
const char *scr_dyn_handle_cls(const ScrDyn *d);
/* The installed ops for a HANDLE box (aborts on a missing install — an
* internal error: emitted programs install a unit's ops whenever they
* can box its handles). The dispatch unit's entry into the registry. */
const ScrDynHandleOps *scr_dyn_handle_ops_of(const ScrDyn *d);
/* Boxes a handle by reference (+1 box; retains h through the tag's ops).
* The tag MUST be installed — emitted programs install a unit's ops
* whenever they can box its handles. */
ScrDyn *scr_dyn_new_handle(void *h, ScrDynHandleTag tag);
/* dynCheck extraction (`u as IncomingMessage`, a boxed listener's typed
* parameter): tag match answers the RETAINED pointer (+1 — reference
* identity, no copy); anything else throws the path-annotated catchable
* TypeError and returns NULL. */
void *scr_dyn_handle_unbox(const ScrDyn *d, ScrDynHandleTag tag, const ScrDynPath *path, const char *want);
/* Keyed read on a HANDLE box (the emitted sc_dyn_key_get's arm): the
* tag's modeled properties answer boxed values; everything else answers
* the undefined singleton (the checked-dynamic tree's own-property stance — SEMANTICS.md)
* unless the ops fence it loudly. +1, or NULL with a pending exception. */
ScrDyn *scr_dyn_handle_key_get(const ScrDyn *d, const ScrStr *k);
/* ── promises in the checked-dynamic tree (SCR_DYN_PROMISE) ────────────────────────────
* Boxes by REFERENCE (+1 box; retains p). The boundary contract: p
* settles with a dyn payload — promise<dyn> boxes directly; other inner
* types go through scr_dyn_new_promise_adapting, which parks an emitted
* payload-converting adapter (the Promise.race cb-waiter machinery) on
* `src` and boxes the fresh destination promise instead. Rejections copy
* raw through the same machinery (reasons are dynamically tagged) and
* count as HANDLED on src — the boxed destination is the tracked one,
* like a JS .then chain. */
ScrDyn *scr_dyn_new_promise(ScrPromise *p); /* scr_async_dyn.c (gated) */
/* The allocator view the gated boxes use; installs the release arm's
* promise-release edge (runtime-internal). */
ScrDyn *scr_dyn_alloc_promise(void (*release_fn)(ScrPromise *p));
extern void (*scr_dyn_promise_release_fn)(ScrPromise *p);
ScrDyn *scr_dyn_new_promise_adapting(ScrPromise *src,
void (*adapt)(ScrPromise *dst, ScrPromise *src));
/* BORROWED peek at the boxed promise; NULL when d is not a promise box. */
ScrPromise *scr_dyn_promise_of(const ScrDyn *d);
/* ── island values in the checked-dynamic tree (SCR_DYN_JSVAL) ─────────────────────────
* Engine routing ops for JSVAL nodes, installed by the gated constructor
* (scr_dyn_from_jsval, scr_island.c — the scr_dyn_alloc_promise hook
* story: JSVAL nodes exist only after the constructor ran, so the ops
* are always installed when a dispatch arm meets the kind, and a
* dynamic-free link never references engine symbols). Contracts mirror
* the scr_jsval_* entries they route to. */
typedef struct ScrDynJsvalOps {
void (*release)(ScrJsval *cell);
ScrStr *(*type_of)(ScrJsval *cell); /* engine typeof; +1, never throws */
bool (*truthy)(ScrJsval *cell); /* engine ToBoolean; never throws */
/* String(v) in the engine (the full ToString protocol — user toString
* runs, its throw bridges): +1, or NULL with the exception pending. */
ScrStr *(*to_str)(ScrJsval *cell);
bool (*strict_eq)(ScrJsval *a, ScrJsval *b); /* engine ===; never throws */
bool (*is_array)(ScrJsval *cell); /* Array.isArray, engine-side */
bool (*is_error)(ScrJsval *cell); /* native Error instance, engine-side */
/* ── the routed operation set (lane dyn-routing-ops) ────────────────
* Each routes to the engine at the moment of use and converts at the
* boundary: dyn ARGUMENTS cross through scr_jsval_from_dyn (wrapped
* cells unwrap by reference, dyn data deep-copies, dyn FUNC boxes
* cross through the generic host-function shim), engine RESULTS come
* back through scr_dyn_from_jsval (scalar-normalizing). Fallible ops
* bridge the ENGINE's exception catchably and answer NULL/false/-1. */
ScrDyn *(*key_get)(ScrJsval *cell, const ScrStr *k); /* o[k]; +1 or NULL pending */
bool (*key_set)(ScrJsval *cell, const ScrStr *k, const ScrDyn *v); /* false = pending */
ScrDyn *(*call)(ScrJsval *cell, ScrDyn *const *args, size_t argc); /* f(...); +1 or NULL pending */
/* o.m(...) — the ENGINE's own prototypes run (JS-exact flatMap/map/
* forEach/...). A missing or non-callable member throws Node's
* "<what> is not a function" (the call site's spelling — V8's text,
* front-run before the engine's terser claim). */
ScrDyn *(*invoke)(ScrJsval *cell, const char *method, ScrDyn *const *args, size_t argc, const char *what);
bool (*is_nullish)(ScrJsval *cell); /* engine undefined/null; never throws
* (always false today — the wrap
* constructor scalar-normalizes) */
/* Object.keys/values/entries (mode 0/1/2) as a NATIVE dyn array (+1):
* keys are dyn strings, values wrap per element, entries are native
* dyn pairs. NULL with the engine's exception pending on refusal. */
ScrDyn *(*obj_walk)(ScrJsval *cell, int mode);
int (*has_own)(ScrJsval *cell, const ScrStr *k); /* 0/1; -1 = pending */
/* Object.assign(target, src) with the ENGINE target: src converts per
* member semantics (a wrapped src spreads by reference; dyn data
* enters as the usual deep copy). false = pending. */
bool (*assign)(ScrJsval *cell, const ScrDyn *src);
/* JSON.stringify text of the engine value (+1) — the engine's own
* stringify (toJSON protocols, cycle TypeErrors). NULL + pending when
* not JSON-representable. */
ScrStr *(*to_json)(ScrJsval *cell);
/* Drain the ENGINE's own iterator protocol into a fresh dyn array
* (elements wrap back scalar-normalized) — the for-of/destructuring/
* spread arm over a wrapped value. The guard's TypeError wording on a
* non-iterable: spread true takes V8's spread-call text; otherwise the
* compile-time spelling `spell` verbatim when non-NULL (the named-
* source form), else the kind wording. An iterating getter/next throw
* bridges with the ENGINE's message. +1, or NULL with the exception
* pending. */
ScrDyn *(*iter_drain)(ScrJsval *cell, bool spread, const ScrStr *spell);
} ScrDynJsvalOps;
/* The allocator view the gated constructor uses (installs the ops);
* ownership of `cell` MOVES in (the caller retains first). */
ScrDyn *scr_dyn_alloc_jsval(ScrJsval *cell, const ScrDynJsvalOps *ops);
/* The installed ops (traps on a missing install — impossible unless a
* JSVAL node was forged without the constructor). */
const ScrDynJsvalOps *scr_dyn_jsval_ops(void);
/* dynTest arms that need the ENGINE's answer on a JSVAL node, or the
* materialized answer for a live typed-reference capsule. Each answers
* false for every other kind (callers test unconditionally — the emitted
* narrowing tests stay branch-free). Never throw. */
bool scr_dyn_isl_typeof_is(const ScrDyn *d, const char *name);
bool scr_dyn_isl_is_array(const ScrDyn *d);
bool scr_dyn_isl_is_error(const ScrDyn *d);
/* The JSVAL honesty ladder: when d is a JSVAL node, THROWS the catchable
* "<what> on an island value held in 'unknown' is not supported yet"
* Error and returns false; every other kind returns false untouched.
* Callers gate un-armed operations with it — never a silent wrong
* answer (the retired fence-box bug). */
bool scr_dyn_isl_fence(const ScrDyn *d, const char *what);
/* The emitted keyed READ's JSVAL arm (sc_dyn_key_get): routes o[k] to the
* engine through the installed ops and wraps the result back (+1, scalars
* normalized) — the retired `.length -> fence` row. d MUST be a JSVAL
* node; NULL with the engine's exception bridged catchably. */
ScrDyn *scr_dyn_isl_key_get(const ScrDyn *d, const ScrStr *k);
/* The `??`/optional-chain nullish test over a dyn value: UNDEF/NULL
* native, JSVAL through the engine's own test (defensively — the wrap
* constructor scalar-normalizes engine null/undefined away), every other
* kind false. Never throws. */
bool scr_dyn_is_nullish(const ScrDyn *d);
/* scr_dyn_isl_tostr_buf (the display walkers' JSVAL arm) is declared
* with the ScrJsonBuf surface below. */
/* ── the ambient receiver (JS `this` inside listener/callback bodies) ──
* Node calls a handle's listeners with `this` bound to the emitting
* handle (server.listen(0, function() { this.address().port })), and a
* dyn OBJ method call binds the object. Compiled closures carry no
* receiver parameter; instead the CALLING site binds the receiver for
* the call's synchronous extent (push/pop nest strictly — the runtime is
* single-threaded and fires listeners synchronously) and a plain-function
* `this` read (JS entries only — TypeScript keeps the noImplicitThis
* fence) answers the innermost binding: the emitted libCall dyn.this.
* With no binding the read answers the strict-mode undefined singleton,
* exactly the old stance. The receiver survives into nested plain CALLS
* made during the window (where Node would re-bind undefined) and does
* NOT survive a resumed async body — both are documented divergences;
* the payoff is that test/common's mustCall wrapper (`return
* fn.apply(this, arguments)`) forwards the receiver for free. */
void scr_dyn_this_push(void *h, ScrDynHandleTag tag); /* h BORROWED (the firing site holds it); NULL binds undefined */
void scr_dyn_this_push_dyn(const ScrDyn *v); /* retained for the window */
void scr_dyn_this_pop(void);
/* The innermost binding as a dyn value (+1): a boxed handle, the pushed
* dyn value, or the undefined singleton (empty stack, NULL handle, or a
* handle whose tag has no installed ops — a unit that fires without its
* dyn half never binds). */
ScrDyn *scr_dyn_this_get(void);
/* The data-chunk encoding window (setEncoding — scr_json.c's note): the
* firing site opens/closes it around a 'data' pass; scr_dyn_new_chunk
* answers a Buffer-flavored bytes box, or a string inside the window. */
void scr_dyn_chunk_enc(bool utf8);
ScrDyn *scr_dyn_new_chunk(const ScrBytes *b);
/* util.format's %j over a dyn value: the JSON.stringify text (+1),
* "undefined" when the root drops (undefined/function), or NULL with a
* pending exception (a runtime handle inside the tree fences loudly). */
ScrStr *scr_dyn_format_j(const ScrDyn *d);
/* Node's ERR_INVALID_ARG_TYPE listener gate over a dyn value — shared by
* the emitter unit's checked-dynamic registrations and the handle
* dispatchers' .on(...) paths (both render errors.js's
* determineSpecificType shapes). Throws when cb is not a function kind. */
void scr_dyn_check_listener(const ScrDyn *cb, const char *argname);
/* errors.js's determineSpecificType tail ("type number (5)", "an
* instance of Object") rendered into buf when payload-carrying; the
* generic ERR_INVALID_ARG_TYPE thrower over it (`expected` is the whole
* "of type ..." clause). The handle dispatchers' per-arg gates. */
const char *scr_dyn_specific_type(const ScrDyn *v, char *buf, size_t cap);
/* ERR_INVALID_ARG_TYPE with the runtime-rendered Received tail (the
* error.argTypeThrow libCall). Borrows all three; always throws. */
void scr_throw_arg_type(const ScrStr *argname, const ScrStr *expected, const ScrDyn *got);
void scr_dyn_arg_type_fail(const char *argname, const char *expected, const ScrDyn *got);
/* The property flavor ("The \"options.x\" property must be ...") — the
* option-bag validators' gate (error.propTypeThrow). Always throws. */
void scr_throw_prop_type(const ScrStr *name, const ScrStr *expected, const ScrDyn *got);
void scr_dyn_prop_type_fail(const char *name, const char *expected, const ScrDyn *got);
/* Node's ERR_INVALID_ARG_VALUE ("The argument 'encoding' is invalid
* encoding. Received 'no'") — reason NULL renders "is invalid".
* TypeError; always throws catchably. */
void scr_dyn_arg_value_fail(const char *name, const char *reason, const ScrDyn *got);
/* The ERR_INVALID_ARG_VALUE/%j "Received" renderer (inspect-lite:
* strings quote, scalars print plain, deep shapes sketch). */
const char *scr_dyn_inspect_lite(const ScrDyn *v, char *buf, size_t cap);
/* A ladder's post-validation refuse: throws the compiler-rendered SC2020
* statement-fence text verbatim (Node's validation errors run first). */
void scr_throw_lowering_fence(const ScrStr *msg);
/* ERR_OUT_OF_RANGE's "Received" number rendering (Node's
* addNumericalSeparator underscores past 2^32) — scr_bytes.c's renderer,
* shared by the fs/net/tls option-ladder validators. */
size_t scr_num_received(double v, char out[48]);
/* Listener-closure builders for the handle dispatchers' .on(...) paths:
* a runtime-built ScrClosure whose capture is the boxed dyn listener and
* whose invoke boxes the event tuple back into the checked-dynamic tree and calls through
* the checked-dynamic machinery. The matching fn-pointer shapes are the
* per-event thunk types the registration entries take (fire0's zero-arg
* convention / ScrNetDataFn / ScrChildErrFn). cb borrowed; result +1. */
ScrClosure *scr_dyn_listener_closure0(const ScrDyn *cb);
ScrClosure *scr_dyn_listener_closure_data(const ScrDyn *cb);
ScrClosure *scr_dyn_listener_closure_err(const ScrDyn *cb);
/* The generic pair for fire thunks the OWNING units define (handle-boxing
* tuples this unit cannot spell: 'connection' sockets, 'request' pairs):
* the same one-capture closure with a caller-supplied fire, and the
* capture read those fires start from (+1). */
ScrClosure *scr_dyn_listener_closure_fn(const ScrDyn *cb, void *fire);
ScrDyn *scr_dyn_listener_fn(ScrClosure *cb);
typedef void (*ScrDynListenerDataFn)(ScrClosure *cb, ScrBytes *chunk);
typedef void (*ScrDynListenerErrFn)(ScrClosure *cb, ScrStr *msg);
extern void scr_dyn_listener_fire0(ScrClosure *cb);
extern void scr_dyn_listener_fire_data(ScrClosure *cb, ScrBytes *chunk);
extern void scr_dyn_listener_fire_err(ScrClosure *cb, ScrStr *msg);
/* Output buffer for the compiler-emitted type-directed JSON serializers.
* Value-typed and stack-allocated by the emitted code; _finish hands the
* bytes over as a +1 ScrStr and frees the buffer storage (including the
* circular-detection stack below). */
struct ScrJsonSeenEnt;
typedef struct {
char *data;
size_t len;
size_t cap;
/* Circular-structure detection for RECURSIVE record types (cyclic
* values must throw Node's exact TypeError, never recurse forever):
* the stack of container values currently being serialized, each with
* its outgoing edge label (the member being written). Only walkers over
* cycle-CAPABLE types (the collector-fixpoint set) maintain it; acyclic
* types keep the zero-cost path. */
struct ScrJsonSeenEnt *seen;
size_t seen_len;
size_t seen_cap;
} ScrJsonBuf;
void scr_jb_init(ScrJsonBuf *b);
/* Append one borrowed runtime string verbatim (no JSON quoting). */
void scr_jb_put_str(ScrJsonBuf *b, const ScrStr *s);
/* Push a container onto the circular-detection stack before serializing
* its members. If `v` is already ON the stack, throws V8's exact
* "Converting circular structure to JSON" TypeError (the --> starting at /
* |property/index hops/--- closes the circle rendering, ellipsis rules
* included) and returns false — the caller returns immediately; the
* emitted stringify site runs the pending check. `is_array` picks the
* constructor name in the message ('Array' for arrays AND tuple shapes —
* their JS values are arrays — 'Object' for records). */
bool scr_jb_enter(ScrJsonBuf *b, const void *v, bool is_array);
void scr_jb_leave(ScrJsonBuf *b);
/* Record the edge currently being serialized on the stack top: a static
* property name (emitted C literal), an overflow key (borrowed for the
* duration of the member write), or an array/tuple index. */
void scr_jb_edge_prop(ScrJsonBuf *b, const char *name);
void scr_jb_edge_key(ScrJsonBuf *b, const ScrStr *key);
void scr_jb_edge_idx(ScrJsonBuf *b, size_t i);
/* Circular guard for the compiler-emitted typed→dyn converters (sc_td_*
* over cycle-capable containers): enter TRAPS on a value already being
* converted (a cyclic value has no finite dyn copy — Node shares the
* reference instead; SEMANTICS.md), else pushes. */
void scr_dyn_from_enter(const void *v);
void scr_dyn_from_leave(void);
void scr_jb_putc(ScrJsonBuf *b, char c);
void scr_jb_puts(ScrJsonBuf *b, const char *s);
/* JS JSON.stringify number rules: NaN/±Infinity → null, -0 → 0, else
* shortest-roundtrip via scr_f64_to_str. */
void scr_jb_put_f64(ScrJsonBuf *b, double v);
/* Quoted + escaped JSON string: \" \\ \n \r \t \b \f, other control chars
* as \u00XX, everything else (UTF-8 included) verbatim — exactly the JS
* JSON.stringify escape set for well-formed strings. */
void scr_jb_put_json_str(ScrJsonBuf *b, const ScrStr *s);
/* String(v) of a JSVAL node appended into b (the emitted sc_ds display
* walkers' arm; scr_dyn_display/to_string route here too): the engine's
* ToString. A bridged failure (throwing user toString, a symbol) leaves
* the exception PENDING and appends nothing — the loud path, never a
* fabricated rendering. */
void scr_dyn_isl_tostr_buf(ScrJsonBuf *b, const ScrDyn *d);
/* JSON-serialize a dyn value into the buffer — the sc_jw_* walker for the
* dyn leaves the compiler cannot type: object members holding undefined
* DROP, array slots holding undefined print null (exactly Node). */
void scr_jb_put_dyn(ScrJsonBuf *b, const ScrDyn *d);
ScrStr *scr_jb_finish(ScrJsonBuf *b); /* returns +1; frees the buffer */
void *scr_dyn_retain_v(void *d);
void scr_dyn_release_v(void *d);
void scr_dyn_trace_v(void *d, ScrTraceVisit visit, void *ctx);
#ifdef SCR_RC_AUDIT
long scr_dyn_live_count(void);
#endif
/* ── class instances ─────────────────────────────────────────────────
* Objects are per-class C structs emitted by the compiler (shared header:
* `size_t rc` first), with per-class new/retain/release helpers also
* emitted. The runtime provides only the RC-audit hooks those helpers call.
*/
void scr_obj_alloc_note(void);
void scr_obj_free_note(void);
#ifdef SCR_RC_AUDIT
long scr_obj_live_count(void);
#endif
/* ── async: promises, fibers, event loop ────────────────────────────
* Async function bodies run on stackful fibers; `await` parks the fiber on
* a promise; a dependency-free loop (microtasks before timers, FIFO
* tiebreaks) drives everything after %main returns. See scr_async.c.
*/
ScrPromise *scr_promise_new(void);
ScrPromise *scr_promise_retain(ScrPromise *p);
void scr_promise_release(ScrPromise *p);
void *scr_promise_retain_v(void *p);
void scr_promise_release_v(void *p);
typedef struct ScrFiber ScrFiber;
/* Spawn + run eagerly to the first suspension; returns the promise, +1. */
ScrPromise *scr_async_spawn(void (*entry)(ScrFiber *, void *), void *argpack);
/* Runtime-authored C waiters cannot themselves become LLVM switched
* coroutines on wasm32. Spawn after `dependency` settles so their first
* (and only) await is extraction from an already-settled promise, while
* retaining the ordinary promise-job hop on every target. +1. */
ScrPromise *scr_async_spawn_after(ScrPromise *dependency,
void (*entry)(ScrFiber *, void *),
void *argpack);
/* wasm32-wasi lowers async functions and generators through LLVM switched
* coroutines instead of the native stack-switching implementations. The
* first two adapters are emitted into the program module; the remaining
* helpers let scr_async.c register, queue, and finish those coroutine
* frames through the same ScrFiber scheduler. These declarations are part
* of the LLVM/runtime ABI even though native targets never call them. */
void scr_wasi_coro_resume(void *handle);
void scr_wasi_coro_destroy(void *handle);
void scr_wasi_coro_started(void *handle);
void scr_wasi_await_prepare(ScrFiber *self, ScrPromise *p);
void scr_wasi_await_hop_prepare(ScrFiber *self);
bool scr_wasi_module_await_prepare(ScrFiber *self, ScrPromise *p);
void scr_wasi_async_finish(ScrFiber *self);
void scr_wasi_gen_finish(ScrFiber *self);
double scr_await_f64(ScrPromise *p); /* rejected promises re-throw */
bool scr_await_bool(ScrPromise *p);
ScrStr *scr_await_str(ScrPromise *p); /* +1 */
void *scr_await_ref(ScrPromise *p); /* +1 via the stored retain */
void scr_await_void(ScrPromise *p);
/* Internal ESM dependency wait: parks while pending but, unlike a
* JavaScript await expression, does not hop when already settled. */
void scr_module_await(ScrPromise *p);
/* After the root-aware event loop returns, inspect the executable's async
* module-root promise without another microtask hop: 0 = fulfilled,
* 1 = rejected, 13 = still pending with no ref'd work capable of settling
* it (Node's unsettled top-level-await exit status). A rejected root is
* marked observed here but re-thrown separately; earlier-checkpoint
* rejections were already decided by the loop and same-checkpoint
* competitors are suppressed by the executable-module verdict. */
int scr_promise_finish_top_level(ScrPromise *p);
void scr_promise_rethrow_top_level(ScrPromise *p);
/* The checked-dynamic tree-crossing await (SCR_DYN_PROMISE's boundary contract): the
* payload as a dyn value (+1; void fulfillments answer the undefined
* value), or NULL with the rejection re-thrown into the awaiter. */
ScrDyn *scr_await_dyn(ScrPromise *p);
/* `await v` over a checked-dynamic VALUE: dyn promises adopt, everything
* else takes JS's one-hop non-thenable await and answers itself (+1). */
ScrDyn *scr_await_dyn_value(ScrDyn *v);
/* .then/.catch/.finally over a dyn promise (scr_dyn_invoke's promise arm
* and the compiled dyn-receiver path): one reaction fiber per
* registration — awaits src, runs the checked-dynamic tree handler (non-callable handlers
* pass the settlement through; a returned dyn promise is adopted), and
* settles a fresh result promise, answered BOXED (+1). Microtask-exact
* ordering rides the fiber machinery. */
ScrDyn *scr_dyn_promise_then(ScrPromise *src, ScrDyn *onf, ScrDyn *onr, ScrDyn *onfin);
/* ── AsyncLocalStorage (node:async_hooks — scr_async.c) ───────────────
* Stores are process-lived f64 handles; the CONTEXT is an immutable
* refcounted snapshot of (store → dyn value) entries riding the fiber
* machinery: one active-slot pointer (the exc-cell pattern) swapped at
* every fiber switch, inherited by spawned fibers (Node's init-time
* capture). enter answers the PREVIOUS snapshot (owned) for restore —
* strict nesting is the callers' contract (run/runStores wrap calls). */
/* The snapshot layout (full declaration — the ScrExcCell stance: the
* fiber machinery in scr_async.c owns the active slot and the RC pair;
* the gated API TU (scr_async_dyn.c) builds and reads snapshots). */
typedef struct {
double id;
ScrDyn *value; /* owned */
} ScrAlsEntry;
typedef struct ScrAlsCtx {
size_t rc;
size_t len;
ScrAlsEntry entries[]; /* flexible */
} ScrAlsCtx;
/* Runtime-internal (the fiber machinery's always-linked core). */
extern ScrAlsCtx **scr_als_active;
ScrAlsCtx *scr_als_ctx_retain(ScrAlsCtx *c);
void scr_als_ctx_release(ScrAlsCtx *c);
double scr_als_new(void);
ScrDyn *scr_als_get(double id); /* +1; the undefined value when unset */
ScrAlsCtx *scr_als_enter(double id, ScrDyn *value); /* borrows value; prev out (owned) */
ScrAlsCtx *scr_als_enter_absent(double id); /* exit()'s cleared arm */
void scr_als_restore(ScrAlsCtx *prev); /* moves prev back in */
void scr_als_enter_with(double id, ScrDyn *value); /* no restore point (enterWith) */
void scr_als_disable(double id);
/* run/exit: enter (or clear), call the dyn function with the forwarded
* argument vector, restore (the finally). Result +1 or NULL pending. */
ScrDyn *scr_als_run(double id, ScrDyn *value, ScrDyn *fn, ScrDyn *args);
ScrDyn *scr_als_exit_run(double id, ScrDyn *fn, ScrDyn *args);
/* process.on/once('unhandledRejection', fn): dyn listeners dispatched
* per never-observed rejection at the end of its nextTick/microtask
* checkpoint — (reason, promise), suppressing the default report and the
* exit-1. `once` auto-removes after one delivery; off removes by closure
* identity (the warning registry's story). Throws Node's
* ERR_INVALID_ARG_TYPE on a non-function. */
void scr_process_on_unhandled_rejection(ScrDyn *fn, bool once);
void scr_process_off_unhandled_rejection(ScrDyn *fn);
/* process.on/once/off('rejectionHandled', fn): the sibling registry. A
* promise handled after its unhandledRejection delivery fires once, with
* the promise as Node's payload. */
void scr_process_on_rejection_handled(ScrDyn *fn, bool once);
void scr_process_off_rejection_handled(ScrDyn *fn);
/* The checkpoint delivery hook the registration above installs
* (scr_async_dyn.c → scr_report_unhandled_rejections; NULL = default
* report). Runtime-internal. */
extern bool (*scr_urj_deliver_fn)(ScrPromise *p);
/* The late-handled hook (scr_async_dyn.c installs it when a
* 'rejectionHandled' listener registers): scr_async.c calls it when a
* promise the report already delivered as unhandled gains a handler.
* Runtime-internal. */
extern void (*scr_rjh_notify_fn)(ScrPromise *p);
/* The attach-time handled mark (a dyn then/catch carrying a rejection
* handler, or the module loader taking ownership of an evaluation
* promise): marks pending and rejected sources observed at Node's attach
* moment, firing the late-handled hook when the report already delivered
* it. Runtime-internal. */
void scr_promise_mark_handled(ScrPromise *p);
/* A rejected promise's reason as a dyn value (identity-preserving for
* dyn payloads and %Error instances). +1. */
ScrDyn *scr_promise_reason_dyn(const ScrPromise *p);
/* process warnings (scr_lib.c — always linked so any unit can emit a
* deprecation): dyn listeners plus Node's default stderr report
* ("(node:pid) [CODE] Name: message" and a detail second line). Emission
* is SYNCHRONOUS at the call (Node defers a tick — SEMANTICS.md 138's
* precedent). scr_process_emit_warning takes the ARGUMENT VECTOR as one
* dyn array and applies Node's full grammar/TypeErrors; scr_emit_warning
* is the C-side deprecation entry. */
void scr_process_on_warning(ScrDyn *fn);
void scr_process_off_warning(ScrDyn *fn);
void scr_process_emit_warning(ScrDyn *args);
void scr_emit_warning(const char *name, const char *code, ScrStr *message);
/* The promise-or-absent await's unit arm: one microtask hop, like JS's
* await of any non-thenable (and like awaiting a settled promise). */
void scr_await_hop(void);
void scr_promise_fulfill_f64(ScrPromise *p, double v);
void scr_promise_fulfill_bool(ScrPromise *p, bool v);
void scr_promise_fulfill_str(ScrPromise *p, ScrStr *v); /* moves in */
/* trace: non-NULL iff the payload type carries a cycle header (a promise
* fulfilled with a cycle-capable value is itself a cycle member candidate:
* its trace visits the payload). */
void scr_promise_fulfill_ref(ScrPromise *p, void *v, void *(*retain)(void *), void (*release)(void *), ScrTraceFn trace);
void scr_promise_fulfill_void(ScrPromise *p);
/* Reject `p` with the exception pending in the active cell (moved out —
* the cell resets); wakes waiters, enters the unhandled ledger. No-op
* with a clean cell; an already-settled `p` just clears the cell. The
* island promise bridge's rejection half (scr_island.c). */
void scr_promise_reject_pending(ScrPromise *p);
/* Mint an ALREADY-SETTLED promise from a just-run synchronous operation:
* a pending exception in the active cell moves in as the REJECTION (the
* cell resets — callers' pending checks see it clean) and the payload is
* dropped; otherwise the payload fulfills. The fs/promises bridge. */
ScrPromise *scr_promise_settled_str(ScrStr *v); /* moves v in */
ScrPromise *scr_promise_settled_void(void);
ScrPromise *scr_promise_settled_ref(void *v, void *(*retain)(void *), void (*release)(void *), ScrTraceFn trace);
/* ── Promise.race (compiler-emitted combinator plumbing) ──────────────
* race_add: settle the result from an already-settled entry, or park a
* callback waiter that fires inside the entry's settle — fulfillments run
* `adapt` (an emitted per-type adapter building the result's inner type
* from the entry's payload; scr_promise_adapt_copy is the same-type
* one), rejections copy raw and count as HANDLED on the entry. The
* payload accessors return retained/by-value fulfillment payloads for
* the emitted adapters (losing entries keep their settlements). */
void scr_promise_race_add(ScrPromise *race, ScrPromise *in,
void (*adapt)(ScrPromise *dst, ScrPromise *src));
/* ── Promise.all (compiler-emitted combinator plumbing) ───────────────
* BORROWS the Promise<T>[] entries array and the pre-capacity (empty,
* cap >= ps->len) values array — NULL values/store for Promise<void>
* entries, whose result fulfills void — retains what it keeps, and
* returns the result promise +1. Node-exact: values land at their INPUT
* index regardless of settlement order, the first rejection in
* SETTLEMENT order wins (later ones count as handled on their entries),
* and the empty array fulfills immediately. The store helpers are the
* per-element-kind payload writers the compiler picks from. */
ScrPromise *scr_promise_all(ScrArr *ps, ScrArr *values,
void (*store)(ScrArr *a, double i, ScrPromise *src));
void scr_promise_all_store_f64(ScrArr *a, double i, ScrPromise *src);
void scr_promise_all_store_bool(ScrArr *a, double i, ScrPromise *src);
void scr_promise_all_store_str(ScrArr *a, double i, ScrPromise *src);
void scr_promise_all_store_ref(ScrArr *a, double i, ScrPromise *src);
void scr_promise_adapt_copy(ScrPromise *dst, ScrPromise *src);
double scr_promise_payload_f64(ScrPromise *p);
bool scr_promise_payload_bool(ScrPromise *p);
ScrStr *scr_promise_payload_str(ScrPromise *p); /* +1 */
void *scr_promise_payload_ref(ScrPromise *p); /* +1 via the stored retain */
/* Thin payload views for the gated dyn-async TU (scr_async_dyn.c). */
int scr_promise_payload_kind(const ScrPromise *p); /* ScrExcKind */
bool scr_promise_payload_is_dyn(const ScrPromise *p);
double scr_promise_payload_num(const ScrPromise *p);
bool scr_promise_payload_flag(const ScrPromise *p);
bool scr_promise_await_settled(ScrPromise *p); /* park/hop; false = rethrown */
void scr_promise_payload_release(const ScrPromise *p, void *v);
void scr_set_timeout(ScrClosure *cb, double ms); /* cb ownership moves in */
/* setInterval/clearInterval: the handle is the number the fallback
* declarations promise (ids start at 1, so `if (handle)` narrows like
* Node's Timeout truthiness). A live interval keeps the loop alive;
* clearInterval removes it eagerly (clearing from inside its own callback
* works). An unknown handle is a no-op, like Node. */
double scr_set_interval(ScrClosure *cb, double ms); /* cb ownership moves in */
/* `new Promise(setImmediate)`: a fresh promise an armed immediate
* fulfills with the undefined dyn value (+1). */
ScrPromise *scr_immediate_promise(void);
void scr_clear_interval(double handle);
/* The island timer bridge (scr_web.c): a one-shot entry WITH a clear
* handle (island clearTimeout cancels; ids share the interval space so
* scr_clear_interval serves both), and the teardown sweep that releases
* every armed closure before the engine dies. */
double scr_set_timeout_handle(ScrClosure *cb, double ms); /* cb ownership moves in */
void scr_timers_teardown(void);
/* Timeout.unref()/ref()/hasRef(): loop-liveness bookkeeping over the timer
* handle (setTimeout/setInterval's id). unref drops the timer from the
* liveness count — it still fires if the loop runs on, but never keeps the
* process alive by itself (Node's semantics); ref restores it; hasRef
* answers the current state. Absent/NaN handles are tolerated no-ops. */
void scr_timer_unref(double handle);
void scr_timer_ref(double handle);
bool scr_timer_has_ref(double handle);
/* Timeout.refresh(): re-arm to now + the original delay. Works on armed
* heap entries and from inside the firing timer's own callback (the loop
* re-arms after the callback returns); a one-shot that already fired on
* an earlier turn is gone and no-ops (documented divergence). */
void scr_timer_refresh(double handle);
/* setImmediate/clearImmediate — Node's check phase: FIFO once per loop
* turn after due timers, immediates queued mid-phase wait for the next
* turn. The handle is an f64 id in its own space (clearTimeout of an
* Immediate is a no-op, like Node); the Immediate ref trio mirrors the
* Timeout one (unref'd pending immediates don't keep the loop alive and
* never fire once nothing reffed remains). scr_timers_teardown sweeps
* the immediate queue too. */
double scr_set_immediate(ScrClosure *cb); /* cb ownership moves in */
/* queueMicrotask: the callback enters the SAME FIFO promise continuations
* ride (one microtask order, like V8's queue); a throw is an UNCAUGHT
* exception, never a rejection. cb ownership moves in. The _dyn form
* (checked-dynamic arguments) throws Node's ERR_INVALID_ARG_TYPE
* synchronously on a non-function; borrowed. */
void scr_queue_microtask(ScrClosure *cb);
void scr_queue_microtask_dyn(const ScrDyn *cb);
/* setImmediate AS A dyn VALUE (the traceCallback shape): a minted dyn
* callable that schedules args[0](args[1..]) on the immediate queue and
* answers undefined; a non-function first argument throws Node's
* ERR_INVALID_ARG_TYPE. Result +1. */
struct ScrDyn *scr_set_immediate_dyn_value(void);
void scr_clear_immediate(double handle);
void scr_immediate_unref(double handle);
void scr_immediate_ref(double handle);
bool scr_immediate_has_ref(double handle);
/* process.nextTick: enqueue a zero-param callback on the user tick
* queue — drained BEFORE promise jobs at every loop checkpoint, to
* joint exhaustion with them (Node's tick-then-microtask order).
* Pending ticks keep the loop alive; ticks left queued at termination
* ('exit' listeners, the uncaught paths) never run and release in
* scr_timers_teardown — and in scr_nticks_teardown, which the
* exit-listener runner calls for ticks enqueued AFTER the loop's own
* teardown (they must never run yet must not leak). cb ownership moves
* in. */
void scr_next_tick(ScrClosure *cb);
/* A successful process.stdout/stderr.write completion on that same queue.
* The callback and its program-shaped Error | null adapter both MOVE into
* the entry; the adapter is invoked with NULL (Node's success argument). */
void scr_process_write_callback(ScrClosure *cb, ScrFsRenameFn fn);
/* A raw C-hook entry on the SAME queue: the stream unit enqueues one
* marker per deferred stream emission, so stream ticks and user
* nextTicks run in true FIFO order (in Node they are the same queue).
* Teardown drops markers without running them. */
void scr_next_tick_raw(void (*fn)(void));
void scr_nticks_teardown(void);
/* ── the events unit (scr_events.c — OPTIONAL, link-gated) ────────────
* Process signal/exit events and the piped-stdin surface. The unit links
* ONLY into binaries whose IR uses these surfaces (moduleUsesProcessEvents
* — the scr_regex/scr_fetch/scr_zlib precedent); the emitted main calls
* scr_events_install(), which fills the loop's nullable event hooks and
* the two scr_lib.c hooks below. Event-free builds pay zero bytes.
*
* Signal events (process.on/once/off of "SIGINT"/"SIGTERM"): sigaction +
* self-pipe, dispatched at loop turns as macrotasks. Watching replaces the
* default disposition; removing the last listener restores it. Signal
* listeners do NOT keep the loop alive (Node). `on` MOVES the callback in;
* `off` borrows and removes the first pointer-identical entry.
*
* The process 'exit' event: listeners run synchronously at normal
* termination (the unit's atexit) and inside process.exit() (through
* scr_process_exit_hook); the adapter receives the exit code (runtime
* thunks below for the 0-param and (code) shapes).
*
* process.stdin events (piped-stdin slice) and the for-await chunk
* source. Listener callbacks MOVE in; data adapters get the chunk
* BORROWED (the bytes thunk retains for the callee). next_chunk returns a
* +1 promise of the next chunk — the EMPTY bytes value is the done
* sentinel (a POSIX read never delivers an empty data chunk). While a
* consumer exists (data listener or parked chunk promise) stdin keeps the
* loop alive, like Node's flowing stdin. */
typedef struct ScrBytes ScrBytes; /* full definition below (C11 repeat) */
void scr_events_install(void);
void scr_signal_on(ScrStr *name, ScrDyn *cb, bool once);
void scr_signal_off(ScrStr *name, ScrDyn *cb);
void scr_process_on_exit(ScrClosure *cb, void (*fn)(ScrClosure *, double), bool once);
void scr_process_off_exit(ScrClosure *cb);
void scr_run_exit_listeners(double code);
void scr_exit_thunk0(ScrClosure *cb, double code);
void scr_exit_thunk_code(ScrClosure *cb, double code);
void scr_stdin_on_data(ScrClosure *cb, void (*fn)(ScrClosure *, ScrBytes *), bool once);
void scr_stdin_on_end(ScrClosure *cb, bool once);
void scr_stdin_on_error(ScrClosure *cb, ScrChildErrFn fn, bool once);
ScrPromise *scr_stdin_next_chunk(void); /* +1 */
void scr_stdin_destroy(void);
bool scr_stdin_pending(void);
/* node:readline's hooks into the stdin unit: removal by closure identity
* (close() detaches the shared consumer) and the ended probe (EOF seen or
* destroyed — a later createInterface is born dead). */
void scr_stdin_remove_data(ScrClosure *cb);
bool scr_stdin_ended(void);
/* ── node:readline (scr_readline.c, linked under the events gate) ──────
* The question/close slice, Node's semantics pinned under pipes (the file
* header has the full model): create answers the f64 interface handle;
* question writes the query to stdout and delivers the next line's text
* through the adapter (fn(cb, +1 answer)) — throwing Node's
* "readline was closed" on a closed interface (may-throw); close fires
* the 'close' listeners SYNCHRONOUSLY (Node's inline emit) and detaches
* the stdin consumer; onClose registers a zero-arg listener (moves).
* Answer adapters: thunk0 ignores the line, thunk_str passes it. */
double scr_rl_create(void);
void scr_rl_question(double id, const ScrStr *query, ScrClosure *cb /*moves*/,
void (*fn)(ScrClosure *, ScrStr *));
void scr_rl_close(double id);
void scr_rl_on_close(double id, ScrClosure *cb /*moves*/);
void scr_rl_answer_thunk0(ScrClosure *cb, ScrStr *answer);
void scr_rl_answer_thunk_str(ScrClosure *cb, ScrStr *answer);
void scr_stdin_data_thunk0(ScrClosure *cb, ScrBytes *chunk);
void scr_stdin_data_thunk_bytes(ScrClosure *cb, ScrBytes *chunk);
/* The loop-side registration (scr_async.c, always linked) and the
* scr_lib.c hooks the unit fills. The abnormal-exit code hint lives in
* scr_async.c so the uncaught/unhandled reporters can note it whether or
* not the unit is present. */
void scr_loop_set_events(bool (*pending)(void), bool (*watching)(void),
void (*dispatch)(void), int (*pollfds)(int out[2]));
extern void (*scr_process_exit_hook)(double code);
extern void (*scr_stdin_destroy_hook)(void);
void scr_exit_code_note(int code);
int scr_exit_code_hint_get(void);
/* The pollable child-exit wake fd (the child kqueue / pidfd epoll) for the loop's
* poll(2) sleep, or -1 when it can't wake for every pending child. */
int scr_children_wake_fd(void);
double scr_now_ms(void); /* the loop's monotonic clock, in ms */
/* Run to ordinary loop exhaustion, except that a non-NULL executable
* module-root promise stops the loop as soon as it is rejected at a
* microtask checkpoint. Fulfilled roots do not stop the loop: Node keeps
* running ref'd work scheduled by a successfully evaluated module.
* Returns true when a default/listener-crashing unhandled rejection
* already selected and reported exit status 1. */
bool scr_loop_run(ScrPromise *top_level);
/* External I/O hook, polled at loop quiescence like the child registry:
* `pending` keeps the loop alive; `poll` makes progress and may SLEEP up
* to max_wait_ms (on real fds — socket readiness wakes it early), so the
* loop skips its own nanosleep for that turn. One registrant: the dynamic
* island (engine promise jobs + in-flight fetch transfers). Static builds
* never set it. */
void scr_loop_set_io(bool (*pending)(void), void (*poll)(double max_wait_ms));
bool scr_report_unhandled_rejections(void); /* true = exit 1 */
void scr_discard_unhandled_rejections(void);
/* The island's unhandled-rejection ledger joins the report above (one
* registrant, set at engine boot; static builds never set it): called with
* print=true when the static ledger reported nothing, prints its FIRST
* never-observed rejection in the same voice, frees the ledger either way,
* and returns whether it had any. */
void scr_loop_set_island_rejections(bool (*fn)(bool print),
int (*drain_jobs)(void));
/* The island's earliest armed timer deadline (scr_island_timers_deadline;
* HUGE_VAL when none) joins the loop's sleep computation: an armed
* AbortSignal.timeout must fire on time while the loop sleeps on socket
* readiness, without keeping the loop alive by itself (Node's unref'd
* timer). One registrant, set at engine boot; static builds never set it. */
void scr_loop_set_island_deadline(double (*fn)(void));
long scr_abandoned_fiber_count(void);
/* True while executing on an async fiber (vs the main stack) — the island
* sizes its engine stack budget per stack (see scr_island.c). */
bool scr_on_fiber(void);
/* Identity of the running fiber (NULL on the main stack) — the island's
* stack-anchor bookkeeping tells STACKS apart, not just fiber-vs-main
* (a host callback may spawn a fiber that re-enters the engine). */
void *scr_fiber_self(void);
void scr_note_abandoned_fibers(long n); /* scr_console.c owns the flag */
/* new Promise(executor): kind 0 f64, 1 bool, 2 str, 3 void; ref-kind
* resolve thunks are emitted (they know the concrete RC helpers) over
* scr_resolve_ref_impl, constructed via scr_make_resolve_fn. The
* executor runs synchronously; an escaping throw rejects the promise. */
ScrClosure *scr_make_resolve(ScrPromise *p, int kind);
ScrClosure *scr_make_resolve_fn(ScrPromise *p, void *fn);
void scr_resolve_ref_impl(ScrClosure *self, void *v, void *(*retain)(void *), void (*release)(void *), ScrTraceFn trace);
/* The reject closure: `(reason: Error) => void` — stores the reason as a
* SCR_EXC_OBJ payload (the thrown-Error representation) and wakes waiters;
* a reject after any settle is a no-op (first settle wins, exactly JS). */
ScrClosure *scr_make_reject(ScrPromise *p);
void scr_promise_run_executor(ScrPromise *p, ScrClosure *exec, ScrClosure *resolve);
void scr_promise_run_executor0(ScrPromise *p, ScrClosure *exec);
void scr_promise_run_executor2(ScrPromise *p, ScrClosure *exec, ScrClosure *resolve, ScrClosure *reject);
/* The promise a fiber settles (borrowed) — the emitted trampolines fulfill
* through it. */
ScrPromise *scr_fiber_promise(ScrFiber *f);
/* ── sync generators (function*) ──────────────────────────────────────
* A generator is a refcounted handle (ScrGen) over a fiber created
* SUSPENDED — nothing runs until the first resume — plus three
* type-erased value slots (kind + payload + release fn, the exception
* cell's technique): OUT (the yielded value, or the completion value once
* done), IN (the `.next(v)` argument), and RET (a parked `.return(v)`
* value, promoted to OUT by the trampoline when the GENRET unwind
* completes). The emitted code is type-directed: setters/takes move
* ownership (+1 in, +1 out); a NONE slot is JS's undefined.
*
* Resume protocol (consumer side, synchronous — no event loop):
* - scr_gen_resume: `.next()`. Starts or resumes the fiber and returns
* when it yields or completes. A body exception (or an injected
* `.throw`) moves into the CALLER's cell — the emitted pending check
* after the resume propagates it. Resuming a RUNNING generator throws
* Node's TypeError; resuming a DONE one is a no-op (OUT stays NONE —
* `{ value: undefined, done: true }`).
* - scr_gen_resume_return: `.return(v)` with v already parked in RET.
* UNSTARTED/DONE: the body never runs (the unstarted fiber tears down,
* its argpack dropped); RET promotes to OUT. SUSPENDED: the GENRET
* sentinel enters the fiber's cell and the fiber resumes — finallys
* run; a finally that YIELDS parks the sentinel (done stays false; a
* later resume continues the unwind); a finally that THROWS replaces it.
* - scr_gen_resume_throw: `.throw(e)` with e pending in the CALLER's cell
* (the emitted scr_throw_* just ran). SUSPENDED: the payload moves into
* the fiber's cell and the fiber resumes — the body's own try/catch
* sees it. UNSTARTED/DONE: the generator becomes done and the payload
* stays pending in the caller (the `.throw` call itself throws).
*
* Abandonment: releasing a SUSPENDED generator leaks its fiber
* deliberately (unwinding would run user finallys Node's GC never runs);
* the fiber stays in the live count, so programs that abandon one get the
* abandoned-fiber RC-audit note (the loop-exhaustion story). An UNSTARTED
* generator tears down cleanly: drop_args releases the packed arguments.
*/
typedef struct ScrGen ScrGen;
ScrGen *scr_gen_new(void (*entry)(ScrFiber *, void *), void *argpack,
void (*drop_args)(void *));
ScrGen *scr_async_gen_new(void (*entry)(ScrFiber *, void *), void *argpack,
void (*drop_args)(void *),
void (*settle)(ScrGen *, ScrPromise *));
ScrGen *scr_gen_retain(ScrGen *g);
void scr_gen_release(ScrGen *g); /* NULL-tolerant */
void *scr_gen_retain_v(void *g);
void scr_gen_release_v(void *g);
void scr_gen_resume(ScrGen *g);
void scr_gen_resume_return(ScrGen *g);
void scr_gen_resume_throw(ScrGen *g);
bool scr_gen_done(ScrGen *g);
/* Async-generator resume methods. Reference arguments move into the
* queued request; every function returns a +1 Promise<IteratorResult>. */
ScrPromise *scr_async_gen_next_none(ScrGen *g);
ScrPromise *scr_async_gen_next_f64(ScrGen *g, double value);
ScrPromise *scr_async_gen_next_bool(ScrGen *g, bool value);
ScrPromise *scr_async_gen_next_ref(ScrGen *g, void *value,
void (*release)(void *));
ScrPromise *scr_async_gen_return_none(ScrGen *g);
ScrPromise *scr_async_gen_return_f64(ScrGen *g, double value);
ScrPromise *scr_async_gen_return_bool(ScrGen *g, bool value);
ScrPromise *scr_async_gen_return_ref(ScrGen *g, void *value,
void (*release)(void *));
ScrPromise *scr_async_gen_throw(ScrGen *g);
/* IN slot (consumer stores before resume; body takes after the yield). */
void scr_gen_in_f64(ScrGen *g, double v);
void scr_gen_in_bool(ScrGen *g, bool v);
void scr_gen_in_ref(ScrGen *g, void *v, void (*release)(void *)); /* moves */
void scr_gen_in_none(ScrGen *g);
double scr_gen_take_in_f64(void); /* body side: the running fiber's gen */
bool scr_gen_take_in_bool(void);
void *scr_gen_take_in_ref(void); /* +1 moved out; NONE → NULL */
/* RET slot (consumer parks the `.return(v)` value before resume_return). */
void scr_gen_ret_f64(ScrGen *g, double v);
void scr_gen_ret_bool(ScrGen *g, bool v);
void scr_gen_ret_ref(ScrGen *g, void *v, void (*release)(void *)); /* moves */
void scr_gen_ret_none(ScrGen *g);
/* OUT slot (body yields / the emitted trampoline completes; consumer
* takes after resume). The no-gen-argument yield forms run on the CURRENT
* fiber's generator and switch back to the resumer; control returns when
* the consumer resumes again (the emitted pending check right after sees
* an injected .throw payload or the GENRET sentinel). */
void scr_gen_yield_f64(double v);
void scr_gen_yield_bool(bool v);
void scr_gen_yield_ref(void *v, void (*release)(void *)); /* moves */
void scr_async_gen_hop_done(void);
void scr_gen_out_f64(ScrGen *g, double v); /* trampoline completion stores */
void scr_gen_out_bool(ScrGen *g, bool v);
void scr_gen_out_ref(ScrGen *g, void *v, void (*release)(void *)); /* moves */
bool scr_gen_out_has(ScrGen *g); /* false = undefined completion value */
double scr_gen_take_out_f64(ScrGen *g);
bool scr_gen_take_out_bool(ScrGen *g);
void *scr_gen_take_out_ref(ScrGen *g); /* +1 moved out; NONE → NULL */
/* The trampoline's GENRET epilogue: true iff the sentinel is pending in
* the ACTIVE cell (also the emitted catch-prologue test in generator
* bodies). ret_to_out promotes the parked return value to the completion
* value. gen_of answers the generator a fiber belongs to (borrowed). */
bool scr_exc_genret_pending(void);
void scr_gen_ret_to_out(ScrGen *g);
ScrGen *scr_gen_of_fiber(ScrFiber *f);
#ifdef SCR_RC_AUDIT
long scr_promise_live_count(void);
#endif
/* ── fetch (scr_fetch.c) ──────────────────────────────────────────────
* The native bridge over scr_net + scr_tls + scr_http's client parser.
* Static fetch(url) resolves at the response head with native Response
* and ReadableStream handles; Response.json consumes that same stream.
* AbortSignal and streaming request bodies stay native too. Under
* --dynamic the same TU boots the broader engine web surface. The
* emitted main calls scr_fetch_install in either form. */
void scr_fetch_install(void);
ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init); /* +1 promise<Response handle> */
ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init); /* borrowed args; +1 Response handle or NULL pending */
ScrPromise *scr_fetch_response_json(ScrDyn *response); /* +1 promise<dyn> */
ScrPromise *scr_fetch_response_text(ScrDyn *response); /* +1 promise<dyn> */
ScrPromise *scr_fetch_response_array_buffer(ScrDyn *response); /* +1 promise<dyn> */
ScrPromise *scr_fetch_response_bytes(ScrDyn *response); /* +1 promise<dyn> */
ScrDyn *scr_fetch_abort_controller_new(void); /* +1 AbortController handle */
/* Borrowed number; +1 AbortSignal handle or NULL pending. */
ScrDyn *scr_fetch_abort_timeout(ScrDyn *delay);
ScrDyn *scr_fetch_abort_now(ScrDyn *reason); /* borrowed reason; +1 handle */
ScrDyn *scr_fetch_abort_any(ScrDyn *signals); /* borrowed array; +1 handle or NULL pending */
ScrDyn *scr_fetch_stream_new(ScrDyn *source); /* borrowed source; +1 handle or NULL pending */
ScrDyn *scr_fetch_stream_from(ScrDyn *iterable); /* borrowed iterable; +1 handle or NULL pending */
ScrDyn *scr_fetch_stream_from_array(
ScrArr *iterable,
ScrDyn *(*item)(ScrArr *, double)); /* borrowed array/callback; +1 handle */
ScrDyn *scr_fetch_stream_from_bytes(
ScrBytes *iterable); /* borrowed bytes; +1 handle */
ScrDyn *scr_fetch_stream_from_string(
ScrStr *iterable); /* borrowed string; +1 handle */
ScrPromise *scr_fetch_reader_read(ScrDyn *reader); /* borrowed handle; +1 */
/* ── dynamic island (scr_island.c; --dynamic builds ONLY) ────────────
* The embedded QuickJS-ng engine. Compiled and linked only under
* -DSCR_DYNAMIC; static builds never reference these symbols (nor the
* engine headers — this block deliberately names no engine type). One
* runtime+context per process, created lazily on first entry, torn down
* at exit before the RC audit; every entry re-anchors the engine's
* stack-overflow check so calls from ucontext fibers are safe. Not
* reentrant from engine callbacks.
*/
#ifdef SCR_DYNAMIC
/* Evaluate UTF-8 source in the island's global scope; returns
* String(result) as a +1 ScrStr. Borrows code. An island exception is
* bridged into the exception cell as a catchable string ("TypeError:
* boom" — String(e)) and NULL comes back — callers are compiler-emitted
* pending checks (may-throw, like the fs.* surface). */
ScrStr *scr_island_eval(ScrStr *code);
/* The libregexp opaque for scr_regex.c (--dynamic builds share the engine
* archive's libregexp, whose host hooks want the island's JSContext):
* lazily boots the engine and re-anchors its stack check, then hands the
* context out as an opaque pointer. */
void *scr_island_lre_opaque(void);
/* ── embedded npm modules ─────────────────────────────────────────────
* npm package code is embedded into the binary at BUILD time: the emitted
* C carries every reached module's source as a static string plus the
* (importer, specifier) → target resolution edges, and registers both
* tables at the top of main. The island's module loader and its CommonJS
* require shim resolve exclusively from these tables — binaries never
* read node_modules at runtime. Edge targets are module keys or "node:*"
* builtins, which the island shims (events, path, process, fs stubs,
* child_process stubs). */
typedef struct ScrIslandModule {
const char *key; /* resolved path — the loader's cache key */
/* UTF-8 source — or raw-DEFLATE bytes when src_raw > 0 (the emitter
* compresses big module texts; the loader inflates LAZILY at first
* load through the inflater the emitted main installs, so a module a
* run never loads never inflates — and its pages never fault in). */
const char *src;
size_t len; /* byte length of src AS STORED */
size_t src_raw; /* 0: src is plain text; else the inflated byte length */
int format; /* 0 = ESM, 1 = CommonJS, 2 = JSON */
/* CommonJS only: the ESM facade the loader evaluates when an ES module
* imports this file — default (module.exports itself) plus the named
* exports LEXED from the source at BUILD time with the compiler's port
* of Node's vendored CJS lexer. NULL for ESM sources (compiled
* natively) and JSON (default-only wrapper, like Node). The same
* compression contract as src, via esm_raw. */
const char *esm;
size_t esm_len;
size_t esm_raw;
} ScrIslandModule;
typedef struct ScrIslandEdge {
const char *from;
const char *spec;
const char *to;
/* Which call forms this resolution serves — Node picks the "exports"
* condition set from the CALL FORM, so a dual package embeds TWO edges
* for one (from, spec): its "import" condition's target behind kind 1
* and its "require" condition's behind kind 2. 0 answers both lookups
* (relative files, builtins). */
int kind; /* 0 = any, 1 = import, 2 = require */
} ScrIslandEdge;
/* Registers the emitted tables (static data; no copy, no engine boot).
* Must run before any island entry that imports — main calls it right
* after scr_lib_init. */
void scr_island_modules(const ScrIslandModule *mods, size_t nmods,
const ScrIslandEdge *edges, size_t nedges);
/* The inflater for compressed embedded module text (raw DEFLATE, exact
* inflated size known from the table row). The emitted main installs
* scr_zlib.c's scr_zlib_inflate_exact exactly when some module compressed
* at build time — index.ts links scr_zlib.c/libz on the same predicate,
* so compression-free builds keep their link line and the island keeps no
* zlib reference of its own. */
void scr_island_set_inflate(bool (*inflate)(const unsigned char *src, size_t src_len,
unsigned char *dst, size_t dst_len));
/* scr_zlib.c: one-shot raw-DEFLATE inflate into a caller-sized buffer;
* true only when the stream ends exactly at dst_len. */
bool scr_zlib_inflate_exact(const unsigned char *src, size_t src_len,
unsigned char *dst, size_t dst_len);
/* The island process shim's implicit exit status (process.exitCode):
* Node's set-it-and-return-normally contract. The emitted main returns
* this after the loop drains; 0 when never set. The version advances on
* every assignment so an exit listener can override a provisional status. */
int scr_island_exit_code(void);
size_t scr_island_exit_code_version(void);
/* ── web-platform globals (scr_web.c) ─────────────────────────────────
* The pure-JS prelude defining the island's WHATWG subset (streams et
* al.), evaluated at engine boot before any embedded module. `jsctx` is
* the island's JSContext as an opaque pointer — engine types stay out of
* this header. */
void scr_island_web_boot(void *jsctx);
/* Runs the engine's pending promise-reaction jobs to exhaustion (no-op
* before the engine boots). Returns the number executed. The loop's io
* hook drains through this so island async chains progress at quiescence,
* exactly where Node runs its microtasks. */
int scr_island_drain_jobs(void);
/* Re-anchors the engine's stack-overflow check to the CURRENT stack (the
* every-island-entry rule) — for units entering the engine from loop
* dispatch stations (the fetch bridge's net callbacks) rather than
* through an emitted island op. */
void scr_island_host_enter(void);
/* Bridges the engine's CURRENT pending exception into the scriptc
* pending cell (the island timer bridge fires engine callbacks from the
* static heap; a throw there must become the loop's uncaught report). */
void scr_island_bridge_exception(void);
/* ── island timers (scr_web.c) ────────────────────────────────────────
* The machinery behind AbortSignal.timeout: one-shot engine callbacks
* armed by the web prelude's host.timer. Deliberately UNREF'd, like
* Node's AbortSignal.timeout timer — an armed timer never keeps the loop
* alive; it fires only if other work (a live transfer, engine jobs) keeps
* the process running, and the island's io hook caps its sleeps at the
* earliest deadline so a fetch timeout fires on time. Deadlines are on
* the scr_now_ms clock; _deadline returns HUGE_VAL when none are armed.
* Teardown frees unfired callbacks so the engine audit stays zero. */
double scr_island_timers_deadline(void);
bool scr_island_timers_due(void);
bool scr_island_timers_fire_due(void); /* true = fired at least one */
void scr_island_timers_teardown(void);
/* ── dynamic fetch hooks ──────────────────────────────────────────────
* The native bridge registers NO pending/poll hooks (its
* transfers live on real sockets the loop's poller sleeps on); the curl
* reference implementation (scr_fetch_curl.c, SCRIPTC_FETCH_CURL=1 —
* one release as the flip's reference) still registers all four so the
* loop can sleep on curl's fds. */
void scr_island_set_fetch(void (*boot)(void *jsctx), bool (*pending)(void),
void (*poll)(double max_wait_ms), void (*teardown)(void));
/* ── the island's node:http/https client bridge (scr_net_island.c) ────
* The one TU referencing BOTH the socket units and the island (the
* scr_zlib_island.c precedent): native-toolchain.ts compiles it exactly when a
* --dynamic build links the socket units, and the emitted main calls
* scr_net_island_install before any island entry. `attach` runs while
* the island builds its bootstrap host object (jsctx and host_obj are
* the engine's context and host-object pointers, opaque here — engine
* types stay out of this header) and adds the bridge's host functions; `teardown`
* frees engine values still held by in-flight requests before the
* engine dies, the fetch-teardown story. Without the bridge the island
* keeps its "does not provide the 'node:http' builtin" refusal. */
void scr_island_set_netmod(void (*attach)(void *jsctx, void *host_obj), void (*teardown)(void));
void scr_net_island_install(void);
/* ── island value handles (the `any` runtime type) ────────────────────
* A ScrJsval is an ordinary refcounted scriptc heap cell owning ONE
* engine value. It is opaque here (the engine's value type must not leak
* into static-build translation units) and deliberately NOT
* cycle-collector-traced: its internal references live in the engine's
* own GC world, and cycles that cross the boundary are documented as
* uncollectable (SEMANTICS.md). Releasing the last reference frees the
* engine value — or only the cell itself after engine teardown (the
* engine frees every value it still owns when it goes down, so a
* post-teardown release must not touch it).
*
* Conventions, mirroring the rest of the runtime: operands are BORROWED,
* refcounted results come back owned (+1). Fallible operations bridge
* the engine exception into the exception cell (catchable) and report
* failure via NULL (pointer results) or -1 (int results) — callers are
* compiler-emitted pending checks. Every entry re-anchors the engine's
* stack check, so all of these are fiber-safe. */
typedef struct ScrJsval ScrJsval;
ScrJsval *scr_jsval_retain(ScrJsval *v);
void scr_jsval_release(ScrJsval *v);
void *scr_jsval_retain_v(void *v);
void scr_jsval_release_v(void *v);
/* Binary operator codes for scr_jsval_binop / scr_jsval_cmp. The JS
* semantics (ToPrimitive, coercion) come from prelude helper closures
* evaluated at island init, not from C reimplementations. */
#define SCR_JSOP_ADD 0
#define SCR_JSOP_SUB 1
#define SCR_JSOP_MUL 2
#define SCR_JSOP_DIV 3
#define SCR_JSOP_MOD 4
#define SCR_JSOP_POW 5
#define SCR_JSOP_LT 6
#define SCR_JSOP_LE 7
#define SCR_JSOP_GT 8
#define SCR_JSOP_GE 9
#define SCR_JSOP_EQ 10 /* strict === */
#define SCR_JSOP_NEQ 11 /* strict !== */
#define SCR_JSOP_COUNT 12
/* Marshal in (static → island). Each returns a fresh +1 cell. from_str
* borrows s; from_json borrows JSON text produced by the emitted
* type-directed serializers (a DEEP COPY into the island — the documented
* aliasing divergence) and cannot fail on that trusted input. */
ScrJsval *scr_jsval_from_f64(double v);
ScrJsval *scr_jsval_from_bool(bool v);
ScrJsval *scr_jsval_from_str(const ScrStr *s);
ScrJsval *scr_jsval_from_json(const ScrStr *json);
/* A CHECKED-DYNAMIC (dyn) value entering the island — deep copy for data
* kinds (a boxed handle/promise throws the catchable TypeError). A JSVAL
* node unwraps to its OWN cell (+1) — an engine value that crossed into
* the checked-dynamic tree and back is the SAME engine value, by reference (nested JSVAL
* members embed their engine values directly). A boxed FUNCTION crosses
* as one generic host-function shim over its uniform ScrDynThunk: the
* shim wraps engine arguments as dyn values (scalar-normalizing), calls
* the thunk, and converts the dyn result back — each crossing mints a
* fresh engine function (identity is not preserved for re-crossings;
* SEMANTICS.md). NULL with a pending exception on failure. Borrows d. */
ScrJsval *scr_jsval_from_dyn(const ScrDyn *d);
/* The jsval→dyn crossing (the IR's dynFromJsval): wraps an island value
* as a SCR_DYN_JSVAL node, SCALAR-NORMALIZING first — engine numbers/
* strings/booleans/null/undefined convert to the native dyn kinds (the
* strict exits cannot fail on engine-reported scalars), so JSVAL nodes
* only ever hold engine objects/arrays/functions (and the symbol/bigint
* edge). Installs the checked-dynamic tree's engine-routing ops on first use. Borrows
* the cell (retains it into the node); +1 out; never throws. */
ScrDyn *scr_dyn_from_jsval(ScrJsval *cell);
/* Engine operations. Arithmetic yields a fresh cell (NULL = bridged);
* comparisons yield 0/1 (-1 = bridged); truthy/not never fail. */
ScrJsval *scr_jsval_binop(int op, ScrJsval *a, ScrJsval *b);
int scr_jsval_cmp(int op, ScrJsval *a, ScrJsval *b);
/* `v instanceof C` in the engine (JS_IsInstanceOf — the spec operator,
* Symbol.hasInstance included). 0/1; -1 = bridged (non-object RHS throws
* the engine's own TypeError, catchably). */
int scr_jsval_instance_of(ScrJsval *v, ScrJsval *c);
ScrJsval *scr_jsval_neg(ScrJsval *a);
/* GetIterator over an island value (the for-of head over 'any'): the
* engine's protocol lookup; NULL + pending TypeError when not iterable. */
ScrJsval *scr_jsval_iter_new(ScrJsval *a);
ScrJsval *scr_jsval_plus(ScrJsval *a); /* unary + (ToNumber) */
int scr_jsval_truthy(ScrJsval *a);
ScrStr *scr_jsval_typeof(ScrJsval *a);
bool scr_jsval_is_object(ScrJsval *a);
ScrStr *scr_jsval_to_str(ScrJsval *a); /* String(v); NULL = bridged */
/* Property/element access and calls. Names are NUL-terminated ScrStr
* (identifier property names from source); computed keys are jsvals. */
ScrJsval *scr_jsval_get_prop(ScrJsval *o, const ScrStr *name);
ScrJsval *scr_jsval_global_get(const ScrStr *name);
int scr_jsval_set_prop(ScrJsval *o, const ScrStr *name, ScrJsval *v);
ScrJsval *scr_jsval_get_idx(ScrJsval *o, ScrJsval *key);
/* Destructuring guards over island values: RequireObjectCoercible with
* V8's exact TypeError text (pass-through result, +1), and GetIterator +
* the pattern's width as a fresh engine array (undefined-padded,
* IteratorClose per spec); both throw catchably, NULL + pending on throw. */
ScrJsval *scr_jsval_destr_check(ScrJsval *v, const char *spell, const char *first);
ScrJsval *scr_jsval_iter_n(ScrJsval *v, double n);
int scr_jsval_set_idx(ScrJsval *o, ScrJsval *key, ScrJsval *v);
ScrJsval *scr_jsval_call_method(ScrJsval *o, const ScrStr *name, int argc, ScrJsval **argv);
ScrJsval *scr_jsval_call_this(ScrJsval *f, ScrJsval *receiver, int argc, ScrJsval **argv);
/* `o.name?.(...)`: a nullish member answers the engine's undefined;
* anything else calls with this = o (non-callables throw in the engine). */
ScrJsval *scr_jsval_opt_call_method(ScrJsval *o, const ScrStr *name, int argc, ScrJsval **argv);
ScrJsval *scr_jsval_call(ScrJsval *f, int argc, ScrJsval **argv);
/* Spread application on an island callee — `f(...pre, ...spread)` through
* the prelude helper's REAL spread syntax (iterator protocols are the
* engine's own; the guards front-run V8's exact spread-call TypeError
* texts). `pre` is the engine array of leading fixed arguments; `what` the
* spread expression's source spelling (the nullish text spells it).
* Borrows everything; +1 out, or NULL with the exception bridged. */
ScrJsval *scr_jsval_call_spread(ScrJsval *f, ScrJsval *pre, ScrJsval *spread, const ScrStr *what);
/* `new X(...)` on an island callee (jsOp construct) — JS_CallConstructor.
* Borrows everything; +1 out, or NULL with the exception bridged. */
ScrJsval *scr_jsval_construct(ScrJsval *f, int argc, ScrJsval **argv);
/* A scriptc closure entering the island as an engine function (the
* package-callback pattern: `.action((a, b) => ...)`). `adapt` is the
* compiler-emitted per-signature adapter — one uniform shape over the
* closure ABI: borrowed argument cells in (the closure ABI consumes its
* params, so the adapter retains them), the +1 result cell (or NULL for
* void) out. The wrapper retains the closure; the engine finalizer
* releases it (before the RC audit — teardown precedes it). Calls from
* the engine pad missing arguments with undefined and drop surplus, like
* any JS call; a scriptc exception thrown inside reverse-bridges to an
* engine throw (strings stay strings — the two bridges round-trip). */
ScrJsval *scr_jsval_from_closure(ScrClosure *c, int arity,
ScrJsval *(*adapt)(ScrClosure *, ScrJsval **));
/* True iff the cell holds the engine's undefined — the TYPED host-call
* adapters ask before converting a `T | undefined` parameter (an absent
* or undefined engine argument takes the undefined arm; anything else
* converts through the exit machinery). */
bool scr_jsval_is_undefined(ScrJsval *v);
/* Optional chains on island values: the nullish test on the HANDLE
* (`x?.y` on 'any' — undefined OR null short-circuits, JS-exact) and the
* engine's own undefined/null as +1 cells (the unit path's result, and
* the union lift's unit arms). All infallible. */
bool scr_jsval_is_nullish(ScrJsval *v);
ScrJsval *scr_jsval_undefined(void);
ScrJsval *scr_jsval_null(void);
/* Non-JSON marshals IN: a URL crosses as an engine URL instance built
* from its href (the first call installs a minimal island URL class when
* no global exists — construction re-parses through scr_url.c's WHATWG
* parser; href/protocol/pathname readable, other components throw, see
* SEMANTICS.md). Borrows; +1 cell out, or NULL with the exception
* bridged. scr_jsval_from_bytes is its typed-array sibling — declared
* with the bytes surface below. */
ScrJsval *scr_jsval_from_url(ScrUrl *u);
/* Payload tags for scr_jsval_from_promise — the fulfillment types an
* async island callback may resolve with (the compiler fences the rest). */
enum {
SCR_ISLP_VOID = 0,
SCR_ISLP_F64 = 1,
SCR_ISLP_BOOL = 2,
SCR_ISLP_STR = 3,
SCR_ISLP_JSVAL = 4,
/* Promise<any[]> — the fulfillment is a native array of engine cells
* (the jsval-element-array spelling): it re-enters the engine as a
* fresh engine array whose ELEMENTS are the same engine values (the
* loadPlugins shape: identity crosses, the spine is a copy). */
SCR_ISLP_JSVAL_ARR = 5,
};
/* Wrap a scriptc promise as an ENGINE promise (async callbacks crossing
* into the island: the package sees a real thenable). Takes ownership of
* `p` (+1 moves in). A waiter fiber awaits it and settles the engine
* capability when it settles — fulfillment marshals per `payload`
* (SCR_ISLP_*), rejection reverse-bridges the reason (Errors cross as
* real engine Errors). The await OBSERVES a rejection for the static
* ledger, and the engine's rejection tracker takes over for the wrapper
* promise — one report, one voice, whichever world drops the ball.
* Wrappers still pending at teardown free their engine values then (the
* island audit sees zero live allocations). NULL only if the engine
* cannot mint a capability (exception bridged). */
ScrJsval *scr_jsval_from_promise(ScrPromise *p, int payload);
/* The REVERSE bridge: an ENGINE promise (a package call's jsval whose
* declared type is Promise<T>) settles a fresh STATIC promise, so
* `await pkgCall()` parks a fiber and `.catch/.finally` desugar over it.
* Borrows `v`; the +1 result is pending until the engine promise settles
* (subscription is Promise.resolve(v).then(onF, onR) through a pinned
* prelude helper — thenables and plain values behave like `await`).
* Fulfillment stores the retained value cell (SCR_ISLP_JSVAL) or nothing
* (SCR_ISLP_VOID; the compiler passes no other tags); rejection converts
* the reason exactly like a bridged exception (engine Errors become
* ScrErrors picked by name) and rejects — re-thrown at the await,
* ledger-tracked if never observed. The .then marks the engine promise
* HANDLED for the island's rejection tracker, so exactly one world
* reports a dropped rejection. Bridging the same engine promise twice
* just makes two static observers of one settlement. NULL only on an
* engine-level surprise minting the subscription (exception bridged). */
ScrPromise *scr_jsval_bridge_promise(ScrJsval *v, int payload);
/* Island-native literals: build an object from key/value pairs (keys are
* marshaled strings) or an array from elements. Borrow argv; +1 out.
* Cannot fail (allocation failure aborts, like every runtime OOM). */
ScrJsval *scr_jsval_obj_lit(int npairs, ScrJsval **kv);
/* Getter completion for an island literal: defines `key` on `obj` as an
* engine getter invoking `fn`; answers the object retained (+1). */
ScrJsval *scr_jsval_define_getter(ScrJsval *obj, ScrJsval *key, ScrJsval *fn);
/* The engine TemplateStringsArray for an island tag call: n cooked then n
* raw strings; the result array carries `.raw` (+1). */
ScrJsval *scr_jsval_tpl_strings(int n, ScrJsval **kv);
/* Spread completion for an island literal: engine CopyDataProperties of
* `src` onto `obj`; answers the target (+1), NULL + pending on a getter
* throw. */
ScrJsval *scr_jsval_obj_spread(ScrJsval *obj, ScrJsval *src);
ScrJsval *scr_jsval_arr_lit(int n, ScrJsval **elems);
/* Marshal out (island → static): validated, STRICT extraction — a
* non-number refuses to exit as number (no coercion), throwing a
* catchable path-less TypeError like the dynCheck walkers'. Composite
* targets exit via scr_jsval_to_json + the existing json.parse +
* dynCheck pipeline instead (to_json throws if the value is not
* JSON-representable, e.g. contains a function). */
int scr_jsval_exit_f64(ScrJsval *v, double *out);
int scr_jsval_exit_bool(ScrJsval *v, bool *out);
ScrStr *scr_jsval_exit_str(ScrJsval *v);
/* Uint8Array exit (engine Buffers pass — they ARE Uint8Arrays): a fresh
* u8 COPY (+1), the boundary's aliasing stance; NULL = thrown. */
ScrBytes *scr_jsval_exit_bytes(ScrJsval *v);
/* `any[]`-declared slot exit (the jsval-element-array spelling):
* Array.isArray-gated, elements BY REFERENCE into a native array of
* engine cells (identity crosses, the spine is a snapshot copy). +1;
* NULL = thrown. */
ScrArr *scr_jsval_exit_jsval_arr(ScrJsval *v);
ScrStr *scr_jsval_to_json(ScrJsval *v);
/* The import boundary (libCall island.import): loads an embedded module —
* the package's runtime entry — through the island's module system and
* takes one export as an owned (+1) jsval. `name` is an export name,
* "default", or "*" (the namespace object; for CommonJS entries, an
* interop object over module.exports). The engine's module registry (and
* the require shim's cache) make repeated imports lookups, not
* re-evaluations. `specifier` is the specifier as WRITTEN at the import
* site: an ESM entry that does not provide a requested named export
* throws Node's link-time SyntaxError naming it ("The requested module
* 'x' does not provide an export named 'y'"). Borrows all args. NULL =
* bridged exception (a package's top-level code can throw). */
ScrJsval *scr_jsval_import(const ScrStr *key, const ScrStr *name, const ScrStr *specifier);
/* Dynamic import() (libCall island.importDyn): loads a module — an
* embedded module's key or a builtin shim's "node:x" key — through the
* island's module system and answers an owned (+1) jsval holding an
* ENGINE PROMISE of the namespace object. ALWAYS a promise: load and
* evaluation failures REJECT it (Node's dynamic-import shape). Borrows
* the key. NULL only on an engine-level surprise minting the rejected
* promise. */
ScrJsval *scr_jsval_import_dyn(const ScrStr *key);
/* The deferred boundary failure (libCall island.castFail): throws a
* catchable TypeError naming the target type — a checked cast of an
* island value to a type with no validated exit fails at RUNTIME, at the
* cast, instead of refusing the build. Borrows both args; the exception
* is pending on return. */
void scr_jsval_cast_fail(ScrJsval *v, const ScrStr *target);
#endif /* SCR_DYNAMIC */
/* ── numbers ────────────────────────────────────────────────────────────
* JS-exact Number-to-string (ECMA-262 §6.1.6.1.20 Number::toString, radix
* 10): shortest digit string that round-trips, ECMA placement rules.
* Writes NUL-terminated result into buf (>= 32 bytes), returns length.
*/
size_t scr_f64_to_str(double x, char *buf);
/* The Ryū digit core (scr_number.c), shared with the Intl en-US number
* formatter: the shortest round-tripping digits of a POSITIVE finite
* double — value = 0.digits × 10^n, no trailing zeros, NUL-terminated.
* Returns k, the digit count (≤ 17). */
int scr_f64_digits(double x, char digits[18], int *n_out);
/* ToString for template literals / string coercion. Returns +1. */
ScrStr *scr_f64_to_scrstr(double x);
ScrStr *scr_bool_to_scrstr(bool b); /* interned "true"/"false" */
/* ── String surface (scr_lib.c) ───────────────────────────────────────
* fromCharCode: ONE packed f64[] of UTF-16 code units — ToUint16 each,
* adjacent surrogate pairs combine, lone surrogates become U+FFFD
* (divergence 1's policy). Borrowed args; the string result is +1; neither
* throws. */
ScrStr *scr_str_from_char_code(ScrArr *codes);
/* Single numeric argument; avoids argument packing. Defined in scr_string.c
* to share its character cache. Same ToUint16 and lone-surrogate behavior. */
ScrStr *scr_str_from_char_code_one(double code);
/* The spread-typed-array form (String.fromCharCode(...bytes) — the
* magic-number ASCII probe); same semantics per element. */
ScrStr *scr_str_from_char_code_bytes(ScrBytes *codes);
/* Numeric Unicode code points, borrowed source; +1 string or NULL with
* a pending RangeError. Adjacent surrogate code points combine. */
ScrStr *scr_str_from_code_point(ScrArr *codes);
ScrStr *scr_str_from_code_point_bytes(ScrBytes *codes);
/* ── Number statics (scr_lib.c) ───────────────────────────────────────
* JS-exact by construction: Number.isFinite/isNaN/isInteger/isSafeInteger
* never coerce, and the compiler only routes f64-typed arguments here.
* None throws. */
bool scr_num_is_finite(double x);
bool scr_num_is_nan(double x);
bool scr_num_is_integer(double x);
bool scr_num_is_safe_integer(double x);
/* Number.prototype formatters: toExponential() is the fraction-digits-free
* shortest correctly-rounded mantissa; toFixed0 is the non-throwing omitted
* argument form. toFixed implements an explicit 0..100 fractionDigits with
* exact binary-value rounding and THROWS V8's catchable RangeError outside
* that range. All successful results are +1. */
ScrStr *scr_num_to_exponential(double x);
ScrStr *scr_num_to_fixed0(double x);
ScrStr *scr_num_to_fixed(double x, double fraction_digits);
/* Object.is over two numbers — the spec's SameValue on doubles: NaN
* equals NaN, +0 differs from -0, everything else is ==. Never throws. */
bool scr_num_same_value(double a, double b);
/* Intl.NumberFormat("en-US").format(x) / x.toLocaleString("en-US") with
* default options: decimal notation, 0–3 fraction digits rounded half-up
* on the shortest round-tripping decimal (ICU's rounding input — NOT
* toFixed's exact-value rounding), "," grouping every three integer
* digits, "∞"/"NaN" texts, "-0" for negative inputs rounding to zero.
* en-US is the one embedded locale. Result +1; never throws. */
ScrStr *scr_intl_num_format_en_us(double x);
/* ── Date, the read-only value slice (scr_lib.c) ──────────────────────
* A Date value is its TimeClip'd epoch-millisecond scalar. Identity and
* mutation are frontend-fenced; construction, storage, getters, and ISO
* formatting are exact over this representation. */
double scr_date_now(void); /* integer ms since epoch, like Node */
double scr_date_new_ms(double ms); /* TimeClip (NaN when invalid) */
double scr_date_get_time(double ms);
/* Node's exact ISO 8601 UTC format (expanded ±YYYYYY years outside
* 0–9999). THROWS Node's "Invalid time value" RangeError on NaN or
* |ms| > 8.64e15 and returns NULL; +1 otherwise. */
ScrStr *scr_date_to_iso(double ms);
/* new Date(dateString).getTime(): the BOUNDED parse (the X509 validity
* shape + ECMA's format with an explicit offset); NaN elsewhere. */
double scr_date_parse_get_time(ScrStr *s);
/* Date.UTC over already-number arguments (frontend completes the spec's
* defaults): MakeDay/MakeTime/TimeClip exactly — 0–99 maps to 1900+year,
* month/date rollover, NaN for non-finite parts, out-of-range results,
* and years past V8's ±1e6 MakeDay bound. Never throws. */
double scr_date_utc(double y, double mo, double d,
double h, double mi, double s, double ms);
double scr_date_get_full_year(double ms, bool utc);
double scr_date_get_month(double ms, bool utc);
double scr_date_get_date(double ms, bool utc);
double scr_date_get_day(double ms, bool utc);
double scr_date_get_hours(double ms, bool utc);
double scr_date_get_minutes(double ms, bool utc);
double scr_date_get_seconds(double ms, bool utc);
double scr_date_get_milliseconds(double ms);
double scr_date_get_timezone_offset(double ms);
/* One-argument ABI wrappers used by the LLVM lib-call table. */
double scr_date_get_full_year_local(double ms);
double scr_date_get_full_year_utc(double ms);
double scr_date_get_month_local(double ms);
double scr_date_get_month_utc(double ms);
double scr_date_get_date_local(double ms);
double scr_date_get_date_utc(double ms);
double scr_date_get_day_local(double ms);
double scr_date_get_day_utc(double ms);
double scr_date_get_hours_local(double ms);
double scr_date_get_hours_utc(double ms);
double scr_date_get_minutes_local(double ms);
double scr_date_get_minutes_utc(double ms);
double scr_date_get_seconds_local(double ms);
double scr_date_get_seconds_utc(double ms);
/* ── numeric coercion + bitwise operators ─────────────────────────────
* JS-exact ToInt32/ToUint32 semantics: NaN/±Infinity → 0, truncation
* toward zero, modular wrap into 32 bits; the operation runs in 32-bit
* space (shift counts mask to 5 bits, `>>` is an arithmetic shift spelled
* portably — no C UB/implementation-defined shifts) and the result
* returns to f64: `>>>` as Uint32, everything else as Int32.
* scr_to_uint32 lives in scr_number.c; the bitwise operations in scr_lib.c. */
uint32_t scr_to_uint32(double d);
double scr_bit_and(double a, double b);
double scr_bit_or(double a, double b);
double scr_bit_xor(double a, double b);
double scr_bit_shl(double a, double b);
double scr_bit_shr(double a, double b);
double scr_bit_ushr(double a, double b);
double scr_bit_not(double a);
/* ── typed arrays / Buffer (scr_bytes.c) ──────────────────────────────
* ONE runtime representation for the supported numeric typed arrays,
* Node's Buffer (a Uint8Array subclass), and DataView: a refcounted,
* MUTABLE, fixed-length element buffer. An ScrBytes either OWNS its
* storage (backing == NULL, byteOffset 0) or is a VIEW: its `data` points
* INTO an owner's storage and its `backing` retains that owner (chain
* depth is always exactly 1 — views over views resolve to the owner at
* construction), so reads/writes through the view alias the source
* JS-exactly. Views come from DataView (scr_dataview_new) and from
* subarray()/Buffer-slice() (scr_bytes_subarray — Buffer's slice is
* subarray's deprecated alias in Node); only the plain typed arrays'
* slice() copies (scr_bytes_slice, matching JS).
* Elements are scalars only and the backing edge is acyclic by
* construction (owners point at nothing): never part of a cycle, no
* trace. Element reads widen to double; writes coerce JS-exactly (ToUint8
* / ToUint32 modular truncation, double→float rounding for f32).
* Out-of-bounds ELEMENT access traps like arrays (JS returns undefined /
* ignores the write — documented divergence); the Node-shaped operations
* (construction lengths, set(), the read/write numeric families, the
* DataView constructor and getters) THROW catchable RangeErrors exactly
* where Node throws. */
typedef enum ScrBytesElem {
SCR_BYTES_U8, /* Uint8Array / Buffer */
SCR_BYTES_U32, /* Uint32Array */
SCR_BYTES_F32, /* Float32Array */
SCR_BYTES_I32, /* Int32Array (reads sign-extend; writes ToInt32-wrap) */
SCR_BYTES_F64, /* Float64Array */
SCR_BYTES_I8, /* Int8Array */
SCR_BYTES_U16, /* Uint16Array */
SCR_BYTES_I16, /* Int16Array */
SCR_BYTES_U8C, /* Uint8ClampedArray */
} ScrBytesElem;
typedef struct ScrBytes {
size_t rc;
size_t len; /* ELEMENT count, fixed at construction */
ScrBytesElem elem;
uint8_t *data; /* len * elem_size bytes; owned unless backing is set */
/* NULL for owners. A view (DataView, subarray, Buffer-slice) sets this
* to the retained OWNER it aliases (chain depth is always exactly 1:
* views over views resolve to the owner at construction) and its `data`
* points into backing->data — released, never freed. */
struct ScrBytes *backing;
bool is_buffer; /* Buffer brand belongs to the view, not its backing. */
bool external; /* Root aliases foreign memory; releasing it never frees data. */
} ScrBytes;
ScrBytes *scr_bytes_from_external(void *data, size_t length);
ScrDyn *scr_ffi_memory_module(ScrDyn *catalog);
ScrDyn *scr_ffi_argument(ScrDyn *value, ScrStr *type);
size_t scr_bytes_elem_size(ScrBytesElem elem); /* 1, 2, 4, or 8 */
const char *scr_bytes_elem_name(ScrBytesElem elem);
double scr_bytes_to_u8_clamp(double value);
/* node:string_decoder's StringDecoder (scr_bytes.c, beside the decoders
* it shares): the decoder value is a record holding the CANONICAL
* encoding name and an f64 packing the pending partial sequence (count +
* up to 3 raw bytes — every encoding buffers at most 3); the compiler's
* interned %strdec helpers thread both through these pure functions.
* write answers the decoded complete prefix of pending+chunk, next the
* packed new pending, end the buffered partial's flush (utf8 replacement
* chars, base64's padded remainder, utf16le's held bytes; the stateless
* latin1/ascii/hex flush nothing). Node-exact per encoding — utf8's
* incomplete-tail walk, base64's mod-3 groups, utf16le's odd-byte and
* trailing-lead-surrogate holds. None throws. */
ScrStr *scr_strdec_write(const ScrStr *enc, double pending, const ScrBytes *chunk);
double scr_strdec_next(const ScrStr *enc, double pending, const ScrBytes *chunk);
ScrStr *scr_strdec_end(const ScrStr *enc, double pending);
#ifdef SCR_DYNAMIC
/* Static → island typed-array marshal (scr_island.c; --dynamic builds
* only): an engine typed array of the same element kind — a COPY, the
* boundary's copy stance. Borrows; +1 cell, or NULL bridged. */
ScrJsval *scr_jsval_from_bytes(const ScrBytes *b);
#endif
/* `new Uint8Array(n)` / Buffer.alloc(n): zero-filled. n goes through
* ToIndex like JS (NaN → 0, truncate toward zero; negative or > 2^53-1
* THROWS Node's "Invalid typed array length" RangeError catchably and
* returns NULL with the exception pending). */
ScrBytes *scr_bytes_new(ScrBytesElem elem, double n); /* +1 */
/* Shared u8 view over exactly the source view's raw bytes. Borrows; +1. */
ScrBytes *scr_bytes_raw_view(ScrBytes *bytes);
ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes); /* borrows; +1, Buffer factory only */
/* Views retain the root allocation; offsets are relative to its complete
* byte storage, including when src is itself a subarray. */
ScrBytes *scr_bytes_buffer_view(ScrBytes *src, ScrBytesElem elem,
double offset, bool has_len, double length);
ScrDyn *scr_array_buffer_new(ScrDyn *length);
ScrDyn *scr_array_buffer_from_bytes(ScrBytes *view);
bool scr_array_buffer_is(const ScrDyn *value);
bool scr_array_buffer_is_view(const ScrDyn *value);
double scr_array_buffer_byte_length_getter(void);
ScrDyn *scr_array_buffer_byte_length_descriptor(ScrDyn *getter);
ScrBytes *scr_array_buffer_view(ScrBytesElem elem, const ScrDyn *buffer,
const ScrDyn *offset, const ScrDyn *length);
ScrBytes *scr_array_buffer_view_u8c(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_i8(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_u16(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_i16(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_u8(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_u32(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_i32(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_f32(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_f64(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_dv(ScrDyn *, ScrDyn *, ScrDyn *);
/* Native callback boundary: exact owned u8 copy. NULL with len == 0 is an
* empty span. */
ScrBytes *scr_bytes_from_data(const uint8_t *data, size_t len); /* +1 */
/* `new Uint8Array(src)` / Buffer.from(u8): a same-elem-kind copy (the
* compiler fences cross-kind construction). Borrows src. Never throws. */
ScrBytes *scr_bytes_copy(const ScrBytes *src); /* +1 */
/* `new Uint8Array([1, 2, 3])` / Buffer.from(number[]): each f64 element
* coerces per the element kind (ToUint8/ToUint32/float). Borrows arr
* (SCR_ELEM_F64). Never throws. */
ScrBytes *scr_bytes_from_arr(ScrBytesElem elem, const ScrArr *arr); /* +1 */
static inline ScrBytes *scr_bytes_retain(ScrBytes *b) {
if (b->rc != SIZE_MAX) b->rc++;
return b;
}
void scr_bytes_release(ScrBytes *b); /* NULL-tolerant */
void *scr_bytes_retain_v(void *b);
void scr_bytes_release_v(void *b);
#ifdef SCR_RC_AUDIT
long scr_bytes_live_count(void);
#endif
double scr_bytes_len(const ScrBytes *b); /* element count */
double scr_bytes_byte_len(const ScrBytes *b); /* len * elem size */
/* `.byteOffset`: 0 for owners (scriptc typed arrays always own their
* whole storage — SEMANTICS.md notes the divergence from Node's Buffer
* pooling), the view's offset into its owner's storage for a DataView. */
double scr_bytes_byte_offset(const ScrBytes *b);
/* `new DataView(src.buffer, byteOffset?, byteLength?)` — src is the bytes
* value whose `.buffer` the program named (any elem kind; a DataView src
* resolves to ITS owner, so offsets stay relative to the one buffer,
* exactly like JS). Both indices go through ToIndex (NaN → 0, truncate
* toward zero); a bad offset THROWS Node's "Start offset %s is outside
* the bounds of the buffer" and a bad/overflowing length "Invalid
* DataView length %s" (catchable RangeErrors, NULL result with the
* exception pending). has_len distinguishes an omitted byteLength (view
* to the end of the buffer) from an explicit one. Borrows src; the
* result retains the owner. */
ScrBytes *scr_dataview_new(ScrBytes *src, double byte_off, bool has_len, double byte_len); /* +1 */
/* DataView getters. LE flag false = big-endian (the JS default). The
* BIGU64/BIGI64 kinds answer the composed Number(view.getBigUint64/
* getBigInt64(...)) lowering: the 8-byte integer converted to double
* (round-to-nearest-even — exactly Number(bigint)). Offsets go through
* ToIndex; any invalid or out-of-range offset THROWS Node's "Offset is
* outside the bounds of the DataView" RangeError catchably (0 result
* with the exception pending). */
typedef enum ScrDataViewGet {
SCR_DV_U8, SCR_DV_I8, SCR_DV_U16, SCR_DV_I16, SCR_DV_U32, SCR_DV_I32,
SCR_DV_F32, SCR_DV_F64, SCR_DV_BIGU64, SCR_DV_BIGI64,
} ScrDataViewGet;
double scr_dataview_get(const ScrBytes *b, double byte_off, ScrDataViewGet kind, bool le);
/* DataView setters (the integer/float kinds only — the BIG kinds never
* lower: bigint arguments have no representation). Values coerce
* JS-exactly: the integer kinds by modular truncation (ToUint32's residue
* — the narrower widths store its low bytes, the same 2^width residue),
* F32 by double→float round-to-nearest-even. Offsets go through ToIndex
* with the getters' one RangeError. */
void scr_dataview_set(ScrBytes *b, double byte_off, double value, ScrDataViewGet kind, bool le);
/* Element read/write. Any invalid index — negative, fractional, NaN, or
* out of bounds — TRAPS like the array runtime (SEMANTICS.md documents
* the divergence from JS's undefined-read/ignored-write). Writes coerce
* JS-exactly: u8/u32 by modular truncation (NaN/±Infinity → 0, truncate
* toward zero, wrap mod 2^8/2^32), f32 by double→float rounding. */
double scr_bytes_get(const ScrBytes *b, double i);
void scr_bytes_set(ScrBytes *b, double i, double v);
/* Replace a live typed-array capsule's fixed-size payload from its dyn
* snapshot. Source and target have the same compiler-checked bytes type. */
void scr_bytes_copy_contents(ScrBytes *dst, const ScrBytes *src);
/* TypedArray.prototype.slice(start, end): relative indices clamp like
* string/array slice (ToIntegerOrInfinity, negatives from the end); the
* result is a fresh same-kind copy. Never throws. */
ScrBytes *scr_bytes_slice(const ScrBytes *b, double start, double end); /* +1 */
/* Independent element-coerced copy; same-kind copies preserve raw bits. */
ScrBytes *scr_bytes_convert(ScrBytesElem elem, const ScrBytes *src); /* +1 */
/* Checked native constructor/from input; no JavaScript engine required.
* `from` selects iterable/array-like semantics, otherwise primitives are
* converted to lengths. Borrows source, returns +1 or NULL+pending. */
ScrBytes *scr_bytes_from_dyn(ScrBytesElem elem, const ScrDyn *value, bool from);
/* In-place, overlapping copy with relative/clamped element indices;
* returns the retained receiver, including for a shared subarray view. */
ScrBytes *scr_bytes_copy_within(ScrBytes *b, double target, double start, double end); /* +1 */
/* ES2023 typed-array copying methods. Both preserve the receiver's element
* kind and return a fresh +1 owner. with() raises Node's catchable
* "Invalid typed array index" RangeError for an invalid relative index. */
ScrBytes *scr_bytes_to_reversed(const ScrBytes *b); /* +1 */
ScrBytes *scr_bytes_with(const ScrBytes *b, double index, double value); /* +1 */
/* Numeric typed-array iteration drained into number[], and Uint8Array.join.
* Inputs borrowed; results fresh +1. */
ScrArr *scr_bytes_to_arr(const ScrBytes *b); /* +1 */
ScrStr *scr_bytes_join(const ScrBytes *b, const ScrStr *separator); /* +1 */
/* TypedArray.prototype.fill on non-u8 receivers: per-element fill with
* the element write's coercion, slice-clamped relative indices; answers
* the receiver +1 (chaining). Never throws. */
ScrBytes *scr_bytes_fill_elem(ScrBytes *b, double v, double start, double end); /* +1 */
/* TypedArray.prototype.subarray(start, end) — and Buffer's slice(), its
* deprecated alias: a same-elem VIEW aliasing the receiver's storage
* (mutations visible both ways, JS-exactly). The view retains the OWNER
* (chain depth exactly 1, the DataView rule) and its byteOffset composes.
* Same index clamping as slice; never throws. */
ScrBytes *scr_bytes_subarray(ScrBytes *b, double start, double end); /* +1 */
/* dst.set(src, offset): overlapping-safe, element-coerced copy.
* offset goes through ToIntegerOrInfinity; a negative offset or
* src.len + offset > dst.len THROWS Node's "offset is out of bounds"
* RangeError catchably. */
void scr_bytes_set_from(ScrBytes *dst, const ScrBytes *src, double offset);
/* Array-like source with sequential element coercion and catchable errors.
* Earlier writes remain visible when a later coercion throws. */
void scr_bytes_set_from_dyn(ScrBytes *dst, const ScrDyn *src, double offset);
/* buf.toString(enc) on u8 bytes: "utf8" decodes with WHATWG per-maximal-
* subpart U+FFFD replacement (Node-exact for invalid sequences), "hex" is
* lowercase pairs, "base64"/"base64url" the standard/url alphabets (url
* unpadded), "latin1" maps bytes to U+00XX, "ascii" masks to & 0x7f, and
* "utf16le" decodes LE code units (surrogate pairs combine; a LONE
* surrogate surfaces as U+FFFD — the documented divergence; an odd tail
* byte drops). Aliases arrive NORMALIZED (the compiler folds "binary",
* "ucs2", "utf-8", ...); NULL selects the default UTF-8 encoding for
* implicit native coercions. Borrows both; +1 result. Never throws (the
* compiler fences other encodings). */
ScrStr *scr_bytes_to_str(const ScrBytes *b, const ScrStr *enc);
ScrStr *scr_bytes_to_str_range(const ScrBytes *b, const ScrStr *enc, double start, double end);
/* Runtime-valued Buffer.toString encoding: aliases/case canonicalize;
* unknown names throw ERR_UNKNOWN_ENCODING. +1, or NULL when throwing. */
ScrStr *scr_bytes_to_str_checked(const ScrBytes *b, const ScrStr *enc);
ScrStr *scr_bytes_to_str_checked_range(const ScrBytes *b, const ScrStr *enc, double start, double end);
/* WHATWG TextDecoder.decode over u8 bytes (utf-8, default options): the
* same replacement decode as toString("utf8") with the leading BOM
* stripped. Borrows; +1; never throws. */
ScrStr *scr_text_decode(const ScrBytes *b);
/* TextDecoder with a compile-time WHATWG legacy-encoding id. The frontend
* owns label canonicalization and emits only the ids understood by
* scr_bytes.c; keeping this separate from scr_text_decode means default
* UTF-8 users do not retain the legacy mapping tables. Borrows; +1. */
ScrStr *scr_text_decode_legacy(const ScrBytes *b, double encoding);
/* Buffer.from(string, enc): "utf8" copies the bytes; "hex" parses pairs
* and stops at the first invalid/odd tail (Node-lenient); "base64" and
* "base64url" decode the standard AND url-safe alphabets, skipping
* invalid bytes (Node-lenient); "latin1"/"ascii" write each UTF-16
* unit's low byte (astral code points contribute their two surrogates');
* "utf16le" writes LE code units, surrogate pairs included. Borrows
* both; +1 u8 result. Never throws. */
ScrBytes *scr_bytes_from_str(const ScrStr *s, const ScrStr *enc);
/* Buffer.byteLength(string, enc) — enc NORMALIZED like from_str — and
* Buffer.isEncoding(name) (case-insensitive over Node's alias set).
* Borrow; never throw. */
double scr_bytes_byte_length_str(ScrStr *s, const ScrStr *enc);
bool scr_bytes_is_encoding(const ScrStr *s);
/* equals / compare / indexOf-lastIndexOf / fill / copy / swap / write on
* u8 bytes, Node-exact (corpus 1663). compare and copy take nargs — the
* count of PRESENT index args (defaults skip validation like Node);
* their ladders and fill/write's mirror validateOffset ('an integer',
* then '>= 0 && <= max' — Node spells '&&' here) and copy's C++ ladder
* (fractionals truncate silently; targetStart/sourceEnd check only
* >= 0). indexOf coerces byteOffset (NaN = search everything) and steps
* by `align` (2 for utf16le needles — matches land on even offsets).
* fill repeats the pattern over [offset, end) and answers the RECEIVER
* (+1, chaining); the string form zero-fills on an empty pattern where
* the bytes form throws Node's TypeError. swap reverses w-byte groups in
* place (+1 receiver; ERR_INVALID_BUFFER_SIZE off the width). write
* encodes and truncates at unit boundaries (whole UTF-8 code points,
* even utf16le lengths), returning the bytes written. concat_len
* truncates/zero-pads the concatenation to the validated total. */
bool scr_bytes_equals(const ScrBytes *a, const ScrBytes *b);
double scr_bytes_compare(const ScrBytes *src, const ScrBytes *target, double nargs,
double ts, double te, double ss, double se);
/* Node's validateOffset ladder (buffer.js): non-integers (±Infinity and
* NaN included) throw the 'an integer' ERR_OUT_OF_RANGE RangeError, the
* rest the '>= 0 && <= max' render; max < 0 drops the upper bound.
* Returns false after arming the pending throw. */
bool scr_bytes_validate_off(const char *name, double value, double max);
/* The checked-dynamic compare/equals validators (scr_bytes_io.c): Node's
* argument ladder over dyn-boxed arguments — a non-bytes value throws
* ERR_INVALID_ARG_TYPE with the API's own argument name ("buf1"/"buf2",
* "otherBuffer", "target"), non-number offsets ERR_INVALID_ARG_TYPE
* "of type number", out-of-range numbers the validateOffset RangeError;
* an undefined offset takes its Node default. All arguments BORROWED. */
double scr_buffer_compare_chk(const ScrDyn *a, const ScrDyn *b);
ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding); /* +1 or NULL+pending; borrowed args */
bool scr_bytes_equals_chk(const ScrBytes *recv, const ScrDyn *other);
double scr_bytes_compare_chk(const ScrBytes *src, const ScrDyn *target,
const ScrDyn *ts, const ScrDyn *te,
const ScrDyn *ss, const ScrDyn *se);
/* new Buffer(number, encoding)'s string-arm type error (always throws;
* borrowed). */
ScrBytes *scr_buffer_new_string_fail(const ScrDyn *got);
/* fs._toUnixTimestamp over a dyn time value: numeric strings and finite
* numbers coerce (negatives answer now/1000), the rest throw Node's
* ERR_INVALID_ARG_TYPE. Borrowed. */
double scr_fs_to_unix_timestamp(const ScrDyn *t);
/* The fs argument-validation ladders (the fs.*Chk libCalls): Node-order
* validation over dyn values with Node's exact typed errors; a pass
* meets the real operation where one exists (mkdtempSync, macOS
* lchmodSync) or the compiler-rendered fence. All borrowed; the Chk
* forms without results always leave an exception pending. */
ScrDyn *scr_fs_exists_async(const ScrDyn *path, const ScrDyn *cb);
void scr_fs_mkdtemp_chk(const ScrDyn *prefix, const ScrDyn *cb, const ScrStr *fence);
ScrStr *scr_fs_mkdtemp_sync_chk(const ScrDyn *prefix, const ScrDyn *opts, const ScrStr *fence);
void scr_fs_read_file_chk(const ScrDyn *path, const ScrDyn *opts, const ScrDyn *cb, const ScrStr *fence);
void scr_fs_opendir_chk(const ScrDyn *path, const ScrDyn *opts, const ScrStr *fence);
void scr_fs_watch_file_chk(const ScrDyn *path, const ScrDyn *listener, const ScrStr *fence);
void scr_fs_lchmod_chk(const ScrDyn *path, const ScrDyn *mode, const ScrDyn *cb, const ScrStr *fence);
ScrDyn *scr_fs_lchmod_sync_chk(const ScrDyn *path, const ScrDyn *mode);
ScrPromise *scr_fsp_lchmod_chk(const ScrDyn *path, const ScrDyn *mode);
void scr_fs_read_chk(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset,
const ScrDyn *length, const ScrDyn *position, const ScrStr *fence);
void scr_fs_stream_opts_chk(const ScrDyn *path, const ScrDyn *opts, const ScrStr *fence);
/* The checked-dynamic max-listeners ladders (scr_events_emitter.c). */
ScrEmitter *scr_emitter_set_max_chk(ScrEmitter *em, const ScrDyn *n);
void scr_emitter_set_default_max_chk(const ScrDyn *n, const ScrStr *name);
double scr_bytes_index_of(const ScrBytes *b, const ScrBytes *needle, double off, double align, bool fwd);
double scr_bytes_index_of_num(const ScrBytes *b, double v, double off, bool fwd);
ScrBytes *scr_bytes_fill(ScrBytes *b, const ScrBytes *pattern, double nargs, double offset, double end);
ScrBytes *scr_bytes_fill_num(ScrBytes *b, double v, double nargs, double offset, double end);
ScrBytes *scr_bytes_fill_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc,
double nargs, double offset, double end);
double scr_bytes_copy_into(const ScrBytes *src, ScrBytes *dst, double nargs,
double ts, double ss, double se);
ScrBytes *scr_bytes_swap(ScrBytes *b, double width);
double scr_bytes_write_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc,
double offset, double len, bool has_len);
ScrBytes *scr_bytes_concat_len(const ScrArr *list, double total);
/* Buffer.concat(list): list is a SCR_ELEM_BYTES array of u8 bytes values;
* the result is one fresh copy of their concatenation. Borrows the list.
* Never throws. */
ScrBytes *scr_bytes_concat(const ScrArr *list); /* +1 */
/* ── util.inspect (scr_inspect.c — linked ONLY when the program calls
* util.inspect/format; see native-toolchain.ts) ─────────────────────────────────────
* The runtime half of the static inspect rendering: the compiler
* synthesizes one traversal helper per static type; these entries own
* Node's exact scalar formatting and the layout engine (frames,
* break-length, grid grouping) for the DEFAULT options. All arguments
* BORROWED; ScrStr results +1. Nothing here throws.
*
* The frame protocol, mirroring Node's formatRaw: a non-empty composite
* calls begin(recurse+1) (indentation +2, currentDepth), renders each
* child to a string and entry()s it (is_num drives the grid's
* padStart/padEnd order), then end(base, brace0, brace1, recurse+1,
* array_extras, trailing_more) reduces to one string at the parent
* indentation. Empty composites and depth placeholders never touch the
* stack (the frontend emits their literals directly). */
ScrStr *scr_insp_f64(double x); /* JS ToString, except -0 → "-0" */
ScrStr *scr_insp_str(ScrStr *s); /* the quoting ladder + line splitting */
ScrStr *scr_insp_regex(ScrRegex *re); /* /source/flags */
ScrStr *scr_insp_buffer(ScrBytes *b); /* <Buffer aa bb ...>, 50-byte cap */
/* The stackless error form: [Name: message] / [Name], plus the stamped
* code slot as its one extra property ({ code: 'X' }); [Name] beyond
* depth when a code makes it composite. */
ScrStr *scr_insp_error(ScrError *e, double recurse, double depth);
/* The checked-dynamic tree: shape lives in the value, so the whole
* traversal is here. dyn-boxed bytes render in the checked-dynamic tree's documented
* Uint8Array identity. */
ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth);
/* format's %s / rest-args twin: dyn strings pass verbatim. */
ScrStr *scr_insp_dyn_s(ScrDyn *d, double depth);
#ifdef SCR_DYNAMIC
/* Island `any` (scr_inspect_island.c — linked when a --dynamic build's
* IR carries insp.* libCalls): the scalar kinds render exactly (typeof
* dispatch); composites THROW a catchable TypeError (callers are
* compiler-emitted pending checks). */
ScrStr *scr_insp_jsval(ScrJsval *v, double recurse, double depth);
#endif
void scr_insp_begin(double recurse);
void scr_insp_entry(ScrStr *s, bool is_num);
/* Circular references, Node-exact (<ref *N> / [Circular *N]): the
* compiler-emitted helpers over CYCLE-CAPABLE composites call circ_check
* FIRST (a value already on the traversal stack answers its circular id,
* assigned in discovery order — the caller renders scr_insp_circular and
* descends no further), seen_push after begin, and ref_wrap around end's
* result (pops the stack; values in the circular map gain the "<ref *N> "
* prefix). State resets at each top-level value's first frame
* (scr_insp_begin(1)). ref_wrap BORROWS s and returns +1. */
double scr_insp_circ_check(const void *v);
void scr_insp_seen_push(const void *v);
ScrStr *scr_insp_circular(double id);
ScrStr *scr_insp_ref_wrap(const void *v, ScrStr *s);
ScrStr *scr_insp_more_items(double remaining); /* "... N more items" */
ScrStr *scr_insp_key(ScrStr *k); /* bare-or-quoted property-name ladder; +1 */
ScrStr *scr_insp_end(ScrStr *base, ScrStr *b0, ScrStr *b1, double recurse,
bool array_extras, bool trailing_more);
/* The buf.read* / buf.write* numeric families on u8 bytes, Node-exact:
* boundsError's ladder (non-integer offset → 'It must be an integer', a
* buffer shorter than the width → 'Attempt to access memory outside
* buffer bounds', otherwise the '>= 0 and <= max' render — big Received
* values carry Node's underscore separators) and checkInt's value gate
* for the integer writes (range first, widths past 4 bytes in the
* '2 ** N' form; NaN passes and writes zeros). All catchable. Fixed
* widths take a kind + littleEndian flag; the variable-width family
* (read/writeUIntLE-style) validates byteLength (1-6) first, then value
* (writes), then offset. Writes return offset + width; float kinds skip
* the value gate entirely (any double stores, f32 rounds to nearest). */
typedef enum {
SCR_BN_U8,
SCR_BN_I8,
SCR_BN_U16,
SCR_BN_I16,
SCR_BN_U32,
SCR_BN_I32,
SCR_BN_F32,
SCR_BN_F64,
} ScrBytesNumKind;
double scr_bytes_read_num(const ScrBytes *b, double offset, ScrBytesNumKind kind, bool le);
double scr_bytes_write_num(ScrBytes *b, double value, double offset, ScrBytesNumKind kind, bool le);
double scr_bytes_read_var(const ScrBytes *b, double offset, double byte_length, bool sign, bool le);
double scr_bytes_write_var(ScrBytes *b, double value, double offset, double byte_length, bool sign, bool le);
bool scr_bytes_read_u64_raw(const ScrBytes *b, double offset, bool le, uint64_t *out);
bool scr_bytes_write_u64_raw(ScrBytes *b, double offset, bool le, uint64_t value);
bool scr_dataview_read_u64_raw(const ScrBytes *b, double offset, bool le, uint64_t *out);
bool scr_dataview_write_u64_raw(ScrBytes *b, double offset, bool le, uint64_t value);
/* fs.readFileSync(path) [no encoding] / writeFileSync(path, buf) — the
* Buffer forms of scr_lib.c's utf8 pair, byte-exact and NUL-safe.
* Failures THROW catchably (scr_fs_throw, Node-shaped messages). */
ScrBytes *scr_fs_read_file_bytes(ScrStr *path); /* +1 */
/* readFileSync's runtime-encoding form (scr_bytes_io.c's note): +1 dyn
* value — a Buffer box for undefined/null, a string for utf8 — or NULL
* with the exception pending. */
ScrDyn *scr_fs_read_file_sync_dyn(ScrStr *path, const ScrDyn *enc);
ScrBytes *scr_fs_read_fd_bytes(double fd); /* +1; the fd form (scr_lib.c) */
void scr_fs_write_file_bytes(ScrStr *path, const ScrBytes *data);
void scr_fs_append_file_bytes(ScrStr *path, const ScrBytes *data);
/* fs/promises readFile(path) [no encoding]: the same read behind an
* already-settled promise — failure REJECTS (catchable at the await). */
ScrPromise *scr_fsp_read_file_bytes(ScrStr *path); /* +1 */
/* crypto.randomBytes(n) → a real u8 Buffer. Out-of-range n THROWS Node's
* RangeError catchably (same check as scr_crypto_random_string). */
ScrBytes *scr_crypto_random_bytes(double n); /* +1 */
/* process.stdout/stderr.write(buf[, encoding]): the raw byte writes' Buffer
* overloads (same streams and buffering as the string forms). The encoding
* is evaluated by the caller and ignored here, as Node does for bytes.
* Constantly true. */
bool scr_process_stdout_write_bytes(const ScrBytes *b, const ScrStr *encoding);
bool scr_process_stderr_write_bytes(const ScrBytes *b, const ScrStr *encoding);
/* The Node-shaped fs error thrower (scr_lib.c): formats "ENOENT: no such
* file or directory, open 'x'" and throws it catchably. Shared with
* scr_bytes.c's fs forms. */
void scr_fs_throw(int e, const char *op, const ScrStr *path);
/* ── zlib (scr_zlib.c — compiled and linked with -lz only when the
* program uses zlib, like scr_regex.c/libregexp; see native-toolchain.ts) ──────────
* deflateSync/inflateSync over u8 bytes with Node's default options
* (zlib format, default level/windowBits). deflate never throws (OOM
* aborts); inflate of corrupt input THROWS Node's error catchably
* ("incorrect header check", ...). Borrow their input; results +1. */
ScrBytes *scr_zlib_deflate(const ScrBytes *data);
ScrBytes *scr_zlib_inflate(const ScrBytes *data);
/* The format-mode variants (0 zlib / 1 raw / 2 gzip; inflate adds
* 3 = auto-detect). Static one-shot codecs call these directly, and
* scr_zlib_island.c bridges them into the embedded engine's node:zlib shim. */
ScrBytes *scr_zlib_deflate_mode(const ScrBytes *data, double mode, double level);
ScrBytes *scr_zlib_inflate_mode(const ScrBytes *data, double mode);
double scr_zlib_crc32(const ScrBytes *data, double value);
typedef void (*ScrZlibBytesFn)(ScrClosure *cb, ScrError *err,
ScrBytes *value /* moves, NULL on error */);
void scr_zlib_codec_async(const ScrBytes *data, double mode, bool compressing,
ScrClosure *cb /* moves */, ScrZlibBytesFn fn);
void scr_zlib_island_install(void);
/* ── node:net (scr_net.c — compiled and linked ONLY when the program
* uses the net surface, like scr_events.c; see native-toolchain.ts) ─────────────────
* TCP servers and sockets over the unit's own readiness poller
* (scr_platform.h: kqueue on macOS/BSD, epoll on Linux): refcounted handles
* whose listeners MOVE in and drop at settlement (the ScrChild ownership
* story), consumer-driven reads (the stdin discipline), deferred
* 'listening'/'error'/'close' emits delivered by the loop's dispatch
* hook. The emitted main calls scr_net_install() before %main; net-free
* builds keep their exact link line. Full design note atop scr_net.c. */
typedef struct ScrNetServer ScrNetServer;
typedef struct ScrNetSocket ScrNetSocket;
typedef void (*ScrNetConnFn)(ScrClosure *cb, ScrNetSocket *sock); /* sock +1 */
typedef void (*ScrNetDataFn)(ScrClosure *cb, ScrBytes *chunk); /* borrowed */
/* The listener-list family (snapshot firing, once-before-run): owned by
* scr_net.c, reused by scr_http.c for the request-body event lists. */
typedef struct ScrNetOnce ScrNetOnce;
typedef struct {
ScrClosure *cb; /* owned */
void *fn; /* adapter with the event's firing ABI */
bool once;
ScrNetOnce *shared_once; /* owned, nullable: one once registration mirrored across lists */
} ScrNetL;
typedef struct {
ScrNetL *ls;
size_t n, cap;
} ScrNetLs;
void scr_net_ls_add(ScrNetLs *l, ScrClosure *cb, void *fn, bool once);
ScrNetOnce *scr_net_once_new(void); /* +1 */
ScrNetOnce *scr_net_once_retain(ScrNetOnce *once); /* +1 */
void scr_net_once_release(ScrNetOnce *once);
void scr_net_ls_add_shared_once(ScrNetLs *l, ScrClosure *cb, void *fn,
ScrNetOnce *once /*moves*/);
bool scr_net_ls_has_live(const ScrNetLs *l);
void scr_net_ls_drop(ScrNetLs *l);
size_t scr_net_ls_snapshot(ScrNetLs *l, ScrNetL **out);
void scr_net_fire0(ScrNetLs *l);
/* The receiver-binding twins: the emitting handle rides the pass as the
* ambient receiver (scr_dyn_this_push — `this` in listener bodies).
* `self` BORROWED; NULL binds undefined (= the plain spellings above). */
void scr_net_fire0_this(ScrNetLs *l, void *self, ScrDynHandleTag tag);
void scr_net_fire_err_this(ScrNetLs *l, ScrStr *msg, void *self, ScrDynHandleTag tag);
/* Protocol-layer (scr_http.c) hooks: a C-level connection consumer on
* servers, and a C-level reader on sockets (counts as a consumer for
* read-arming; the context BORROWS its socket and is freed with it). */
typedef void (*ScrNetNativeConnFn)(void *ctx, ScrNetSocket *sock);
typedef void (*ScrNetNativeDataFn)(void *ctx, const char *buf, size_t n);
typedef void (*ScrNetNativeEventFn)(void *ctx);
typedef bool (*ScrNetNativeTimeoutHandledFn)(void *ctx);
typedef bool (*ScrNetNativeErrFn)(void *ctx, ScrStr *msg); /* true = consumed */
void scr_net_server_set_native_conn(ScrNetServer *s, ScrNetNativeConnFn fn, void *ctx, void (*ctx_free)(void *));
/* The HTTP-parser ctx ALIAS: scr_http.c stamps its server ctx here so
* late 'request' listener installs (scr_http_server_on_request) can
* reach it even after a TLS wrap replaced the native-conn hook (the
* https/http2 servers: scr_tls.c owns the native ctx, the http ctx sits
* inside it). BORROWED — owned via the native-conn chain, valid while
* the server lives; NULL on servers with no HTTP parser. */
void scr_net_server_set_http_ctx(ScrNetServer *s, void *ctx);
void *scr_net_server_get_http_ctx(ScrNetServer *s);
/* The protocol layer's settle hook: called once when the server settles
* ('close' emitted, lists dropped) with the http_ctx alias — scr_http.c
* drops its own request/upgrade/connect lists there (the settle-releases-
* listeners story across the layer seam). */
void scr_net_server_set_proto_settle(ScrNetServer *s, void (*fn)(void *));
bool scr_net_server_settled(ScrNetServer *s); /* 'close' already emitted */
/* The dyn-dispatch hook for HTTP server events: scr_http.c registers it
* at scr_http_dyn_install, and the netServer handle ops route
* on('request', ...) (and future http events) through it — link gating
* keeps scr_net.c from naming http entry points. Returns false for an
* event the http layer does not model (the caller fences loudly). */
void scr_net_set_dynh_http_on(bool (*fn)(ScrNetServer *, const char *, const ScrDyn *, bool));
void scr_net_sock_set_native_reader(ScrNetSocket *s, ScrNetNativeDataFn data, ScrNetNativeEventFn eof, ScrNetNativeEventFn closed, void *ctx, void (*ctx_free)(void *));
void scr_net_sock_set_native_idle_checker(ScrNetSocket *s, bool (*fn)(void *));
/* The upgrade handover: clear the reader's fn pointers, keep the ctx. */
void scr_net_sock_clear_native_reader(ScrNetSocket *s);
/* The accepting server (BORROWED; NULL on client sockets) — the protocol
* layer's receiver for server-level fires ('request': this === server). */
ScrNetServer *scr_net_sock_server(ScrNetSocket *s);
/* socket.setEncoding(enc): utf8 flips 'data' delivery to strings (the
* chunk-encoding window); real-but-unsupported encodings fence loudly;
* unknown names throw Node's ERR_UNKNOWN_ENCODING. May throw. */
void scr_net_sock_set_encoding(ScrNetSocket *s, ScrStr *enc /*borrowed*/);
bool scr_net_sock_destroyed(ScrNetSocket *s); /* socket.destroyed */
bool scr_net_sock_writable(ScrNetSocket *s); /* socket.writable */
bool scr_net_sock_established(ScrNetSocket *s); /* connected, including TLS handshake */
void scr_net_sock_set_native_events(ScrNetSocket *s, ScrNetNativeEventFn timeout, ScrNetNativeErrFn err);
void scr_net_sock_set_native_http_timeout(ScrNetSocket *s, ScrNetNativeTimeoutHandledFn fn);
void scr_net_sock_write_native(ScrNetSocket *s, const char *buf, size_t n);
/* The protocol layer's deferred-emit hook: `pending` joins the loop's
* liveness test, `sweep` runs at every net sweep top. */
void scr_net_set_proto_sweep(bool (*pending)(void), void (*sweep)(void));
void scr_net_fire_err(ScrNetLs *l, ScrStr *msg); /* unhandled => exit(1) */
ScrNetServer *scr_net_server_retain(ScrNetServer *s);
void scr_net_server_release(ScrNetServer *s);
void *scr_net_server_retain_v(void *p);
void scr_net_server_release_v(void *p);
ScrNetSocket *scr_net_sock_retain(ScrNetSocket *s);
void scr_net_sock_release(ScrNetSocket *s);
void *scr_net_sock_retain_v(void *p);
void scr_net_sock_release_v(void *p);
ScrNetServer *scr_net_create_server(ScrClosure *handler /*moves, nullable*/, ScrNetConnFn fn); /* +1 */
void scr_net_listen(ScrNetServer *s, double port, ScrClosure *cb /*moves, nullable*/);
/* listen({ port, host, ipv6Only, reusePort }): host is an IP literal ("" =
* the dual-stack any default); ipv6Only sets IPV6_V6ONLY before the bind.
* The old entry point ignores reusePort and is retained for old IR. */
void scr_net_listen_opts(ScrNetServer *s, double port, ScrStr *host /*borrowed*/,
bool ipv6_only, ScrClosure *cb /*moves, nullable*/);
/* Additive ABI for listen({ port, host, ipv6Only, reusePort }): reuse_port
* requests the platform matrix documented in scr_net.c. Unsupported
* platforms and kernels fail asynchronously; they never fall back to an
* ordinary single listener. */
void scr_net_listen_opts_reuse_port(ScrNetServer *s, double port, ScrStr *host /*borrowed*/,
bool ipv6_only, bool reuse_port,
ScrClosure *cb /*moves, nullable*/);
double scr_net_server_port(ScrNetServer *s); /* address().port */
bool scr_net_server_listening(ScrNetServer *s);
/* Writable http.Server timeout property storage. `field` is the compiler
* ABI selector: timeout, keepAliveTimeout, headersTimeout, requestTimeout,
* keepAliveTimeoutBuffer. Typed reads validate that no dynamic write left
* a non-number in the ordinary JS property slot. */
double scr_net_server_timeout_get(ScrNetServer *s, double field);
void scr_net_server_timeout_set(ScrNetServer *s, double field, double value);
void scr_net_server_set_timeout_plain(ScrNetServer *s, double ms);
void scr_net_server_set_timeout_cb(ScrNetServer *s, double ms, ScrClosure *cb /*moves*/, ScrNetConnFn fn);
void scr_net_server_on_timeout(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrNetConnFn fn, bool once);
/* Marks the shared net-server handle as an HTTP/1 or HTTPS server. The
* dynamic handle uses this to keep HTTP-only fields off net/TLS/H2. */
void scr_net_server_enable_http_timeout_surface(ScrNetServer *s);
/* Constructor-option twin: undefined is absent; otherwise validates type
* and a non-negative safe integer before storing, matching Node's option
* ladder (plain property writes do not validate). */
bool scr_net_server_timeout_option_check(double field, double value);
void scr_net_server_timeout_option_set(ScrNetServer *s, double field, const struct ScrDyn *value);
ScrStr *scr_net_server_addr_ip(ScrNetServer *s); /* +1 — address().address */
ScrStr *scr_net_server_addr_family(ScrNetServer *s); /* +1 — address().family */
void scr_net_server_close(ScrNetServer *s, ScrClosure *cb /*moves, nullable*/);
void scr_net_server_close_all_connections(ScrNetServer *s);
void scr_net_server_close_idle_connections(ScrNetServer *s);
/* The REAL close behind `wrapper.close.bind(wrapper)` — never consults
* the override (the proxy-through idiom cannot recurse). */
void scr_net_server_close_direct(ScrNetServer *s, ScrClosure *cb /*moves, nullable*/);
/* wrapper.close = fn — the override MOVES in (a compiler-emitted
* zero-arg wrapper); reassignment releases the old one. */
void scr_net_server_set_close_override(ScrNetServer *s, ScrClosure *ov /*moves*/);
void scr_net_server_on_error(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once);
void scr_net_server_on_close(ScrNetServer *s, ScrClosure *cb /*moves*/, bool once);
/* server.on('listening', cb): the deferred 'listening' list listen(port,
* cb) rides — fires once per successful bind; late registrations on an
* already-listening server never fire (Node's once-per-listen emit). */
void scr_net_server_on_listening(ScrNetServer *s, ScrClosure *cb /*moves*/, bool once);
void scr_net_server_on_connection(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrNetConnFn fn, bool once);
/* 'secureConnection' — the deferred-connection list (TLS handshake
* timing); released unread on a server without deferral (a plain net
* server never fires it, like Node). */
void scr_net_server_on_secure_connection(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrNetConnFn fn, bool once);
ScrNetSocket *scr_net_connect(double port, ScrStr *host /*borrowed, nullable*/, ScrClosure *cb /*moves, nullable*/); /* +1 */
/* connect with a validated autoSelectFamilyAttemptTimeout option: the
* budget runs Node's validateInt32-from-1 ladder (ERR_OUT_OF_RANGE /
* ERR_INVALID_ARG_TYPE) and is then inert — the single dial has nothing
* to time. +1, or NULL with the throw pending. */
ScrNetSocket *scr_net_connect_attempt(double port, ScrStr *host /*borrowed*/, const ScrDyn *t /*borrowed*/);
/* net.connect/createConnection over a RUNTIME option bag (computed
* keys): Node-order validation (objectMode trio, port, host,
* autoSelectFamily, attempt budget), then the compiler-rendered fence —
* always leaves an exception pending. Borrowed. */
void scr_net_connect_opts_chk(const ScrDyn *opts, const ScrStr *fence);
/* connect with a caller lookup (net.connect({ ..., lookup })): invokes
* lookup(hostname, options, answer-closure) synchronously; answer_fn is
* the emitted per-shape thunk that decodes the answer down to
* scr_net_lookup_answer. Dials the answered addresses in order. */
/* Blocking first-answer hostname resolution for the CLIENT bridges (the
* native fetch, the island's http/https client) — the dns.lookup
* precedent: getaddrinfo at call time. +1: the resolved IP, or a retain
* of `host` (already an IP literal / localhost / unresolvable — the dial
* then delivers Node's exact getaddrinfo ENOTFOUND cause). */
ScrStr *scr_net_blocking_lookup(ScrStr *host /*borrowed*/);
ScrNetSocket *scr_net_connect_lookup(double port, ScrStr *host /*borrowed*/,
ScrClosure *lookup /*moves*/, void *answer_fn); /* +1 */
void scr_net_lookup_answer(ScrClosure *self, bool has_err, ScrStr *msg /*borrowed, nullable*/,
ScrArr *ips /*borrowed, nullable*/);
void scr_net_sock_write_str(ScrNetSocket *s, ScrStr *data /*borrowed*/);
void scr_net_sock_write_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/);
void scr_net_sock_end(ScrNetSocket *s);
void scr_net_sock_end_str(ScrNetSocket *s, ScrStr *data /*borrowed*/);
void scr_net_sock_end_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/);
/* Checked-dynamic chunk forms (an untyped JS payload into a typed
* socket): STR/BYTES dispatch; end() takes undefined/null as no chunk;
* anything else throws Node's ERR_INVALID_ARG_TYPE chunk TypeError. */
void scr_net_sock_write_dynv(ScrNetSocket *s, const ScrDyn *d /*borrowed*/);
void scr_net_sock_end_dynv(ScrNetSocket *s, const ScrDyn *d /*borrowed*/);
void scr_net_sock_destroy(ScrNetSocket *s);
/* Flow control (pause/resume — see the struct's flag comments), the
* FIN-flushed destroy (destroySoon), TCP_NODELAY, the deferred write/
* finish callbacks (write(chunk, cb) / end(cb) — sweep-fired), and the
* counters/flags the compat surface reads. */
ScrNetSocket *scr_net_sock_pause(ScrNetSocket *s); /* +1: chaining */
ScrNetSocket *scr_net_sock_resume(ScrNetSocket *s); /* +1: chaining */
ScrNetSocket *scr_net_sock_set_nodelay(ScrNetSocket *s, bool enable); /* +1: chaining */
void scr_net_sock_set_keepalive(ScrNetSocket *s, bool enable, double delay_ms);
void scr_net_sock_destroy_soon(ScrNetSocket *s);
void scr_net_sock_on_finish(ScrNetSocket *s, ScrClosure *cb /*moves*/);
void scr_net_sock_on_write_flush(ScrNetSocket *s, ScrClosure *cb /*moves*/);
double scr_net_sock_bytes_written(ScrNetSocket *s);
bool scr_net_sock_readable(ScrNetSocket *s);
/* The deferred dial (the http agent's maxSockets queue): the socket
* registers "connecting" and buffers writes; dial_start runs the dial. */
ScrNetSocket *scr_net_connect_deferred(double port, ScrStr *host /*borrowed, nullable*/); /* +1 */
void scr_net_sock_dial_start(ScrNetSocket *s);
void scr_net_sock_set_timeout(ScrNetSocket *s, double ms);
void scr_net_sock_on_timeout(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once);
ScrStr *scr_net_sock_remote_address(ScrNetSocket *s); /* +1 or NULL (undefined arm) */
bool scr_net_sock_encrypted(ScrNetSocket *s); /* true = TLS transport (false maps to the undefined arm) */
void scr_net_sock_pipe(ScrNetSocket *src, ScrNetSocket *dst);
void scr_net_sock_on_data(ScrNetSocket *s, ScrClosure *cb /*moves*/, ScrNetDataFn fn, bool once);
void scr_net_sock_on_end(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once);
void scr_net_sock_on_close(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once);
void scr_net_sock_on_error(ScrNetSocket *s, ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once);
void scr_net_sock_on_connect(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once);
/* The paused-mode surface (the demux path: once('readable') + read(1) +
* unshift + emit('connection')). 'readable' listeners are consumers —
* arrived bytes buffer in the receive buffer and the event announces
* them (and EOF, where read answers NULL). read(n) answers exactly n
* buffered bytes (+1) or NULL — Node's less-than-n answer; n <= 0 drains
* everything. unshift returns bytes to the FRONT of the stream, ahead of
* anything still in the kernel. emit('connection', sock) routes a socket
* into another server's protocol layer — the TLS target's 'connection'
* defers to its handshake, the http target's parser drains the buffered
* peek first. */
void scr_net_sock_on_readable(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once);
ScrBytes *scr_net_sock_read_bytes(ScrNetSocket *s, double n); /* +1 or NULL */
void scr_net_sock_unshift_bytes(ScrNetSocket *s, ScrBytes *data /*borrowed*/);
void scr_net_server_emit_connection(ScrNetServer *srv, ScrNetSocket *sock /*borrowed*/);
void scr_net_data_thunk0(ScrClosure *cb, ScrBytes *chunk);
void scr_net_data_thunk_bytes(ScrClosure *cb, ScrBytes *chunk);
void scr_net_data_thunk_str(ScrClosure *cb, ScrBytes *chunk);
/* The dynCheck-adapted listener's flavor: boxes the chunk as a
* Buffer-flavored dyn (toString decodes utf8, Node's 'data' payload). */
void scr_net_data_thunk_dyn(ScrClosure *cb, ScrBytes *chunk);
void scr_net_conn_thunk0(ScrClosure *cb, ScrNetSocket *sock);
void scr_net_conn_thunk_sock(ScrClosure *cb, ScrNetSocket *sock);
void scr_net_install(void);
/* Registers the netSocket handle-dispatch ops (SCR_DYNH_NET_SOCKET) —
* emitted main() calls this alongside scr_net_install exactly when the
* net unit is linked, so the always-linked dyn core never references
* this unit's entry points. */
void scr_net_dyn_install(void);
#ifdef SCR_RC_AUDIT
long scr_net_live_count(void);
#endif
/* ── the socket TRANSPORT hooks (scr_tls.c — compiled only into
* TLS-using binaries; scr_net.c stays transport-agnostic through this
* ops table). A transport sits UNDER the socket kind: reads and writes
* redirect through xread/xwrite once installed (read(2)/write(2) shapes —
* >0 bytes, 0 EOF, -1 with errno, EAGAIN meaning retry on readiness),
* writes buffer until the handshake establishes, and readiness events
* drive `handshake` until it answers done (1) or failed (-1 — the
* transport reports the failure itself via scr_net_sock_transport_error
* first, Node-shaped message or NULL for the server's silent
* tlsClientError default). `on_established` (nullable) lets a protocol
* layer attach AFTER the handshake (the https parser); `pending` answers
* whether decrypted bytes sit buffered inside the transport (the sweep
* drains them — the poller can't see bytes that already left the kernel);
* `shutdown_write` sends the close_notify before the FIN. The ops table
* is static storage in the transport unit; tctx is freed via `free`
* when the socket handle dies. */
typedef struct ScrNetTransportOps {
ssize_t (*xread)(void *tctx, char *buf, size_t n);
ssize_t (*xwrite)(void *tctx, const char *buf, size_t n);
int (*handshake)(void *tctx);
void (*on_established)(void *tctx);
bool (*pending)(void *tctx);
void (*shutdown_write)(void *tctx);
void (*free)(void *tctx);
} ScrNetTransportOps;
void scr_net_sock_set_transport(ScrNetSocket *s, const ScrNetTransportOps *ops, void *tctx);
int scr_net_sock_fd(ScrNetSocket *s);
/* Drain up to n bytes of the socket's receive buffer (peeked/unshifted
* bytes — the demux path); consumers call it before the fd. */
size_t scr_net_sock_take_buffered(ScrNetSocket *s, char *buf, size_t n);
/* Arm the write filter for a handshake send that hit EAGAIN. */
void scr_net_sock_transport_want_write(ScrNetSocket *s);
/* The socket's transport context, guarded by the ops table identity —
* NULL when the socket carries no transport or a different one (the SNI
* answer path re-finds its engine from the boxed socket). */
void *scr_net_sock_transport_ctx_for(ScrNetSocket *s, const ScrNetTransportOps *ops);
/* Re-drive a parked handshake (the asynchronous SNI answer arrived —
* no fd readiness is coming: the client is waiting for the ServerHello). */
void scr_net_sock_transport_resume(ScrNetSocket *s);
/* Replay raw bytes at the FRONT of the receive buffer — the SNI
* pre-parse handing the consumed ClientHello back to the TLS bio. */
void scr_net_sock_replay(ScrNetSocket *s, const char *data, size_t len);
/* Transport-level failure: msg (borrowed, Node's message shape) defers
* 'error' + teardown to the sweep; NULL tears down silently — Node's
* default for an unhandled server-side handshake failure. */
void scr_net_sock_transport_error(ScrNetSocket *s, const char *msg);
/* Defer the server's 'connection' listeners to handshake completion —
* tls.createServer's handler is the 'secureConnection' event, fired with
* the socket only once the TLS layer stands (Node's timing). */
void scr_net_server_defer_connections(ScrNetServer *s);
/* The current native-connection hook (the https wrapper reads the http
* parser installer it is about to wrap). */
void scr_net_server_get_native_conn(ScrNetServer *s, ScrNetNativeConnFn *fn, void **ctx, void (**ctx_free)(void *));
/* ── the checked-dynamic TLS-member hooks (scr_tls.c registers them when
* linked — scr_net.c stays TLS-agnostic, the dynh_http_on precedent).
* Each answers whether it HANDLED the member: `get` fills *out (+1, or
* NULL for the undefined arm) for TLSSocket property reads (authorized,
* authorizationError); `on` registers TLS event names ('secureConnect');
* `invoke` claims TLSSocket method names (the honest per-member fences
* for the not-yet-modeled surface). Unhandled members fall through to
* the plain-socket dispatch unchanged. */
typedef struct ScrNetDynhTlsHooks {
bool (*get)(ScrNetSocket *s, const char *key, struct ScrDyn **out);
bool (*on)(ScrNetSocket *s, const char *event, const struct ScrDyn *cb, bool once);
bool (*invoke)(ScrNetSocket *s, const char *method, struct ScrDyn *const *args, size_t argc,
struct ScrDyn **out);
} ScrNetDynhTlsHooks;
void scr_net_set_dynh_tls(const ScrNetDynhTlsHooks *hooks);
/* ── node:tls + node:https (scr_tls.c — compiled and linked, with the
* vendored mbedTLS archive, only when the program uses them; design
* note atop the file). tls.createServer returns an ORDINARY
* ScrNetServer whose handler fires as 'secureConnection' (post-
* handshake) with a socket that behaves exactly like a net socket;
* https.createServer is http.createServer over the same transport;
* https.request is the http client dialed through a client transport
* (port 443 default, `ca` / rejectUnauthorized for local-CA flows —
* cert/key/ca all arrive as borrowed PEM bytes). */
ScrNetServer *scr_tls_create_server(const char *cert, size_t cert_len, const char *key, size_t key_len, ScrClosure *handler /*moves, nullable*/, ScrNetConnFn fn); /* +1 */
/* ── RUNTIME tls/https options records (the divergence-66 stance: a
* non-literal options value reads its members at runtime; members whose
* literal forms are compile fences become runtime gates that THROW the
* catchable fence instead of behaving differently; undocumented keys
* drop exactly like Node drops them). cert/key values are PEM strings,
* Buffers, or one-element arrays of those; `ca` (client side)
* concatenates array entries into one trust-anchor bundle. The *_dyn
* creators walk a dyn options record (the checked-dynamic JS lane's
* shape) and mint the same servers the literal path mints; NULL returns
* carry a pending exception. scr_tls_pem_from_dyn is the literal path's
* runtime-valued cert/key extraction (`what` names the fence). */
ScrBytes *scr_tls_pem_from_dyn(const struct ScrDyn *v, const char *what); /* +1 or NULL+pending */
ScrNetServer *scr_tls_create_server_dyn(const struct ScrDyn *opts /*borrowed*/, ScrClosure *handler /*moves, nullable*/, ScrNetConnFn fn); /* +1 or NULL+pending */
/* The https twin (its ScrHttpReqFn parameter is declared with the http
* section below): scr_https_create_server_dyn. */
/* ── tls.connect (scr_tls.c): the TLS client socket. Dials TCP like
* net.connect and installs the client transport; the callback (and the
* socket's 'connect'/'secureConnect' listeners) fires post-handshake —
* Node's secureConnect timing. `port` < 0 reads the port from the
* options record; `host` NULL/empty reads host (default "localhost").
* Implemented option members: port, host, rejectUnauthorized (default
* true; false runs the handshake with verification RECORDED, so
* socket.authorized / authorizationError answer Node's split), ca,
* servername (the SNI + verify name). Every other documented member
* throws the runtime fence when present; unknown keys drop. */
ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable*/, const struct ScrDyn *opts /*borrowed, nullable*/, ScrClosure *cb /*moves, nullable*/); /* +1 or NULL+pending */
/* The TLSSocket member surface on the socket kind (all no-ops / honest
* defaults on plain sockets): authorized answers Node's verify verdict
* (false on servers and unverified clients), auth_error the verify-
* failure CODE STRING (+1; NULL = Node's null), on_secure_connect
* registers at the establishment-fired conn list (released unread on a
* plain socket — the event never fires there, like Node), on_session
* registers the received-ticket event (fires once with the serialized
* session as a Buffer; `fn` is the data-listener adapter ABI). */
bool scr_tls_sock_authorized(ScrNetSocket *s);
ScrStr *scr_tls_sock_auth_error(ScrNetSocket *s); /* +1 or NULL */
void scr_tls_sock_on_secure_connect(ScrNetSocket *s, ScrClosure *cb /*moves*/, bool once);
void scr_tls_sock_on_session(ScrNetSocket *s, ScrClosure *cb /*moves*/, void *fn, bool once);
/* ── h2 over TLS (scr_tls.c ↔ scr_http2.c): the ALPN=h2 server wrap and
* the https-authority client wrap. scr_http2.c calls both directly —
* every http2-using binary links the TLS unit (the moduleUsesTls
* switch), so no registration indirection is needed. */
void scr_tls_server_wrap_h2(ScrNetServer *s, const char *cert, size_t cert_len, const char *key, size_t key_len, ScrNetNativeConnFn h2_conn, void *h2_ctx /*moves*/, void (*h2_ctx_free)(void *), ScrClosure *sni_cb /*moves, nullable*/, void *sni_answer_fn);
void scr_tls_h2_client_wrap(ScrNetSocket *sock, ScrStr *host /*borrowed*/, bool reject_unauthorized, const char *ca /*borrowed, len 0 = none*/, size_t ca_len);
/* ── tls.SecureContext + the SNI callback (scr_tls.c) ─────────────────
* A SecureContext is an opaque refcounted parsed cert/key pair
* (tls.createSecureContext({ cert, key })). A server created with an
* SNI callback parses each connection's ClientHello for the server_name
* extension BEFORE the mbedTLS handshake begins (the callback may answer
* asynchronously — cert generation on demand is the portless flow — and
* mbedTLS's own f_sni must answer synchronously, so the runtime resolves
* the name first and replays the hello bytes into the bio), invokes the
* JS callback with (servername, answer-closure), and resumes the
* handshake when scr_tls_sni_answer arrives: has_err tears the socket
* down silently (Node's 'tlsClientError' default), a ctx serves its
* cert/key via mbedtls_ssl_set_hs_own_cert, NULL serves the default
* pair. `answer_fn` is the emitted per-shape thunk that decodes the
* callback's program-interned unions down to this call — it becomes the
* minted answer closure's fn (caps[0] boxes the socket). Connections
* whose ClientHello carries no server_name skip the callback and serve
* the default pair, exactly Node. */
typedef struct ScrSecureCtx ScrSecureCtx;
ScrSecureCtx *scr_tls_create_secure_context(const char *cert, size_t cert_len, const char *key, size_t key_len); /* +1 */
/* createSecureContext over a RUNTIME options record: Node's typed option
* validations first, then the pem walk (+1, or NULL with the exception
* pending). Borrowed. */
ScrSecureCtx *scr_tls_create_secure_context_dyn(const ScrDyn *opts);
/* tls.getCACertificates(type): validateString + the documented name set,
* then the compiler-rendered fence — always leaves an exception pending. */
void scr_tls_ca_certs_chk(const ScrDyn *type, const ScrStr *fence);
ScrSecureCtx *scr_secure_ctx_retain(ScrSecureCtx *c);
void scr_secure_ctx_release(ScrSecureCtx *c);
void *scr_secure_ctx_retain_v(void *p);
void scr_secure_ctx_release_v(void *p);
ScrNetServer *scr_https_create_server_sni(const char *cert, size_t cert_len, const char *key, size_t key_len, ScrClosure *sni_cb /*moves, nullable*/, void *answer_fn); /* +1 */
void scr_tls_sni_answer(ScrClosure *self, bool has_err, ScrSecureCtx *ctx /*moves, nullable*/);
#ifdef SCR_RC_AUDIT
long scr_secure_ctx_live_count(void);
#endif
/* ── node:tls, the CA-store introspection slice (scr_tls_ca.c — its own
* unit and link gate; NO mbedTLS, so a getCACertificates-only binary never
* builds the archive; native-toolchain.ts also compiles it whenever scr_tls.c does). The
* HOST roots (Windows' system certificate stores, the established bundle probe on
* POSIX) stand in for both Node's compiled-in Mozilla roots ('bundled',
* rootCertificates) and the platform store ('system') — the established
* SEMANTICS divergence, extended to introspection; 'extra' uses the
* NODE_EXTRA_CA_CERTS file captured before user code runs. Arrays are cached
* per type (+1 retained answers each call — Node's own caching, and the
* identity the suite pins with strictEqual). */
void scr_tls_ca_install(void); /* snapshots NODE_EXTRA_CA_CERTS + file bytes */
bool scr_tls_ca_extra_pem(const char **pem, size_t *len); /* borrowed launch snapshot */
#ifdef _WIN32
/* Enumerates Windows' trusted ROOT, intermediate CA, and TrustedPeople store
* locations, yielding only certificates whose effective Windows EKU policy
* permits TLS server authentication. DER is borrowed for the call. */
typedef void (*ScrTlsCaWindowsCertFn)(void *ctx, const unsigned char *der, size_t len);
bool scr_tls_ca_windows_cert_server_auth(const void *cert_context);
void scr_tls_ca_windows_certs(ScrTlsCaWindowsCertFn fn, void *ctx);
#endif
ScrArr *scr_tls_ca_get(ScrStr *type); /* +1; throws ERR_INVALID_ARG_VALUE on unknown types */
ScrArr *scr_tls_ca_root(void); /* +1; === getCACertificates("bundled") */
/* Replaces the 'default' set: entries filter to their PEM certificate
* blocks (deduped byte-exactly — Node dedupes by X509 identity); a
* non-empty array yielding no blocks throws Node's
* ERR_CRYPTO_OPERATION_FAILED and leaves the set unchanged. Borrows. */
void scr_tls_ca_set_default(ScrArr *certs);
/* scr_tls.c's anchor consult: true iff setDefaultCACertificates ran;
* *pem/*len then carry the concatenated NUL-terminated blocks (len 0 =
* the empty set — verification fails, Node's own consequence), and *gen
* a counter that bumps per set so the parsed chain re-parses on change. */
bool scr_tls_ca_default_override(const char **pem, size_t *len, uint64_t *gen);
/* ── node:http, the server slice (scr_http.c — compiled only when the
* program uses it; rides scr_net.c wholesale, design note atop the
* file). http.createServer returns an ORDINARY ScrNetServer (listen/
* close/address/'error' reuse the net surface); req/res are lean
* refcounted handles, listeners dropping at settlement like sockets. */
typedef struct ScrHttpReq ScrHttpReq;
typedef struct ScrHttpRes ScrHttpRes;
typedef void (*ScrHttpReqFn)(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res); /* both +1 */
typedef void (*ScrHttpConnectH2Fn)(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res); /* both +1 */
/* 'upgrade' listeners, both sides: (req-or-res, socket, head) — all +1.
* The parser steps aside (native reader cleared) and the socket is the
* listener's raw stream; `head` carries bytes read past the 101/request
* head. Requests with Connection: upgrade + an Upgrade header fire the
* server list INSTEAD of 'request'; a 101 response fires the client list
* INSTEAD of 'response'; either side with NO listener destroys the
* socket, Node's default. */
typedef void (*ScrHttpUpgradeFn)(ScrClosure *cb, ScrHttpReq *req, ScrNetSocket *sock, ScrBytes *head);
ScrHttpReq *scr_http_req_retain(ScrHttpReq *r);
void scr_http_req_release(ScrHttpReq *r);
void *scr_http_req_retain_v(void *p);
void scr_http_req_release_v(void *p);
ScrHttpRes *scr_http_res_retain(ScrHttpRes *r);
void scr_http_res_release(ScrHttpRes *r);
void *scr_http_res_retain_v(void *p);
void scr_http_res_release_v(void *p);
/* ── the h2 compat seam (scr_http.c ⇄ scr_http2.c) ─────────────────────
* Http2ServerRequest/Http2ServerResponse ARE the req/res handles above;
* scr_http2.c (linked only when the real h2 surface is used) installs
* the transport vtable — response write paths become HEADERS/DATA
* frames — and the request-registration hook that routes
* server.on("request", ...) on an h2-tagged server ctx to the h2
* request list. scr_http.c links WITHOUT scr_http2.c, so the seam is a
* pointer pair, never a hard symbol reference in that direction. */
typedef struct ScrHttpH2Ops {
void *(*retain)(void *stream);
void (*release)(void *stream);
/* Serialize + send the response HEADERS frame: :status first, then the
* set headers (connection-specific names dropped, h2's rule), then the
* implicit date unless add_date is false (res.sendDate = false). */
void (*respond)(void *stream, double status, ScrStr *const *names,
ScrStr *const *values, size_t nheaders, bool add_date);
void (*write)(void *stream, const char *data, size_t n);
void (*end)(void *stream, const char *data, size_t n); /* DATA + END_STREAM */
void (*destroy)(void *stream); /* res.destroy: RST */
} ScrHttpH2Ops;
void scr_http_set_h2_ops(const ScrHttpH2Ops *ops);
void scr_http_set_h2_request_hook(void (*hook)(void *h2ctx, ScrClosure *cb /*moves*/,
void *fn, bool once));
void scr_http_set_h2_connect_hook(void (*hook)(void *h2ctx, ScrClosure *cb /*moves*/,
void *h1_fn, void *h2_fn, bool once));
void scr_http_set_h2_upgrade_hook(void (*hook)(void *h2ctx, ScrClosure *cb /*moves*/,
void *fn, bool once));
/* The compat pair, built by scr_http2.c per server stream. */
ScrHttpReq *scr_http_h2_req_new(ScrNetSocket *sock /*borrowed, nullable*/,
void *stream /*borrowed, nullable*/); /* +1 */
void *scr_http_req_h2_stream(ScrHttpReq *r); /* +1 or NULL */
void *scr_http_req_h2_stream_or_throw(ScrHttpReq *r, ScrStr *member /*borrowed*/); /* +1 */
void scr_http_h2_req_line(ScrHttpReq *r, ScrStr *method /*borrowed, nullable*/,
ScrStr *url /*borrowed, nullable*/);
void scr_http_h2_req_header(ScrHttpReq *r, ScrStr *name /*borrowed*/, ScrStr *value /*borrowed*/);
void scr_http_h2_req_data(ScrHttpReq *r, const char *data, size_t n);
void scr_http_h2_req_end(ScrHttpReq *r);
void scr_http_h2_req_aborted(ScrHttpReq *r);
void scr_http_h2_req_close(ScrHttpReq *r);
ScrHttpRes *scr_http_h2_res_new(ScrNetSocket *sock /*borrowed, nullable*/,
void *stream /*borrowed*/); /* +1 */
void scr_http_h2_res_close(ScrHttpRes *r);
bool scr_http_h2_res_finished(ScrHttpRes *r);
void scr_http_req_on_aborted(ScrHttpReq *r, ScrClosure *cb /*moves*/, bool once);
ScrStr *scr_http_req_http_version(ScrHttpReq *r); /* +1 — "1.0"/"1.1"/"2.0" */
double scr_http_req_http_version_major(ScrHttpReq *r);
double scr_http_req_http_version_minor(ScrHttpReq *r);
bool scr_http_req_aborted_flag(ScrHttpReq *r);
bool scr_http_req_complete(ScrHttpReq *r);
/* Registers the httpReq/httpRes handle-dispatch ops (SCR_DYNH_HTTP_REQ /
* SCR_DYNH_HTTP_RES) — emitted main() calls this exactly when the http
* unit is linked (the scr_net_dyn_install story). */
void scr_http_dyn_install(void);
void scr_http_validate_header_name(ScrStr *name /*borrowed*/, ScrStr *label /*borrowed*/);
void scr_http_validate_header_value(ScrStr *name /*borrowed*/, const ScrDyn *value /*borrowed*/);
ScrDyn *scr_http_status_codes(void); /* +1, shared mutable Node v24 table */
ScrArr *scr_http_methods(void); /* +1, shared mutable Node v24 array */
ScrNetServer *scr_http_create_server(ScrClosure *handler /*moves, nullable*/, ScrHttpReqFn fn); /* +1 */
/* The unguarded h2-only stream call: throws Node's exact catchable
* TypeError (member read on undefined). Borrows; never returns. */
void scr_http2_stream_undef_call(ScrStr *member);
/* Late 'request' listener install (server.on/once("request", ...)) — the
* http2 allowHTTP1 server's handler route (created handler-less), and
* http.Server's 'request' event. Listeners resolve at REQUEST time (the
* emit), Node's dispatch. On a server with no HTTP parser (a plain net
* server) the closure releases unregistered: 'request' never fires
* there, like Node. */
void scr_http_server_on_request(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrHttpReqFn fn, bool once);
void scr_http1_ctx_on_request(void *ctx, ScrClosure *cb /*moves*/, ScrHttpReqFn fn, bool once,
ScrNetOnce *shared_once /*moves, nullable*/);
void scr_http1_ctx_on_connect(void *ctx, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn, bool once,
ScrNetOnce *shared_once /*moves, nullable*/);
void scr_http1_ctx_on_upgrade(void *ctx, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn, bool once);
void scr_http1_ctx_settle(void *ctx);
ScrStr *scr_http_req_url(ScrHttpReq *r); /* +1 */
ScrStr *scr_http_req_method(ScrHttpReq *r); /* +1 */
ScrStr *scr_http_req_header(ScrHttpReq *r, ScrStr *name /*borrowed*/); /* +1 or NULL (undefined arm) */
ScrStr *scr_http_req_trailer(ScrHttpReq *r, ScrStr *name /*borrowed*/); /* +1 or NULL */
ScrArr *scr_http_req_header_values(ScrHttpReq *r, ScrStr *name /*borrowed*/); /* +1 or NULL */
ScrArr *scr_http_req_trailer_values(ScrHttpReq *r, ScrStr *name /*borrowed*/); /* +1 or NULL */
void scr_http_req_on_data(ScrHttpReq *r, ScrClosure *cb /*moves*/, ScrNetDataFn fn, bool once);
/* req.pipe(dest): the IncomingMessage body streams into the destination;
* natural end ends it (Node's pipe default). Declared below the client
* typedef for the ClientRequest leg. */
void scr_http_req_pipe_res(ScrHttpReq *r, ScrHttpRes *dst /*borrowed*/);
void scr_http_req_pipe_sock(ScrHttpReq *r, ScrNetSocket *dst /*borrowed*/);
void scr_http_req_on_end(ScrHttpReq *r, ScrClosure *cb /*moves*/, bool once);
void scr_http_res_set_header(ScrHttpRes *r, ScrStr *name /*borrowed*/, ScrStr *value /*borrowed*/);
void scr_http_res_write_head(ScrHttpRes *r, double status);
void scr_http_res_write_head_n(ScrHttpRes *r, double status, ScrArr *names /*borrowed*/, ScrArr *values /*borrowed*/);
void scr_http_res_write_continue(ScrHttpRes *r);
void scr_http_res_write_processing(ScrHttpRes *r);
void scr_http_res_write_early_hints(ScrHttpRes *r, ScrArr *pairs /*borrowed: [name, value, ...] */);
/* writeHead with a CHECKED-DYNAMIC headers object (borrowed): OBJ
* entries setHeader in insertion order (string/number values), then the
* head goes out; undefined/null = the plain head; may throw. */
void scr_http_res_write_head_dyn(ScrHttpRes *r, double status, const ScrDyn *headers);
void scr_http_res_write_str(ScrHttpRes *r, ScrStr *data /*borrowed*/);
void scr_http_res_write_bytes(ScrHttpRes *r, ScrBytes *data /*borrowed*/);
void scr_http_res_end(ScrHttpRes *r);
/* socket.pipe(res): raw source chunks become response body writes; EOF
* ends the response (the extended-CONNECT bridge leg). Borrows both. */
void scr_http_sock_pipe_res(ScrNetSocket *src, ScrHttpRes *dst /*borrowed*/);
void scr_http_res_end_str(ScrHttpRes *r, ScrStr *data /*borrowed*/);
void scr_http_res_end_bytes(ScrHttpRes *r, ScrBytes *data /*borrowed*/);
void scr_http_res_add_trailers(ScrHttpRes *r, ScrArr *pairs /*borrowed: [name, value, ...] */);
/* Checked-dynamic chunk forms (the scr_net_sock_write_dynv story). */
void scr_http_res_write_dynv(ScrHttpRes *r, const ScrDyn *d /*borrowed*/);
void scr_http_res_end_dynv(ScrHttpRes *r, const ScrDyn *d /*borrowed*/);
bool scr_http_res_headers_sent(ScrHttpRes *r);
bool scr_http_res_writable_ended(ScrHttpRes *r);
bool scr_http_res_send_date(ScrHttpRes *r);
void scr_http_res_set_send_date(ScrHttpRes *r, bool value);
bool scr_http_res_strict_content_length(ScrHttpRes *r);
void scr_http_res_set_strict_content_length(ScrHttpRes *r, bool value);
ScrHttpReq *scr_http_res_request(ScrHttpRes *r); /* +1 */
ScrNetSocket *scr_http_res_socket(ScrHttpRes *r); /* +1 or NULL after finish */
bool scr_http_res_writable_finished(ScrHttpRes *r);
/* The res member surface: statusCode (200 until assigned; inert once the
* head went out), statusMessage (the reason phrase — assigned value, or
* the code's default), the header CRUD trio, and end(cb)'s finish slot
* (fires deferred once the body went out — the res 'close' precedent). */
double scr_http_res_status_get(ScrHttpRes *r);
void scr_http_res_status_set(ScrHttpRes *r, double status);
ScrStr *scr_http_res_status_msg_get(ScrHttpRes *r); /* +1 */
void scr_http_res_status_msg_set(ScrHttpRes *r, ScrStr *msg /*borrowed*/);
ScrStr *scr_http_res_get_header(ScrHttpRes *r, ScrStr *name /*borrowed*/); /* +1 or NULL */
ScrArr *scr_http_res_get_header_names(ScrHttpRes *r); /* +1 */
ScrArr *scr_http_res_get_raw_header_names(ScrHttpRes *r); /* +1 */
ScrDyn *scr_http_res_get_headers(ScrHttpRes *r); /* +1 */
bool scr_http_res_has_header_named(ScrHttpRes *r, ScrStr *name /*borrowed*/);
void scr_http_res_remove_header(ScrHttpRes *r, ScrStr *name /*borrowed*/);
void scr_http_res_on_finish(ScrHttpRes *r, ScrClosure *cb /*moves*/);
/* createServer({ joinDuplicateHeaders: true }): repeated request-header
* names read back joined ", " (Node's option; the default keeps first). */
void scr_http_server_join_duplicate_headers(ScrNetServer *s);
/* createServer({ requireHostHeader: false }): allow HTTP/1.1 without Host. */
void scr_http_server_allow_missing_host_header(ScrNetServer *s);
void scr_http_handler_thunk0(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res);
void scr_http_handler_thunk1(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res);
void scr_http_handler_thunk2(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res);
/* server.on("connect", ...) — fired instead of 'request' for CONNECT.
* HTTP/1 uses (req, socket, head); HTTP/2 compatibility uses (req, res). */
void scr_http_server_on_connect(ScrNetServer *s, ScrClosure *cb /*moves*/,
ScrHttpUpgradeFn h1_fn, ScrHttpConnectH2Fn h2_fn,
bool once);
void scr_http_server_on_upgrade(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn, bool once);
ScrArr *scr_http_req_raw_headers(ScrHttpReq *r); /* +1 — [name, value, ...], arrival order/case */
ScrArr *scr_http_req_header_pairs(ScrHttpReq *r); /* +1 — lowercased names, the headers snapshot */
ScrArr *scr_http_req_raw_trailers(ScrHttpReq *r); /* +1 — wire order/case after end */
ScrArr *scr_http_req_trailer_pairs(ScrHttpReq *r); /* +1 — lowercased, joined snapshot after end */
ScrStr *scr_http_req_status_message(ScrHttpReq *r); /* +1 or NULL (undefined arm: server request) */
void scr_http_upgrade_thunk0(ScrClosure *cb, ScrHttpReq *req, ScrNetSocket *sock, ScrBytes *head);
void scr_http_upgrade_thunk1(ScrClosure *cb, ScrHttpReq *req, ScrNetSocket *sock, ScrBytes *head);
void scr_http_upgrade_thunk2(ScrClosure *cb, ScrHttpReq *req, ScrNetSocket *sock, ScrBytes *head);
void scr_http_upgrade_thunk3(ScrClosure *cb, ScrHttpReq *req, ScrNetSocket *sock, ScrBytes *head);
/* The member follow-ups (statusCode / socket / resume / destroy / the
* error+close listener slots on requests; destroy / close / the
* flat-pairs writeHead on responses). */
double scr_http_req_status(ScrHttpReq *r); /* < 0 = the undefined arm (server request) */
ScrNetSocket *scr_http_req_socket(ScrHttpReq *r); /* +1 */
void scr_http_req_resume(ScrHttpReq *r);
/* pause()/resume() hold and drain 'data'/'end' delivery (the parser keeps
* consuming; the drain rides the emit queue); setTimeout delegates to the
* socket's idle timer; the flags back req.destroyed/req.readable. */
void scr_http_req_pause(ScrHttpReq *r);
void scr_http_req_set_timeout(ScrHttpReq *r, double ms, ScrClosure *cb /*moves, nullable*/);
void scr_http_req_set_timeout_plain(ScrHttpReq *r, double ms);
bool scr_http_req_destroyed_flag(ScrHttpReq *r);
bool scr_http_req_readable(ScrHttpReq *r);
/* flushHeaders/cork/uncork/writableCorked, the res.req backref, the
* socket-delegated setTimeout, and write(chunk, cb)'s deferred callback. */
void scr_http_res_flush_headers(ScrHttpRes *r);
void scr_http_res_cork(ScrHttpRes *r);
void scr_http_res_uncork(ScrHttpRes *r);
double scr_http_res_writable_corked(ScrHttpRes *r);
bool scr_http_res_destroyed_flag(ScrHttpRes *r);
void scr_http_res_set_req(ScrHttpRes *r, ScrHttpReq *req /*borrowed, nullable*/);
void scr_http_res_set_timeout(ScrHttpRes *r, double ms, ScrClosure *cb /*moves, nullable*/);
void scr_http_res_set_timeout_plain(ScrHttpRes *r, double ms);
void scr_http_res_on_write_flush(ScrHttpRes *r, ScrClosure *cb /*moves*/);
/* req.setEncoding(enc) — the socket twin's contract; may throw. */
void scr_http_req_set_encoding(ScrHttpReq *r, ScrStr *enc /*borrowed*/);
void scr_http_req_destroy(ScrHttpReq *r);
void scr_http_req_on_error(ScrHttpReq *r, ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once);
void scr_http_req_on_close(ScrHttpReq *r, ScrClosure *cb /*moves*/, bool once);
void scr_http_res_destroy(ScrHttpRes *r);
void scr_http_res_on_close(ScrHttpRes *r, ScrClosure *cb /*moves*/, bool once);
void scr_http_res_write_head_pairs(ScrHttpRes *r, double status, ScrArr *pairs /*borrowed*/);
/* node:http, the CLIENT slice (http.request/http.get over the net client
* machinery — one dialed connection per request, no agent pooling; the
* wire head is Node's exactly). The response arrives as a ScrHttpReq. */
typedef struct ScrHttpClientReq ScrHttpClientReq;
typedef void (*ScrHttpRespFn)(ScrClosure *cb, ScrHttpReq *res); /* res +1 */
ScrHttpClientReq *scr_http_client_retain(ScrHttpClientReq *c);
void scr_http_client_release(ScrHttpClientReq *c);
void *scr_http_client_retain_v(void *p);
void scr_http_client_release_v(void *p);
ScrHttpClientReq *scr_http_request(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, ScrClosure *cb /*moves, nullable*/,
ScrHttpRespFn fn); /* +1 */
/* The createConnection form: conn_cb (a `() => net.Socket` closure) runs
* once, synchronously, and supplies the exchange's socket. */
ScrHttpClientReq *scr_http_request_conn(ScrClosure *conn_cb /*moves*/, ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/, double timeout_ms, ScrArr *header_pairs /*borrowed*/, bool auto_end, ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */
void scr_http_client_write_str(ScrHttpClientReq *c, ScrStr *data /*borrowed*/);
void scr_http_client_write_bytes(ScrHttpClientReq *c, ScrBytes *data /*borrowed*/);
void scr_http_client_end(ScrHttpClientReq *c);
void scr_http_client_end_str(ScrHttpClientReq *c, ScrStr *data /*borrowed*/);
void scr_http_client_end_bytes(ScrHttpClientReq *c, ScrBytes *data /*borrowed*/);
void scr_http_client_flush_headers(ScrHttpClientReq *c);
void scr_http_client_set_header(ScrHttpClientReq *c, ScrStr *name /*borrowed*/, ScrStr *value /*borrowed*/);
ScrStr *scr_http_client_get_header(ScrHttpClientReq *c, ScrStr *name /*borrowed*/); /* +1 or NULL */
bool scr_http_client_has_header_named(ScrHttpClientReq *c, ScrStr *name /*borrowed*/);
void scr_http_client_remove_header(ScrHttpClientReq *c, ScrStr *name /*borrowed*/);
ScrArr *scr_http_client_get_header_names(ScrHttpClientReq *c); /* +1 */
ScrArr *scr_http_client_get_raw_header_names(ScrHttpClientReq *c); /* +1 */
ScrDyn *scr_http_client_get_headers(ScrHttpClientReq *c); /* +1 */
ScrStr *scr_http_client_method(ScrHttpClientReq *c); /* +1 */
ScrStr *scr_http_client_path(ScrHttpClientReq *c); /* +1 */
ScrStr *scr_http_client_host(ScrHttpClientReq *c); /* +1 */
ScrStr *scr_http_client_protocol(ScrHttpClientReq *c); /* +1 */
bool scr_http_client_headers_sent(ScrHttpClientReq *c);
bool scr_http_client_writable_ended(ScrHttpClientReq *c);
bool scr_http_client_writable_finished(ScrHttpClientReq *c);
ScrNetSocket *scr_http_client_socket(ScrHttpClientReq *c); /* +1 */
bool scr_http_client_reused_socket(ScrHttpClientReq *c);
void scr_http_client_set_nodelay(ScrHttpClientReq *c, bool enable);
void scr_http_client_set_socket_keepalive(ScrHttpClientReq *c, bool enable, double delay_ms);
void scr_http_client_set_timeout_cb(ScrHttpClientReq *c, double ms, ScrClosure *cb /*moves*/);
void scr_http_client_add_trailers(ScrHttpClientReq *c, ScrArr *pairs /*borrowed*/);
void scr_http_client_cork(ScrHttpClientReq *c);
void scr_http_client_uncork(ScrHttpClientReq *c);
double scr_http_client_writable_corked(ScrHttpClientReq *c);
/* Checked-dynamic chunk forms (the scr_net_sock_write_dynv story). */
void scr_http_client_write_dynv(ScrHttpClientReq *c, const ScrDyn *d /*borrowed*/);
void scr_http_client_end_dynv(ScrHttpClientReq *c, const ScrDyn *d /*borrowed*/);
void scr_http_client_set_timeout(ScrHttpClientReq *c, double ms);
void scr_http_client_destroy(ScrHttpClientReq *c);
void scr_http_client_abort(ScrHttpClientReq *c);
bool scr_http_client_aborted(ScrHttpClientReq *c);
bool scr_http_client_destroyed(ScrHttpClientReq *c);
void scr_http_client_on_response(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, ScrHttpRespFn fn, bool once);
void scr_http_client_on_socket(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, ScrNetConnFn fn, bool once);
void scr_http_client_on_finish(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, bool once);
void scr_http_client_on_abort(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, bool once);
void scr_http_client_on_error(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once);
void scr_http_client_on_timeout(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, bool once);
void scr_http_client_on_close(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, bool once);
void scr_http_client_on_upgrade(ScrHttpClientReq *c, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn, bool once);
void scr_http_req_pipe_client(ScrHttpReq *r, ScrHttpClientReq *dst /*borrowed*/);
void scr_http_resp_thunk0(ScrClosure *cb, ScrHttpReq *res);
void scr_http_resp_thunk_res(ScrClosure *cb, ScrHttpReq *res);
/* The transport-parameterized client constructor (scr_tls.c's https
* entry): `wrap` (nullable) installs a transport on the freshly dialed
* socket before any bytes go out (they buffer until the handshake
* establishes), and `default_port` is the port the Host header omits
* (80 for http, 443 for https — Node's rule). scr_http_request is this
* with no wrap and port 80. */
ScrHttpClientReq *scr_http_request_ex(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, ScrClosure *cb /*moves, nullable*/,
ScrHttpRespFn fn, int default_port,
void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx); /* +1 */
/* The URL-string client form (http.get('http://host:port/path')): parses
* through the WHATWG unit; throws catchably on an unparsable input
* ("Invalid URL") or a non-http scheme (ERR_INVALID_PROTOCOL) and
* returns NULL with the exception pending. cb MOVES (nullable). */
ScrHttpClientReq *scr_http_request_url(ScrStr *url /*borrowed*/, ScrStr *method /*borrowed*/,
bool auto_end, ScrClosure *cb /*moves, nullable*/,
ScrHttpRespFn fn); /* +1 */
/* Its parse half, shared with scr_tls.c's https spelling: the scheme is
* checked against `secure` (the calling module), so a mismatch is Node's
* ERR_INVALID_PROTOCOL rather than a silent upgrade. false = the
* exception is pending and no out-parameter was written. */
bool scr_http_url_parts(ScrStr *url /*borrowed*/, bool secure, ScrStr **host_out /*+1*/,
double *port_out, ScrStr **path_out /*+1*/);
/* ── the http Agent (new http.Agent(opts) — option surface, getName, and
* the maxSockets queue over one-dial-per-request connections; keep-alive
* POOLING is not modeled: keepAlive: true fences at construction).
* agent_new answers the SCR_DYNH_HTTP_AGENT handle (+1 dyn) or throws.
* max_sockets/max_free arrive < 0 for "unset" (Infinity / 256);
* timeout_ms < 0 = no idle timer. request_agent_ex threads the agent dyn
* (undefined/null = the default path; false = one-shot with Connection:
* close; an Agent handle = queue accounting); port < 0 means "no port
* option" — the agent's (settable) defaultPort, then the scheme's. */
ScrDyn *scr_http_agent_new(bool secure, bool keep_alive, double ka_msecs,
double max_sockets, double max_free, double timeout_ms,
double port /* < 0 = unset: the option-merge default */); /* +1 */
ScrHttpClientReq *scr_http_request_agent_ex(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, const ScrDyn *agent /*borrowed*/,
ScrClosure *cb /*moves, nullable*/,
ScrHttpRespFn fn, int default_port,
void (*wrap)(ScrNetSocket *, void *), void *wrap_ctx); /* +1 */
ScrHttpClientReq *scr_http_request_agent(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, const ScrDyn *agent /*borrowed*/,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */
#ifdef SCR_RC_AUDIT
long scr_http_live_count(void);
#endif
/* ── node:http2 (scr_http2.c — REAL HTTP/2: h2c server + client over
* the net loop, with an in-tree RFC 7540 frame codec and RFC 7541 HPACK;
* the design note atop scr_http2.c has the full story). Sessions and
* streams are refcounted handles like the net pair; 'stream'/'response'
* payloads cross as flat [name, value, ...] pairs arrays and the EMITTED
* adapter closure builds the program-side headers record (the response
* :status rides separately as a number — no string→number conversion
* exists program-side). The srv-ctx `proto` tags (SCR_NET_PROTO_*) keep
* the two parser families apart on the shared http_ctx alias slot. */
enum { SCR_NET_PROTO_HTTP1 = 1, SCR_NET_PROTO_H2 = 2 };
typedef struct ScrH2Session ScrH2Session;
typedef struct ScrH2Stream ScrH2Stream;
typedef void (*ScrH2SessionErrorFn)(ScrClosure *cb, ScrStr *msg,
ScrH2Session *session); /* session +1 */
typedef void (*ScrH2StreamFn)(ScrClosure *cb, ScrH2Stream *st, ScrArr *pairs, double flags); /* st, pairs +1 */
typedef void (*ScrH2RespFn)(ScrClosure *cb, ScrArr *pairs, double status, double flags); /* pairs +1 */
typedef void (*ScrH2SessionFn)(ScrClosure *cb, ScrH2Session *sess); /* sess +1 */
typedef void (*ScrH2ConnectFn)(ScrClosure *cb, ScrH2Session *sess, ScrNetSocket *sock); /* both +1; sock nullable */
typedef void (*ScrH2GoawayFn)(ScrClosure *cb, double code, double last);
typedef void (*ScrH2SettingsFn)(ScrClosure *cb, ScrDyn *settings /*+1*/);
ScrH2Session *scr_http2_session_retain(ScrH2Session *s);
void scr_http2_session_release(ScrH2Session *s);
void *scr_http2_session_retain_v(void *p);
void scr_http2_session_release_v(void *p);
ScrH2Stream *scr_http2_stream_retain(ScrH2Stream *st);
void scr_http2_stream_release(ScrH2Stream *st);
void *scr_http2_stream_retain_v(void *p);
void scr_http2_stream_release_v(void *p);
/* server (an ordinary net server handle; listen/close/address/'error'
* ride the net surface — the scr_http.c shape) */
ScrNetServer *scr_http2_create_server(void); /* +1 */
/* createServer(handler): the eager (req, res) COMPAT handler — the first
* 'request' listener (Http2ServerRequest/Response over h2 streams). */
ScrNetServer *scr_http2_create_server_req(ScrClosure *handler /*moves*/, ScrHttpReqFn fn); /* +1 */
/* The REAL h2-over-TLS server (http2.createSecureServer({ cert, key })):
* the same session machinery behind an mbedTLS handshake whose ALPN
* advertises h2 alone — protocol-identical after establishment. */
ScrNetServer *scr_http2_create_secure_server(const char *cert, size_t cert_len, const char *key, size_t key_len, bool enable_connect_protocol); /* +1 */
ScrNetServer *scr_http2_create_secure_server_allow_http1(const char *cert, size_t cert_len, const char *key, size_t key_len, ScrClosure *handler /*moves, nullable*/, ScrHttpReqFn fn, ScrClosure *sni_cb /*moves, nullable*/, void *sni_answer_fn, bool enable_connect_protocol); /* +1 */
/* createSecureServer(options, handler): the eager COMPAT handler as the
* first 'request' listener over the ALPN=h2 server (the createServerReq
* route, secured). */
ScrNetServer *scr_http2_create_secure_server_req(const char *cert, size_t cert_len, const char *key, size_t key_len, ScrClosure *handler /*moves, nullable*/, ScrHttpReqFn fn, bool enable_connect_protocol); /* +1 */
/* createSecureServer with a RUNTIME options record (the divergence-66
* stance): allowHTTP1 picks the flavor at runtime, cert/key ride the
* shared TLS server walk (out-of-bounds members throw the catchable
* fence), h2 session-tuning keys drop like the literal walk ignores
* them. +1, or NULL with the exception pending. */
ScrNetServer *scr_http2_create_secure_server_dyn(const struct ScrDyn *opts /*borrowed*/, ScrClosure *handler /*moves, nullable*/, ScrHttpReqFn fn);
/* The TLS server-options walk (scr_tls.c) — runtime-internal, shared
* with scr_http2.c's createSecureServerDyn. */
bool scr_tls_srv_opts_walk(const struct ScrDyn *opts, const char *api, ScrBytes **cert_out, ScrBytes **key_out);
void scr_http2_server_on_stream(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrH2StreamFn fn, bool once);
void scr_http2_server_on_session(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrH2SessionFn fn, bool once);
/* client — h2c prior knowledge for http authorities, the TLS transport
* with ALPN ["h2"] for https ones (ca len 0 = the system anchors;
* reject_unauthorized false disables the verify gate — the local-CA
* self-connect shapes); throws catchably on bad input */
ScrH2Session *scr_http2_connect(ScrStr *url /*borrowed*/, bool reject_unauthorized,
const char *ca /*borrowed, len 0 = none*/, size_t ca_len,
ScrClosure *cb /*moves, nullable*/,
ScrH2ConnectFn fn); /* +1 */
/* end_stream: -1 = the method's payload-meaningless default, 0/1 explicit */
ScrH2Stream *scr_http2_session_request(ScrH2Session *s, ScrArr *pairs /*borrowed, nullable*/,
double end_stream); /* +1; may throw */
/* session */
void scr_http2_session_close(ScrH2Session *s, ScrClosure *cb /*moves, nullable*/);
void scr_http2_session_destroy(ScrH2Session *s);
void scr_http2_session_on_close(ScrH2Session *s, ScrClosure *cb /*moves*/, bool once);
void scr_http2_session_on_error(ScrH2Session *s, ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once);
void scr_http2_server_on_session_error(ScrNetServer *s, ScrClosure *cb /*moves*/,
ScrH2SessionErrorFn fn, bool once);
void scr_http2_session_error_thunk0(ScrClosure *cb, ScrStr *msg, ScrH2Session *session);
void scr_http2_session_error_thunk1(ScrClosure *cb, ScrStr *msg, ScrH2Session *session);
void scr_http2_session_error_thunk2(ScrClosure *cb, ScrStr *msg, ScrH2Session *session);
void scr_http2_session_on_connect(ScrH2Session *s, ScrClosure *cb /*moves*/, ScrH2ConnectFn fn, bool once);
void scr_http2_session_on_stream(ScrH2Session *s, ScrClosure *cb /*moves*/, ScrH2StreamFn fn, bool once);
void scr_http2_session_on_goaway(ScrH2Session *s, ScrClosure *cb /*moves*/, ScrH2GoawayFn fn, bool once);
/* The settings surface: session.settings(obj[, cb]) sends a SETTINGS
* frame; local/remote listener lists fire with the settings record on
* ACK/receipt; the property reads answer the tracked records. */
void scr_http2_session_settings(ScrH2Session *s, const ScrDyn *settings /*borrowed, nullable*/,
ScrClosure *cb /*moves, nullable*/, ScrH2SettingsFn cbfn);
void scr_http2_session_on_settings(ScrH2Session *s, ScrClosure *cb /*moves*/, ScrH2SettingsFn fn,
bool once, bool local);
ScrDyn *scr_http2_session_settings_get(ScrH2Session *s, bool local); /* +1 */
bool scr_http2_session_pending_settings_ack(ScrH2Session *s);
ScrDyn *scr_http2_get_default_settings(void); /* +1 */
void scr_http2_settings_thunk(ScrClosure *cb, ScrDyn *settings);
void scr_http2_settings_thunk0(ScrClosure *cb, ScrDyn *settings);
void scr_http2_session_settings_dyncb(ScrH2Session *s, const ScrDyn *settings /*borrowed*/,
const ScrDyn *cb /*borrowed*/);
void scr_http2_session_on_settings_dyn(ScrH2Session *s, const ScrDyn *cb /*borrowed*/,
bool once, bool local);
bool scr_http2_session_closed(ScrH2Session *s);
bool scr_http2_session_destroyed(ScrH2Session *s);
bool scr_http2_session_encrypted(ScrH2Session *s);
double scr_http2_session_type(ScrH2Session *s); /* 0 server / 1 client */
ScrStr *scr_http2_session_alpn(ScrH2Session *s); /* +1 */
ScrNetSocket *scr_http2_session_socket(ScrH2Session *s); /* +1, NULL after teardown */
/* stream */
void scr_http2_stream_respond(ScrH2Stream *st, ScrArr *pairs /*borrowed, nullable*/, bool end_stream);
void scr_http2_stream_write_str(ScrH2Stream *st, ScrStr *data /*borrowed*/);
void scr_http2_stream_write_bytes(ScrH2Stream *st, ScrBytes *data /*borrowed*/);
void scr_http2_stream_end(ScrH2Stream *st);
void scr_http2_stream_end_str(ScrH2Stream *st, ScrStr *data /*borrowed*/);
void scr_http2_stream_end_bytes(ScrH2Stream *st, ScrBytes *data /*borrowed*/);
void scr_http2_stream_close(ScrH2Stream *st, double code, ScrClosure *cb /*moves, nullable*/);
void scr_http2_stream_destroy(ScrH2Stream *st);
void scr_http2_stream_set_encoding(ScrH2Stream *st, ScrStr *enc /*borrowed*/); /* may throw */
ScrH2Stream *scr_http2_stream_set_encoding_ret(ScrH2Stream *st, ScrStr *enc /*borrowed*/); /* +1; may throw */
void scr_http2_stream_resume(ScrH2Stream *st);
void scr_http2_stream_pause(ScrH2Stream *st);
void scr_http2_stream_on_data(ScrH2Stream *st, ScrClosure *cb /*moves*/, ScrNetDataFn fn, bool once);
void scr_http2_stream_on_end(ScrH2Stream *st, ScrClosure *cb /*moves*/, bool once);
void scr_http2_stream_on_close(ScrH2Stream *st, ScrClosure *cb /*moves*/, bool once);
void scr_http2_stream_on_aborted(ScrH2Stream *st, ScrClosure *cb /*moves*/, bool once);
void scr_http2_stream_on_error(ScrH2Stream *st, ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once);
void scr_http2_stream_on_response(ScrH2Stream *st, ScrClosure *cb /*moves*/, ScrH2RespFn fn, bool once);
double scr_http2_stream_id(ScrH2Stream *st);
double scr_http2_stream_rst_code(ScrH2Stream *st);
bool scr_http2_stream_destroyed(ScrH2Stream *st);
bool scr_http2_stream_closed(ScrH2Stream *st);
bool scr_http2_stream_aborted(ScrH2Stream *st);
bool scr_http2_stream_pending(ScrH2Stream *st);
ScrH2Session *scr_http2_stream_session(ScrH2Stream *st); /* +1 */
/* the emitted-adapter thunks (arity + record-building live program-side) */
void scr_http2_stream_thunk(ScrClosure *cb, ScrH2Stream *st, ScrArr *pairs, double flags);
void scr_http2_resp_thunk(ScrClosure *cb, ScrArr *pairs, double status, double flags);
void scr_http2_session_thunk(ScrClosure *cb, ScrH2Session *sess);
void scr_http2_session_thunk0(ScrClosure *cb, ScrH2Session *sess);
void scr_http2_connect_thunk2(ScrClosure *cb, ScrH2Session *sess, ScrNetSocket *sock);
void scr_http2_connect_thunk1(ScrClosure *cb, ScrH2Session *sess, ScrNetSocket *sock);
void scr_http2_connect_thunk0(ScrClosure *cb, ScrH2Session *sess, ScrNetSocket *sock);
void scr_http2_goaway_thunk2(ScrClosure *cb, double code, double last);
void scr_http2_goaway_thunk1(ScrClosure *cb, double code, double last);
void scr_http2_goaway_thunk0(ScrClosure *cb, double code, double last);
void scr_net_sock_set_native_established(ScrNetSocket *s, ScrNetNativeEventFn fn);
/* Registers the h2 session/stream dyn handle ops (the emitted main()
* calls it when the unit is linked — the scr_net_dyn_install story). */
void scr_http2_dyn_install(void);
#ifdef SCR_RC_AUDIT
long scr_http2_live_count(void);
#endif
/* node:https (scr_tls.c — the http server/client over the TLS
* transport; see the tls block above for the ownership story). */
ScrNetServer *scr_https_create_server(const char *cert, size_t cert_len, const char *key, size_t key_len, ScrClosure *handler /*moves*/, ScrHttpReqFn fn); /* +1 */
/* The runtime-options-record twin (the tls block's divergence-66 walk). */
ScrNetServer *scr_https_create_server_dyn(const struct ScrDyn *opts /*borrowed*/, ScrClosure *handler /*moves, nullable*/, ScrHttpReqFn fn); /* +1 or NULL+pending */
ScrHttpClientReq *scr_https_request(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, bool reject_unauthorized,
const char *ca /*borrowed, len 0 = none*/, size_t ca_len,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */
/* The URL-string form (https.get('https://host/path')): scr_http.c's
* shared parse, then the options form's dial with Node's no-options
* defaults (verification on, the default trust anchors). Throws
* catchably on an unparsable input or a non-https scheme. */
ScrHttpClientReq *scr_https_request_url(ScrStr *url /*borrowed*/, ScrStr *method /*borrowed*/,
bool auto_end, ScrClosure *cb /*moves, nullable*/,
ScrHttpRespFn fn); /* +1 */
/* The agent-threaded twin (the scr_http_request_agent story over TLS). */
ScrHttpClientReq *scr_https_request_agent(ScrStr *host /*borrowed*/, double port,
ScrStr *path /*borrowed*/, ScrStr *method /*borrowed*/,
double timeout_ms, ScrArr *header_pairs /*borrowed*/,
bool auto_end, bool reject_unauthorized,
const char *ca /*borrowed, len 0 = none*/, size_t ca_len,
const struct ScrDyn *agent /*borrowed*/,
ScrClosure *cb /*moves, nullable*/, ScrHttpRespFn fn); /* +1 */
/* The fetch unit's https leg (defined in scr_tls.c): a client transport
* context (SNI/verify against the URL hostname — the DIALED address may
* be a resolved IP) plus the wrap hook scr_http_request_ex installs on
* the fresh socket. The ctx moves into the transport at wrap time. */
void *scr_tls_fetch_client_ctx(ScrStr *host /*borrowed*/, bool reject_unauthorized);
void scr_tls_fetch_client_wrap(ScrNetSocket *sock, void *ctx);
/* The loop-side registration (scr_async.c, always linked): `pending`
* joins the exhaustion test (a live handle keeps the process alive, like
* Node's active TCP handles), `dispatch` runs at every turn top like the
* events hook, and `pollfd` exposes the unit's poller fd so the idle
* poll(2) sleep wakes on socket readiness (-1 = no fd yet). */
void scr_loop_set_net(bool (*pending)(void), void (*dispatch)(void), int (*pollfd)(void));
/* True when fibers are queued on the microtask ready queue — dispatch
* hooks use it to yield between event batches so promise jobs interleave
* (net's sweep/drain alternation). */
bool scr_loop_has_ready(void);
/* ── node:dgram + node:dns (scr_dgram.c — compiled only when the program
* uses either; design note atop the file). One lean refcounted handle
* kind (the netSocket ownership story: listeners MOVE in and drop at
* settlement); dns.lookup rides the same unit — getaddrinfo runs AT CALL
* TIME and the callback defers to the next loop turn. Self-contained: no
* symbol here requires scr_net.c to link. */
typedef struct ScrDgramSocket ScrDgramSocket;
/* The message adapter: msg and the rinfo parts arrive BORROWED (multiple
* listeners see the same datagram); adapters retain what they keep. */
typedef void (*ScrDgramMsgFn)(ScrClosure *cb, ScrBytes *msg, ScrStr *addr, ScrStr *family, double port, double size);
/* The dns.lookup adapter: errmsg is NULL on success; everything borrowed. */
typedef void (*ScrDnsLookupFn)(ScrClosure *cb, ScrStr *errmsg, ScrStr *addr, double family);
ScrDgramSocket *scr_dgram_retain(ScrDgramSocket *s);
void scr_dgram_release(ScrDgramSocket *s);
void *scr_dgram_retain_v(void *p);
void scr_dgram_release_v(void *p);
ScrDgramSocket *scr_dgram_create(bool reuse_addr); /* +1 */
/* bind/connect: host borrowed; "" binds 0.0.0.0 (bind's Node default).
* The callback (nullable, moves) registers as once('listening') /
* once('connect'). Both THROW Node's state errors on a bound/connected/
* closed socket; runtime failures (EADDRINUSE) are the async 'error'. */
void scr_dgram_bind(ScrDgramSocket *s, double port, ScrStr *host /*borrowed*/, ScrClosure *cb /*moves, nullable*/);
void scr_dgram_connect(ScrDgramSocket *s, double port, ScrStr *host /*borrowed*/, ScrClosure *cb /*moves, nullable*/);
void scr_dgram_send_str(ScrDgramSocket *s, ScrStr *data /*borrowed*/, double port, ScrStr *host /*borrowed*/);
void scr_dgram_send_bytes(ScrDgramSocket *s, ScrBytes *data /*borrowed*/, double port, ScrStr *host /*borrowed*/);
/* The send argument-validation ladder over dyn arguments (Node's
* signature shuffle, slice bounds, list/type contracts, port/address
* validation, and the connected-state errors); a fully-validated
* unconnected single-payload send RUNS, the rest meet the fence. */
void scr_dgram_send_chk(ScrDgramSocket *s, const ScrDyn *buffer, const ScrDyn *a1,
const ScrDyn *a2, const ScrDyn *a3, const ScrDyn *a4,
const ScrStr *fence);
/* address() parts: ip THROWS "Not running" before bind/connect (+1
* otherwise); family/port are only called after ip succeeded. */
ScrStr *scr_dgram_addr_ip(ScrDgramSocket *s); /* +1, may throw */
ScrStr *scr_dgram_addr_family(ScrDgramSocket *s); /* +1 */
double scr_dgram_addr_port(ScrDgramSocket *s);
void scr_dgram_close(ScrDgramSocket *s, ScrClosure *cb /*moves, nullable*/); /* throws on closed */
void scr_dgram_unref(ScrDgramSocket *s);
void scr_dgram_ref(ScrDgramSocket *s);
void scr_dgram_on_message(ScrDgramSocket *s, ScrClosure *cb /*moves*/, ScrDgramMsgFn fn, bool once);
void scr_dgram_on_error(ScrDgramSocket *s, ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once);
void scr_dgram_on_listening(ScrDgramSocket *s, ScrClosure *cb /*moves*/, bool once);
void scr_dgram_on_close(ScrDgramSocket *s, ScrClosure *cb /*moves*/, bool once);
void scr_dgram_on_connect(ScrDgramSocket *s, ScrClosure *cb /*moves*/, bool once);
/* The runtime-provided message adapters (zero/one-param listeners; the
* two-param rinfo shape is emitted per record shape). */
void scr_dgram_msg_thunk0(ScrClosure *cb, ScrBytes *msg, ScrStr *addr, ScrStr *family, double port, double size);
void scr_dgram_msg_thunk1(ScrClosure *cb, ScrBytes *msg, ScrStr *addr, ScrStr *family, double port, double size);
/* dns.lookup: hostname borrowed, family is 4 (AF_INET pinned by the
* frontend), the callback moves. getaddrinfo runs NOW (blocking —
* SEMANTICS.md); delivery defers to the next loop turn. */
void scr_dns_lookup(ScrStr *hostname /*borrowed*/, double family, ScrClosure *cb /*moves*/, ScrDnsLookupFn fn);
void scr_dns_thunk0(ScrClosure *cb, ScrStr *errmsg, ScrStr *addr, double family);
void scr_dgram_install(void);
#ifdef SCR_RC_AUDIT
long scr_dgram_live_count(void);
#endif
/* The loop-side registration (scr_async.c, always linked) — the net
* hook's exact shape, one more nullable slot set. */
void scr_loop_set_dgram(bool (*pending)(void), void (*dispatch)(void), int (*pollfd)(void));
/* ── fs.watch (scr_watch.c — compiled only when the program uses it;
* design note atop the file). FSWatcher handles over the unit's own
* event backend (kqueue EVFILT_VNODE on macOS/BSD, inotify on Linux):
* refcounted, listeners MOVE in (released at
* close/exit — the child ownership story), an open watcher keeps the
* loop alive until close() (Node's persistent default). */
typedef struct ScrWatcher ScrWatcher;
/* The event adapter: the name ("rename"/"change") arrives BORROWED
* (static storage); adapters build what their listener declared. */
typedef void (*ScrWatchFn)(ScrClosure *cb, const char *event);
ScrWatcher *scr_watcher_retain(ScrWatcher *w);
void scr_watcher_release(ScrWatcher *w);
void *scr_watcher_retain_v(void *p);
void scr_watcher_release_v(void *p);
/* fs.watch(path, listener?) — path borrowed, the callback (nullable)
* MOVES. Opens the path NOW and THROWS Node's fs error when it cannot
* ("ENOENT: ..., watch 'x'" — the polling-fallback catch shape). +1. */
ScrWatcher *scr_fs_watch(ScrStr *path, ScrClosure *cb, ScrWatchFn fn);
void scr_watcher_close(ScrWatcher *w); /* idempotent */
/* The runtime-provided listener adapters (zero-param, and the one-param
* (eventType: string) shape). */
void scr_watch_thunk0(ScrClosure *cb, const char *event);
void scr_watch_thunk_event(ScrClosure *cb, const char *event);
void scr_watch_install(void);
/* The loop-side registration (scr_async.c, always linked) — the net
* hook's exact shape, one more nullable slot set. */
void scr_loop_set_watch(bool (*pending)(void), void (*dispatch)(void), int (*pollfd)(void));
/* ── node:test (scr_test.c — compiled only when the program imports it;
* design note atop the file). Registration builds the test tree while
* module bodies run (describe callbacks execute AT registration, Node's
* collection phase); the first registration spawns the runner FIBER,
* which parks one microtask hop and then runs every test sequentially
* after main returns — sync bodies called on the fiber (its exception
* cell isolates throws), async bodies awaited through their spawn
* wrappers. Reporting is Node v24's spec-reporter shape on stdout;
* durations are real (nondeterministic — harnesses normalize).
* ScrStr arguments are BORROWED; closures MOVE. `mode`: 0 run, 1 skip,
* 2 todo. `flags`: 1 = async body, 2 = body takes the TestContext,
* 4 = { only: true }. `at` is "file:line:col" of the registration call
* (the failing-section "test at" line), "" when unknown. */
typedef struct ScrTestCtx ScrTestCtx;
ScrTestCtx *scr_testctx_retain(ScrTestCtx *t);
void scr_testctx_release(ScrTestCtx *t);
void *scr_testctx_retain_v(void *t);
void scr_testctx_release_v(void *t);
void scr_test_register(ScrStr *name, double mode, ScrStr *msg, ScrClosure *cb, double flags, ScrStr *at);
void scr_test_suite(ScrStr *name, double mode, ScrStr *msg, ScrClosure *cb, ScrStr *at);
/* before/after/beforeEach/afterEach on the CURRENT registration parent
* (`which`: 0 before, 1 after, 2 beforeEach, 3 afterEach). */
void scr_test_hook(double which, ScrClosure *cb, double flags);
/* t.test(...): registers under `t`, runs the subtest INLINE on the
* runner fiber, and returns the settled promise the `await` consumes (+1). */
ScrPromise *scr_test_sub(ScrTestCtx *t, ScrStr *name, double mode, ScrStr *msg, ScrClosure *cb, double flags, ScrStr *at);
void scr_test_ctx_skip(ScrTestCtx *t, ScrStr *msg);
void scr_test_ctx_todo(ScrTestCtx *t, ScrStr *msg);
void scr_test_ctx_diagnostic(ScrTestCtx *t, ScrStr *msg);
ScrStr *scr_test_ctx_name(ScrTestCtx *t); /* +1 */
/* Main's epilogue (emitted when moduleUsesNodeTest): 1 when any non-todo
* test failed, Node's exit contract. */
int scr_test_exit_code(void);
#ifdef SCR_RC_AUDIT
long scr_testctx_live_count(void);
#endif
/* ── node:assert (scr_assert.c; scr_assert_match lives in scr_regex.c —
* it needs the matcher, and every assert.match site carries a regex, so
* the regex link switch is already on). Failures throw a catchable
* AssertionError (%Error with name "AssertionError", code
* "ERR_ASSERTION"); the exception is pending on return, the fs pattern.
* Generated messages are Node's assertion_error.js scalar forms exactly
* (the design note atop scr_assert.c lists the documented divergences).
* All ScrStr/ScrError/ScrRegex arguments are BORROWED. `has_msg`
* distinguishes an omitted user message from an empty one (Node treats
* them differently per operator). */
void scr_assert_fail_msg(ScrStr *message); /* takes ownership; always throws */
ScrStr *scr_assert_inspect_str(const ScrStr *s); /* util.inspect quoting; +1 */
void scr_assert_ok(bool pass, ScrStr *message);
bool scr_assert_same_value_f64(double a, double b); /* Object.is */
/* deepStrictEqual over cyclic values: pair memo (enter answers true for
* a pair already being compared — Node's coinductive memo; leave pops).
* The compiler-emitted helpers over cycle-capable types wrap with these. */
bool scr_assert_deq_enter(const void *a, const void *b);
void scr_assert_deq_leave(void);
void scr_assert_eq_f64(double a, double b, bool negated, bool deep, ScrStr *msg, bool has_msg);
void scr_assert_eq_str(ScrStr *a, ScrStr *b, bool negated, bool deep, ScrStr *msg, bool has_msg);
void scr_assert_eq_bool(bool a, bool b, bool negated, bool deep, ScrStr *msg, bool has_msg);
/* BigInt scalar equality (scr_bigint_assert.c): included only when both
* optional runtime families are active, preserving their independent
* link gates. */
void scr_assert_eq_bigint(ScrBigInt *a, ScrBigInt *b, bool negated, bool deep,
ScrStr *msg, bool has_msg);
void scr_assert_deep_result(bool equal, bool negated, ScrStr *msg, bool has_msg);
/* deepStrictEqual's verdict over two typed-array/Buffer values of one
* static elem kind: brands_eq (Node compares prototypes — Buffer vs
* Uint8Array differ; a compile-time answer from the two arguments' checker
* types) AND same element count AND byte-identical storage. Both sides
* always evaluate (they are ordinary borrowed arguments), so a
* compile-time brand mismatch still runs their side effects. Never
* throws. */
bool scr_assert_bytes_deep_eq(const ScrBytes *a, const ScrBytes *b, bool brands_eq);
/* strictEqual/notStrictEqual over bytes values: reference identity
* (Object.is on objects), with Node's object-comparison headers —
* "Values have same structure but are not reference-equal:" when the
* contents (and static brands) match, the reference-equal expectation
* header otherwise. Header-only messages, the composite stance
* (SEMANTICS.md 102). Borrows a/b/msg; throws on the failing verdict. */
void scr_assert_ref_eq_bytes(const ScrBytes *a, const ScrBytes *b, bool negated,
bool brands_eq, ScrStr *msg, bool has_msg);
/* strictEqual/notStrictEqual over function values: reference identity
* with the same object-comparison headers (two distinct functions are
* never structure-equal, so the failing equal form always expects
* reference equality). Header-only messages (SEMANTICS.md 102). */
void scr_assert_ref_eq_fn(const ScrClosure *a, const ScrClosure *b, bool negated,
ScrStr *msg, bool has_msg);
/* The failing-EQUAL-operator message assembler (createErrDiff's scalar
* slice): exported for the spoke files that carry their own value
* inspection (scr_symbol.c's symbol comparison — the assert.match
* precedent). `ia`/`ib` are the inspected actual/expected bytes;
* `quotes` counts string-typed sides (2 quote chars each, excluded from
* the 12-char short-form budget); `strings` enables the stacked form's
* first-difference `^` indicator. Throws; borrows msg. */
void scr_assert_eq_fail(const char *ia, size_t la, const char *ib, size_t lb,
int quotes, bool both_zero, bool strings, bool deep,
ScrStr *msg, bool has_msg);
/* The failing NOT-equal-operator twin: `insp` is the inspected actual. */
void scr_assert_neq_fail(const char *insp, size_t ilen, bool deep,
ScrStr *msg, bool has_msg);
/* strictEqual/notStrictEqual/deepStrictEqual/notDeepStrictEqual where
* either operand is a checked-dynamic value (the frontend boxes a static
* side into the checked-dynamic tree first). Strict pair: SameValue over the dyn kinds —
* Object.is numbers, byte-equal strings, units by kind, node identity
* for arrays/objects/bytes, BOXED-CLOSURE identity for functions. Deep
* pair: the structural dyn walk (per-element arrays, key-set objects,
* brand-aware bytes via the buffer flavor bit, closure identity for
* functions). Generated messages reproduce assertion_error.js — the
* scalar simple/stacked forms byte-exactly, composite values rendered
* compact:false/sorted through the checked-dynamic tree and diffed with the real myers
* line printer (the design note atop scr_assert.c's dyn section lists
* the divergences). Borrows everything; throws on the failing verdict. */
void scr_assert_eq_dyn(ScrDyn *a, ScrDyn *b, bool negated, bool deep,
ScrStr *msg, bool has_msg);
/* assert.equal/notEqual over the frontend's Abstract Equality verdict;
* actual/expected are borrowed util.inspect renderings. */
void scr_assert_loose_result(bool equal, bool negated, ScrStr *actual,
ScrStr *expected, ScrStr *msg, bool has_msg);
/* assert.throws / assert.rejects whose callback returned (or whose
* promise fulfilled): "Missing expected exception|rejection" with Node's
* details — ` (${expected.name})` when the expected class/shape carries a
* name (has_ename), then `: message` or `.`. */
/* Node's expectsError over an error-INSTANCE expected: the expected dyn
* error's keys (minus the %error marker) each deep-compare against the
* caught value's; mismatches throw the deep-equal AssertionError. */
void scr_assert_expects_err_dyn(ScrDyn *actual, ScrDyn *expected, ScrStr *msg, bool has_msg);
void scr_assert_throws_none(bool rejection, ScrStr *ename, bool has_ename,
ScrStr *msg, bool has_msg);
void scr_assert_throws_mismatch(ScrStr *expected_name, ScrError *err,
ScrStr *msg, bool has_msg);
/* assert.throws(fn, <object shape>) — Node's expectedException over the
* static error surface (name/message/code). A tiny per-call accumulator:
* begin stashes the caught error, each slot call adds one expected key
* (key ids: 0 code, 1 message, 2 name — inspect's SORTED render order),
* and end runs the comparison, throwing Node's deep-equal Comparison
* diff (byte-exact: the bounded key set makes the full rendering
* enumerable) or the custom message. Regex-valued slots enter
* through scr_regex.c (scr_assert_shape_re) which tests eagerly and
* stores the verdict + rendered source, so this file stays libregexp-free;
* scr_assert_shape_actual is its borrowed key-value getter (NULL =
* absent). Straight-line calls between begin and end — no fiber
* suspension, so the accumulator is safely a static. */
void scr_assert_shape_begin(ScrError *err);
void scr_assert_shape_str(int key, ScrStr *v);
void scr_assert_shape_slot_re(int key, bool matched, ScrStr *rendered /* moves */);
ScrStr *scr_assert_shape_actual(int key); /* borrowed; NULL = absent */
void scr_assert_shape_end(ScrStr *msg, bool has_msg);
/* assert.doesNotReject whose rejection MATCHED (or had no expected):
* "Got unwanted rejection[: message].\nActual message: \"...\"". */
void scr_assert_unwanted_rejection(ScrError *err, ScrStr *msg, bool has_msg);
/* assert.ifError over the static surface: throws for ANY value the
* frontend routes here (Node: everything but null/undefined, falsy
* included) — "ifError got unwanted exception: " + the error's message
* (its name when the message is empty — Node reads constructor.name,
* identical for the builtin hierarchy, SEMANTICS.md 105's stance for
* subclasses) or the value's inspection. */
void scr_assert_iferror_err(ScrError *err);
void scr_assert_iferror_f64(double x);
void scr_assert_iferror_str(ScrStr *s);
void scr_assert_iferror_bool(bool b);
/* The checked-dynamic argument: dyn-kind dispatch — units pass quietly,
* %error-marked objects throw with the error's message, everything else
* with the inspection. */
void scr_assert_iferror_dyn(const ScrDyn *v);
/* assert.match / assert.doesNotMatch (scr_regex.c): a fresh exec from
* index 0 — Node's exec on a fresh regex; lastIndex statefulness is not
* modeled (SEMANTICS.md's regex stance), so g/y-flagged regexes test like
* their flag-free twins instead of aborting. */
void scr_assert_match(ScrStr *s, ScrRegex *re, bool negated, ScrStr *msg, bool has_msg);
/* assert.throws(fn, /regex/) whose thrown ERROR did not match: Node tests
* String(actual) ("Name: message") and reports the regex-mismatch message
* with the input inspected (scr_regex.c). Passing calls return. */
void scr_assert_throws_regex(ScrRegex *re, ScrError *err, ScrStr *msg, bool has_msg);
/* assert.throws(fn, {message: /re/, ...}): the regex-valued shape slot —
* tests the stashed actual key eagerly, stores verdict + rendered
* /source/flags into the accumulator (scr_regex.c). */
void scr_assert_shape_re(int key, ScrRegex *re);
/* assert.doesNotReject's regex predicate over String(error) — never
* throws (scr_regex.c). */
bool scr_assert_regex_err_test(ScrRegex *re, ScrError *err);
/* strictEqual/notStrictEqual/deep twins over symbol values: pointer
* identity (a symbol IS its identity), messages from the symbols'
* "Symbol(desc)" rendering — string-style stacked diffs with the `^`
* first-difference indicator, v24 exactly (scr_symbol.c). */
void scr_assert_eq_sym(ScrSym *a, ScrSym *b, bool negated, bool deep,
ScrStr *msg, bool has_msg);
/* ── console ──────────────────────────────────────────────────────────── */
typedef struct {
enum { SCR_ARG_F64, SCR_ARG_STR, SCR_ARG_BOOL } tag;
union {
double f;
ScrStr *s;
bool b;
} v;
} ScrLogArg;
/* Space-joined args + '\n', submitted before return. Borrows string args. */
void scr_console_log(size_t n, const ScrLogArg *args);
/* The stderr twin (console.error AND console.warn — one stream in Node):
* identical formatting, with stdout settled first so merged (2>&1) output
* keeps source order. */
void scr_console_error(size_t n, const ScrLogArg *args);
#endif /* SCR_RUNTIME_H */