From 4d54f6c4d844e6b11947201854755b206cc18447 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 15 Oct 2026 02:06:23 -0500 Subject: [PATCH] Split scr_runtime.h into per-module runtime headers scr_runtime.h now includes 48 module headers (scr_core.h, scr_rc.h, scr_string.h, scr_dyn.h, scr_http.h, ...) in dependency order and holds no declarations. Each module header holds whole sections of the former file, includes the headers whose names it uses, and compiles on its own. Declarations, macros, struct layouts and inline bodies are unchanged. Five blocks move to join their module: the class-hierarchy vtable header (into scr_error.h, after the string core), the node:timers/promises pair (into scr_async.h), the JSON.parse entry points and the JSON output buffer (into scr_json.h, after the dyn declarations) and the EventTarget block (after scr_json.h). No moved block uses or declares a name that a block it moves past uses or declares. The string methods keep their position after the class objects in scr_string_methods.h, because their struct ScrDyn parameters rely on the file-scope struct ScrDyn declaration there. The runtime ABI scan reads scr_runtime.h with its module headers expanded in include order. A runtime test compiles every module header on its own. scripts/runtime-header-hunks.mjs retargets a diff of the single-file header onto the module headers. --- CONTRIBUTING.md | 4 + .../compiler/test/llvm-runtime-abi.test.ts | 26 +- packages/runtime/src/scr_array.h | 425 + packages/runtime/src/scr_assert.h | 161 + packages/runtime/src/scr_async.h | 389 + packages/runtime/src/scr_bigint.h | 72 + packages/runtime/src/scr_blob.h | 83 + packages/runtime/src/scr_bytes.h | 423 + packages/runtime/src/scr_child.h | 289 + packages/runtime/src/scr_closure.h | 148 + packages/runtime/src/scr_console.h | 77 + packages/runtime/src/scr_core.h | 399 + packages/runtime/src/scr_crypto.h | 157 + packages/runtime/src/scr_dc.h | 79 + packages/runtime/src/scr_dgram.h | 72 + packages/runtime/src/scr_dyn.h | 1071 ++ packages/runtime/src/scr_error.h | 265 + packages/runtime/src/scr_event_target.h | 164 + packages/runtime/src/scr_events.h | 56 + packages/runtime/src/scr_events_emitter.h | 149 + packages/runtime/src/scr_exception.h | 199 + packages/runtime/src/scr_ffi.h | 103 + packages/runtime/src/scr_file_handle.h | 62 + packages/runtime/src/scr_generator.h | 184 + packages/runtime/src/scr_http.h | 349 + packages/runtime/src/scr_http2.h | 207 + packages/runtime/src/scr_inspect.h | 128 + packages/runtime/src/scr_json.h | 135 + packages/runtime/src/scr_lib.h | 416 + packages/runtime/src/scr_map.h | 313 + packages/runtime/src/scr_net.h | 342 + packages/runtime/src/scr_number.h | 150 + packages/runtime/src/scr_object.h | 34 + packages/runtime/src/scr_os.h | 81 + packages/runtime/src/scr_path.h | 65 + packages/runtime/src/scr_qs.h | 120 + packages/runtime/src/scr_rc.h | 493 + packages/runtime/src/scr_readline.h | 113 + packages/runtime/src/scr_regex.h | 98 + packages/runtime/src/scr_region.h | 111 + packages/runtime/src/scr_runtime.h | 9180 +---------------- packages/runtime/src/scr_stream.h | 340 + packages/runtime/src/scr_string.h | 264 + packages/runtime/src/scr_string_methods.h | 255 + packages/runtime/src/scr_symbol.h | 31 + packages/runtime/src/scr_test.h | 45 + packages/runtime/src/scr_tls.h | 136 + packages/runtime/src/scr_union.h | 91 + packages/runtime/src/scr_url.h | 112 + packages/runtime/src/scr_watch.h | 38 + packages/runtime/src/scr_zlib.h | 25 + packages/runtime/test/headers.test.ts | 68 + scripts/runtime-header-hunks.mjs | 255 + 53 files changed, 9932 insertions(+), 9120 deletions(-) create mode 100644 packages/runtime/src/scr_array.h create mode 100644 packages/runtime/src/scr_assert.h create mode 100644 packages/runtime/src/scr_async.h create mode 100644 packages/runtime/src/scr_bigint.h create mode 100644 packages/runtime/src/scr_blob.h create mode 100644 packages/runtime/src/scr_bytes.h create mode 100644 packages/runtime/src/scr_child.h create mode 100644 packages/runtime/src/scr_closure.h create mode 100644 packages/runtime/src/scr_console.h create mode 100644 packages/runtime/src/scr_core.h create mode 100644 packages/runtime/src/scr_crypto.h create mode 100644 packages/runtime/src/scr_dc.h create mode 100644 packages/runtime/src/scr_dgram.h create mode 100644 packages/runtime/src/scr_dyn.h create mode 100644 packages/runtime/src/scr_error.h create mode 100644 packages/runtime/src/scr_event_target.h create mode 100644 packages/runtime/src/scr_events.h create mode 100644 packages/runtime/src/scr_events_emitter.h create mode 100644 packages/runtime/src/scr_exception.h create mode 100644 packages/runtime/src/scr_ffi.h create mode 100644 packages/runtime/src/scr_file_handle.h create mode 100644 packages/runtime/src/scr_generator.h create mode 100644 packages/runtime/src/scr_http.h create mode 100644 packages/runtime/src/scr_http2.h create mode 100644 packages/runtime/src/scr_inspect.h create mode 100644 packages/runtime/src/scr_json.h create mode 100644 packages/runtime/src/scr_lib.h create mode 100644 packages/runtime/src/scr_map.h create mode 100644 packages/runtime/src/scr_net.h create mode 100644 packages/runtime/src/scr_number.h create mode 100644 packages/runtime/src/scr_object.h create mode 100644 packages/runtime/src/scr_os.h create mode 100644 packages/runtime/src/scr_path.h create mode 100644 packages/runtime/src/scr_qs.h create mode 100644 packages/runtime/src/scr_rc.h create mode 100644 packages/runtime/src/scr_readline.h create mode 100644 packages/runtime/src/scr_regex.h create mode 100644 packages/runtime/src/scr_region.h create mode 100644 packages/runtime/src/scr_stream.h create mode 100644 packages/runtime/src/scr_string.h create mode 100644 packages/runtime/src/scr_string_methods.h create mode 100644 packages/runtime/src/scr_symbol.h create mode 100644 packages/runtime/src/scr_test.h create mode 100644 packages/runtime/src/scr_tls.h create mode 100644 packages/runtime/src/scr_union.h create mode 100644 packages/runtime/src/scr_url.h create mode 100644 packages/runtime/src/scr_watch.h create mode 100644 packages/runtime/src/scr_zlib.h create mode 100644 packages/runtime/test/headers.test.ts create mode 100644 scripts/runtime-header-hunks.mjs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3311c9c57..43c55884c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,6 +44,10 @@ See the [test harness guide](./tests/harness/README.md) for focused checks, plat When changing native assembly or object emission, runtime-pack selection, or the C runtime, rebuild the relevant native artifacts explicitly. Install CMake, Ninja, the pinned LLVM 22 development package, and Zig 0.16.0 on the target host. Run the matching `@scriptc/llvm-` and `@scriptc/runtime-` packages' `build:native` scripts. The macOS full test suite also needs those generated artifacts. +## Runtime headers + +`packages/runtime/src/scr_runtime.h` includes the per-module runtime headers (`scr_string.h`, `scr_dyn.h`, `scr_http.h`, and so on) in dependency order and holds no declarations itself. Add a new declaration to the header of the runtime unit that defines it: `scr_.h` for `scr_.c`, or the header whose first comment names that unit. The comment at the top of `scr_runtime.h` lists the ordering rules. To move hunks from a branch that still edits the single-file `scr_runtime.h`, run `node scripts/runtime-header-hunks.mjs `. It prints a patch for the module headers. + ## Documentation The docs site is a standalone pnpm workspace under `docs/`. Install its dependencies with `pnpm --dir docs install` and start it with `pnpm --dir docs dev`. Reuse an existing dev server when one is running. diff --git a/packages/compiler/test/llvm-runtime-abi.test.ts b/packages/compiler/test/llvm-runtime-abi.test.ts index 26ec24082..167c77565 100644 --- a/packages/compiler/test/llvm-runtime-abi.test.ts +++ b/packages/compiler/test/llvm-runtime-abi.test.ts @@ -7,7 +7,8 @@ * the class mechanically, from four directions: * * 1. Source scan: every fully-literal `declare ... @scr_*` template - * string in the backend/llvm sources is checked against the header. + * string in the backend/llvm sources is checked against the header + * (scr_runtime.h with its module headers expanded in include order). * 2. Emitted scan: a curated fs/path/os-heavy corpus slice compiles * through the LLVM backend and every `declare @scr_*` in the emitted * .ll — including the generic LIB_FN_SYMS path, whose signatures are @@ -42,7 +43,7 @@ import { compile } from "../src/index.js"; const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../../.."); -const headerPath = join(repoRoot, "packages/runtime/src/scr_runtime.h"); +const runtimeHeaderDir = join(repoRoot, "packages/runtime/src"); const llvmSrcDir = join(import.meta.dirname, "../src/backend/llvm"); const corpusDir = join(repoRoot, "tests/corpus"); @@ -152,13 +153,30 @@ function splitParams(argsText: string): string[] { return out; } +/** scr_runtime.h with every quoted runtime include expanded in place, each + * header once — the declarations a runtime TU sees, in order. */ +async function readRuntimeHeader( + name = "scr_runtime.h", + seen = new Set(), +): Promise { + seen.add(name); + const text = await readFile(join(runtimeHeaderDir, name), "utf8"); + const parts: string[] = []; + for (const line of text.split("\n")) { + const include = line.match(/^#include "(scr_[a-z0-9_]+\.h)"/); + if (include === null) parts.push(line); + else if (!seen.has(include[1]!)) parts.push(await readRuntimeHeader(include[1]!, seen)); + } + return parts.join("\n"); +} + /** Parse scr_runtime.h into name → prototype, plus the extern data * symbols (vtable globals and the like) the emitter references by name. * Only `;`-terminated prototypes count: a `static inline` definition has * no linkage symbol the emitter could declare, so a declare against one * must report as missing. */ async function parseHeader(): Promise<{ protos: Map; dataSyms: Set }> { - const raw = await readFile(headerPath, "utf8"); + const raw = await readRuntimeHeader(); const src = raw .replace(/\\\r?\n/g, "") .replace(/\/\*[\s\S]*?\*\//g, " ") @@ -312,7 +330,7 @@ describe("LLVM backend declares match scr_runtime.h prototypes", () => { const llvm = emitLlvmModule(mod, { runtimeAbiMarker: true }); expect(llvm).toContain(`declare void @${RUNTIME_ABI_MARKER}()`); expect(llvm).toContain(`call void @${RUNTIME_ABI_MARKER}()`); - expect(await readFile(headerPath, "utf8")).toContain(`void ${RUNTIME_ABI_MARKER}(void);`); + expect(await readRuntimeHeader()).toContain(`void ${RUNTIME_ABI_MARKER}(void);`); expect(await readFile(join(repoRoot, "packages/runtime/src/scr_console.c"), "utf8")).toContain( `void ${RUNTIME_ABI_MARKER}(void) {}`, ); diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h new file mode 100644 index 000000000..7a32490c3 --- /dev/null +++ b/packages/runtime/src/scr_array.h @@ -0,0 +1,425 @@ +/* scriptc runtime — arrays and Math over arrays (scr_array.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_ARRAY_H +#define SCR_ARRAY_H + +#include "scr_qs.h" + +/* ── arrays ───────────────────────────────────────────────────────── + * Monomorphic, growable, refcounted. Elements live in 8-byte slots + * (doubles/pointers via memcpy and casts), except SCR_ELEM_BOOL, whose + * dense storage holds one byte (0 or 1) per element; `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 out of bounds (length ) + * 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_ops) as one + * constant descriptor per element type, stored once per array; 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; + +/* The RC entry points of an SCR_ELEM_REF array's element type: one constant + * per element type (emitted by the compiler, or static in the runtime), + * shared by every array of that type. retain/release are the type's `_v` + * adapters; trace is non-NULL exactly when the element type carries a cycle + * header. */ +typedef struct ScrArrOps { + void *(*retain)(void *); + void (*release)(void *); + ScrTraceFn trace; +} ScrArrOps; + +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 */ + /* One ScrElemKind. */ + uint8_t elem; + /* 1 exactly when ops->trace is non-NULL: the element type carries a cycle + * header, so the array is CYCLE-CAPABLE (an element can point back at the + * array that owns it) and allocates with the hidden collector header — + * its 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. Kept beside `elem` so the RC fast paths test it with one load. */ + uint8_t cyc; + uint16_t spare; + /* A lower bound on the hole-free prefix: filled <= len, filled <= cap, + * and every dense state below it is SCR_ARR_VALUE. Element reads below + * it need no state check and element writes below it leave the states + * alone. Any operation that may store another state (a hole, present + * undefined) or move states below it lowers it first (scr_arr_cut); + * 0 is always valid. */ + uint32_t filled; + /* SCR_ELEM_REF only; NULL for every other element kind. */ + const ScrArrOps *ops; + /* Dense values: `cap` 8-byte slots, or `cap` bytes for SCR_ELEM_BOOL + * (scr_arr_slot_width). Sparse entries and properties keep 8-byte slots + * for every kind; a bool slot is 0 or 1 wherever it lives. */ + uint64_t *data; + uint8_t *present; /* one state per dense slot; heap arrays share data allocation */ + /* Rarely used storage (sparse indices, noncanonical numeric properties, + * match-result metadata) lives out of line: NULL until first needed, so + * the common dense array stays small. */ + struct ScrArrExt *ext; +} ScrArr; + +typedef struct ScrArrExt { + 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; + struct ScrDyn *metadata; /* heterogeneous named properties of match results */ +} ScrArrExt; +/* The LLVM backend mirrors ScrArr as %ScrArr: its inline retain/release read + * `cyc` (field 4, i8) to decide mark-live / candidate buffering exactly like + * scr_arr_retain and scr_arr_release; its inline element access uses filled + * (field 6, i32), data (field 8) and present (field 9). */ +_Static_assert(offsetof(ScrArr, cyc) == 3 * sizeof(size_t) + 1, + "inline RC fast paths read ScrArr.cyc as %ScrArr field 4"); +_Static_assert(offsetof(ScrArr, filled) == 3 * sizeof(size_t) + 4, + "inline element access reads ScrArr.filled as %ScrArr field 6"); +_Static_assert(offsetof(ScrArr, data) == 3 * sizeof(size_t) + 8 + sizeof(void *), + "inline element reads use ScrArr.data as %ScrArr field 8"); + +/* Bytes per dense value slot. */ +static inline size_t scr_arr_slot_width(const ScrArr *a) { + return a->elem == SCR_ELEM_BOOL ? 1 : sizeof(uint64_t); +} + +/* The dense value at `index` (< cap) as an 8-byte slot, and its store. */ +static inline uint64_t scr_arr_dense_get(const ScrArr *a, size_t index) { + if (a->elem == SCR_ELEM_BOOL) return ((const uint8_t *)a->data)[index]; + return a->data[index]; +} + +static inline void scr_arr_dense_put(ScrArr *a, size_t index, uint64_t slot) { + if (a->elem == SCR_ELEM_BOOL) ((uint8_t *)a->data)[index] = (uint8_t)slot; + else a->data[index] = slot; +} + +/* Lower the hole-free prefix to at most `index`: call before storing a + * non-value state at `index`, or before moving states into [index, ...). */ +static inline void scr_arr_cut(ScrArr *a, size_t index) { + if (a->filled > index) a->filled = (uint32_t)index; +} + +/* The first index in [f, end) whose state is not a present value (or end). + * Compares eight states per step: bulk copies (slice, concat, spread) end + * with this scan over the whole copied range. */ +static inline size_t scr_arr_value_run(const uint8_t *present, size_t f, size_t end) { + const uint64_t values = 0x0101010101010101ull * SCR_ARR_VALUE; + while (end - f >= 8) { + uint64_t word; + memcpy(&word, present + f, sizeof word); + if (word != values) break; + f += 8; + } + while (f < end && present[f] == SCR_ARR_VALUE) f++; + return f; +} + +/* Extend the hole-free prefix over populated dense states. Starts at the + * current bound, so after an append it costs only the appended range. */ +static inline void scr_arr_refill(ScrArr *a) { + size_t end = a->len < a->cap ? a->len : a->cap; + size_t f = a->filled; + if (f < end) f = scr_arr_value_run(a->present, f, end); + a->filled = (uint32_t)f; +} + +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: `ops` must outlive the array (a constant; the + * compiler emits one per element type) and its retain/release must be + * non-NULL; ops->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_ops(const ScrArrOps *ops, size_t initial_cap); +/* An empty array expected to receive a few elements: its first slots live + * in the array's own block (one allocation; the block of a plain empty + * array plus five 8-byte slots). Otherwise exactly scr_arr_new(elem, 0) and + * scr_arr_new_ops(ops, 0). Returns +1. */ +ScrArr *scr_arr_new_small(ScrElemKind elem); +ScrArr *scr_arr_new_ops_small(const ScrArrOps *ops); + +static inline ScrArr *scr_arr_retain(ScrArr *a) { + if (a->rc != SIZE_MAX) { + a->rc++; + } + return a; +} + +/* Releases ref elements (SCR_ELEM_STR/ARR/BYTES/REF) recursively at rc == 0. + * NULL-tolerant (uninitialized locals). Cycle-capable arrays (`cyc`) feed + * the candidate-root buffer like every headered type. */ +void scr_arr_release(ScrArr *a); +void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target); +/* The array's out-of-line storage, allocated (zeroed) on first use. */ +ScrArrExt *scr_arr_ext(ScrArr *a); +/* Moves inline dense slots (kept in the header block of a small array) to + * their own allocation, so the storage pointers may be swapped with another + * array's. No-op for other arrays. */ +void scr_arr_outline(ScrArr *a); +/* Swaps the sparse slots and numeric properties of two arrays; each keeps + * its own match metadata (refresh of a cached typed view, llvm/dyn.ts). */ +void scr_arr_swap_indexed_ext(ScrArr *a, ScrArr *b); + +/* 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); +/* Mutating indexed operations borrow the receiver and value, preserve + * sparse presence, and return the receiver retained. */ +ScrArr *scr_arr_copy_within(ScrArr *a, double target, double start, double end); +ScrArr *scr_arr_fill_f64(ScrArr *a, double value, double start, double end); +ScrArr *scr_arr_fill_bool(ScrArr *a, bool value, double start, double end); +ScrArr *scr_arr_fill_ref(ScrArr *a, void *value, double start, double end); +ScrArr *scr_arr_fill_undefined(ScrArr *a, double start, double end); +/* 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 */ +void *scr_arr_borrow_ref(ScrArr *a, double i); /* same checks; borrowed */ +/* Borrow a non-null reference element, or NULL for a hole/undefined/missing + * property. Keep the array alive and retain before removing its element. + * This read does not invoke retain callbacks or mutate runtime state. */ +void *scr_arr_peek_ref(const ScrArr *a, double i); + +/* 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); +/* Compiler-built slot buffers do not alias array storage. Both operations + * move ownership of every slot and preserve the evaluated argument order. */ +double scr_arr_push_many(ScrArr *a, size_t count, const uint64_t *slots); +double scr_arr_unshift_many(ScrArr *a, size_t count, const uint64_t *slots); +/* 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); +/* Default primitive String ordering; stable, borrowed receiver, owned result. */ +ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy); +/* Stable comparator sort of the first `count` slots of a private, compacted + * snapshot (every slot a value; f64 or reference elements). `f` is a borrowed + * closure of `arity` (0..2) element parameters returning a number. Elements + * move without reference-count traffic; a pending exception from `f` stops + * the sort and leaves the snapshot unchanged. */ +struct ScrClosure; +/* A nonzero `order` says `f` orders number keys ascending (1) or descending + * (-1) without side effects: f64 elements are their own keys, and reference + * elements read theirs through the borrowed `key` closure (element -> + * number). Keys that are all numbers other than NaN and -0 then sort without + * calling `f`, with the same stable result. */ +void scr_arr_sort_values(ScrArr *a, double count, struct ScrClosure *f, uint32_t arity, + int32_t order, struct ScrClosure *key); +/* Comparator sort of a dense array in place: `a.sort(f)`, or with `owned` + * the private copy of `a.toSorted(f)`. Returns false without calling `f` + * when `a` has holes, storage-level undefined or a sparse tail, or is + * published; the caller then sorts a compacted snapshot. Otherwise sorts + * and returns true; a pending exception from `f` leaves a shared `a` + * unwritten (an owned copy then holds a permutation of its values). */ +/* The borrowed 8-byte slots of `a` when every index below its length holds + * a value in dense storage (length in *len); NULL for holes, storage-level + * undefined, sparse tails, empty arrays and boolean elements. */ +const uint64_t *scr_arr_dense_slots(const ScrArr *a, size_t *len); +bool scr_arr_sort_dense(ScrArr *a, struct ScrClosure *f, uint32_t arity, bool owned, + int32_t order, struct ScrClosure *key); +/* V8's TimSort over n slots with a caller comparison: less(ctx, x, y) + * answers whether x orders before y (comparefn(x, y) < 0), or sets *failed, + * with an exception pending, to stop the sort. Returns false after a + * failure; `values` is then unchanged, and it holds every slot exactly once, + * in its original order, whenever `less` runs. */ +typedef bool (*ScrSortLess)(void *ctx, uint64_t x, uint64_t y, bool *failed); +bool scr_sort_slots_with(uint64_t *values, size_t n, ScrSortLess less, void *ctx); + +/* 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); +/* The same splice in statement position: inserts `count` owned value slots + * (moved in) and releases the removed elements without building a result + * array. Borrows a. */ +void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t count, + const uint64_t *slots); +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); + +#endif /* SCR_ARRAY_H */ diff --git a/packages/runtime/src/scr_assert.h b/packages/runtime/src/scr_assert.h new file mode 100644 index 000000000..fa16f3095 --- /dev/null +++ b/packages/runtime/src/scr_assert.h @@ -0,0 +1,161 @@ +/* scriptc runtime — node:assert (scr_assert.c). Included by scr_runtime.h, + * which says where new declarations go. */ +#ifndef SCR_ASSERT_H +#define SCR_ASSERT_H + +#include "scr_http2.h" + +/* ── 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, ) — 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); +/* No-error assertions share Node's unwanted-exception/rejection message. + * The frontend reads and coerces actual?.message, preserving getter throws. */ +void scr_assert_unwanted_error(ScrStr *actual_message, bool rejection, ScrStr *msg, bool has_msg); +bool scr_assert_no_error_predicate(ScrDyn *predicate, ScrDyn *actual); +bool scr_assert_regex_dyn_test(ScrRegex *re, ScrDyn *actual); +/* 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): RegExp.test semantics, + * including global/sticky lastIndex transitions. */ +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); + +#endif /* SCR_ASSERT_H */ diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h new file mode 100644 index 000000000..c1d4e0ff4 --- /dev/null +++ b/packages/runtime/src/scr_async.h @@ -0,0 +1,389 @@ +/* scriptc runtime — promises, fibers, the event loop, AsyncLocalStorage, + * promise combinators and timers (scr_async.c, scr_async_dyn.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_ASYNC_H +#define SCR_ASYNC_H + +#include "scr_dyn.h" + +/* ── 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); + +/* ── 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); +void scr_promise_share_identity(ScrPromise *dst, ScrPromise *src); +bool scr_promise_identity_equal(ScrPromise *a, ScrPromise *b); +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); +/* `await f(...)` of a fiber-backed async f: spawn and await in one call, + * returning the awaited value (a rejection re-throws into the caller). */ +void scr_async_spawn_await_void(void (*entry)(ScrFiber *, void *), void *argpack); +double scr_async_spawn_await_f64(void (*entry)(ScrFiber *, void *), void *argpack); +bool scr_async_spawn_await_bool(void (*entry)(ScrFiber *, void *), void *argpack); +ScrStr *scr_async_spawn_await_str(void (*entry)(ScrFiber *, void *), void *argpack); +void *scr_async_spawn_await_ref(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); +/* Fiberless async call (native targets): the compiler proves an async body + * can never park its execution context and runs it directly on the + * caller's stack between enter and leave. The frame saves what a fresh + * fiber would isolate — the Error.stack frame chain and the + * AsyncLocalStorage context — and leave restores both, returning a fresh + * pending promise (+1) the caller settles from the body's outcome. */ +typedef struct ScrAsyncInline { + void *stack; + void *als; +} ScrAsyncInline; +void scr_async_inline_enter(ScrAsyncInline *frame); +ScrPromise *scr_async_inline_leave(ScrAsyncInline *frame); +/* Leaves the frame without creating the call's promise: `await f()` of a + * fiberless f consumes the result directly (the emitted await-call entry + * still takes the await's microtask turn). */ +void scr_async_inline_exit(ScrAsyncInline *frame); + +/* 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); +ScrDyn *scr_await_dyn_value_settled(ScrDyn *value); +void scr_promise_resolve_dyn(ScrPromise *destination, ScrDyn *value); +/* `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 SCR_TL ScrAlsCtx **scr_als_active; +#ifdef SCR_WORKERS +ScrAlsCtx **scr_als_slot(void); +#define SCR_ALS_SLOT() scr_als_slot() +#else +#define SCR_ALS_SLOT() scr_als_active +#endif +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 SCR_TL 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 SCR_TL 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). + * scr_process_emit_warning takes the ARGUMENT VECTOR as one dyn array, + * applies Node's full grammar/TypeErrors at the call and emits on the + * next tick, as Node does; scr_emit_warning is the C-side entry and + * reports synchronously. */ +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); +/* scr_emit_warning once a 'warning' listener exists (scr_lib.c; set by the + * listener registration): units that cannot link the dyn-async unit route + * their warnings through it, and print the default report otherwise. */ +extern SCR_TL void (*scr_warning_emit_hook)(const char *name, const char *code, ScrStr *message); +/* process.noDeprecation (which 0) and process.throwDeprecation (which 1): + * the stored value (+1; undefined until assigned), the assignment + * (borrowed), and the truthiness the warning paths test. */ +ScrDyn *scr_process_deprecation_flag(double which); +void scr_process_set_deprecation_flag(double which, ScrDyn *value); +bool scr_process_deprecation_flag_on(int which); +/* Node's emitWarning tail on the next tick (util.deprecate): a + * DeprecationWarning is dropped under noDeprecation and thrown from its + * own tick under throwDeprecation; otherwise the report and the 'warning' + * listeners run. scr_warning_new builds the string form's warning value + * (borrows; +1). */ +void scr_warning_emit_deferred(ScrDyn *warning); +ScrDyn *scr_warning_new(ScrStr *message, const char *type, const ScrStr *code); +/* 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); + +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_join: subscribe the result to an entry with a reaction job that + * runs one microtask turn after the entry settles (or after the join, for + * an entry already settled) — 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; the entry counts as HANDLED from the join. race_add is the same + * callback run inside the entry's own settlement (or now, if settled), + * for destinations that view the entry at another inner type. The + * payload accessors return retained/by-value fulfillment payloads for + * the emitted adapters (losing entries keep their settlements). */ +void scr_promise_race_join(ScrPromise *race, ScrPromise *in, + void (*adapt)(ScrPromise *dst, ScrPromise *src)); +void scr_promise_race_add(ScrPromise *race, ScrPromise *in, + void (*adapt)(ScrPromise *dst, ScrPromise *src)); + +/* ── Promise.all (compiler-emitted combinator plumbing) ─────────────── + * BORROWS the Promise[] entries array and the pre-capacity (empty, + * cap >= ps->len) values array — NULL values/store for Promise + * entries, whose result fulfills void — retains what it keeps, and + * returns the result promise +1. Node-exact: each entry settles the + * result through a reaction job one microtask turn after it settles, + * values land at their INPUT index regardless of settlement order, the + * first rejection's job wins (every entry counts as handled), 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)); + +/* ── Promise.allSettled / Promise.any ───────────────────────────────── + * The same countdown and per-entry reaction jobs as Promise.all, with + * the mode deciding what an entry's job does. allSettled stores EVERY + * settlement through `store` (the emitted or dyn settled-record builder) + * and fulfills with the array once every entry has settled. any settles + * the result from the first fulfillment (same inner type — a copy), + * stores each rejection reason into `errors` at its input index, and + * once every entry has rejected calls `fail(result, errors)`, which + * rejects the result (the AggregateError). The empty array fulfills / + * fails immediately. Both BORROW their arrays and return +1. + * + * The builder underneath (ScrCombine) takes entries one at a time, so a + * caller that runs other work between entries — the dyn path's + * thenable jobs — keeps Node's queue order: settled entries batch into + * one job until scr_promise_combine_flush closes the batch. */ +typedef void (*ScrAllStore)(ScrArr *a, double i, ScrPromise *src); +typedef void (*ScrAnyFail)(ScrPromise *result, ScrArr *errors); +enum { SCR_COMBINE_ALL = 0, SCR_COMBINE_ALL_SETTLED = 1, SCR_COMBINE_ANY = 2 }; +typedef struct ScrCombine { + struct ScrAllState *st; + ScrPromise *result; + size_t next; /* input index of the next entry */ + size_t batched; /* settled entries counted in the open batch */ + ScrPromise *decided; /* the open batch's first deciding entry (+1) */ + bool open; /* the open batch holds at least one entry */ +} ScrCombine; +void scr_promise_combine_begin(ScrCombine *c, int mode, size_t n, ScrArr *values, + ScrAllStore store, ScrAnyFail fail); +void scr_promise_combine_add(ScrCombine *c, ScrPromise *in); /* borrowed */ +void scr_promise_combine_flush(ScrCombine *c); +ScrPromise *scr_promise_combine_end(ScrCombine *c); /* +1 */ +ScrPromise *scr_promise_all_settled(ScrArr *ps, ScrArr *values, ScrAllStore store); +ScrPromise *scr_promise_any(ScrArr *ps, ScrArr *errors, ScrAllStore store, ScrAnyFail fail); +/* The typed Promise.any entry (scr_async_dyn.c): reasons collect as dyn + * values and exhaustion rejects with an AggregateError. */ +ScrPromise *scr_promise_any_typed(ScrArr *ps); +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_is_rejected(const ScrPromise *p); +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); + +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); + +#endif /* SCR_ASYNC_H */ diff --git a/packages/runtime/src/scr_bigint.h b/packages/runtime/src/scr_bigint.h new file mode 100644 index 000000000..2d5c7bcef --- /dev/null +++ b/packages/runtime/src/scr_bigint.h @@ -0,0 +1,72 @@ +/* scriptc runtime — ECMAScript bigint values (scr_bigint.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_BIGINT_H +#define SCR_BIGINT_H + +#include "scr_lib.h" + +/* ── 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); +uint64_t scr_bigint_hash(const ScrBigInt *value); /* normalized value, no allocation */ +bool scr_bigint_eq_string(ScrBigInt *a, ScrStr *b); /* StringToBigInt failure is false */ +double scr_bigint_cmp_string(ScrBigInt *a, ScrStr *b); /* 2 = invalid StringToBigInt */ +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); +/* Small-value bigint trees the compiler evaluates in 64-bit registers: + * mag64 stores the low 64 bits of the magnitude and its length in 64-bit + * words, and returns the sign (-1, 0, 1); from_mag64 builds +-mag (+1). + * wrap_eval is the exact fallback over a compiler-built postfix program + * (leaves, and numbers for BigInt(number) operands); +1 or NULL with an + * exception pending. */ +int scr_bigint_mag64(const ScrBigInt *value, uint64_t *mag, uint32_t *words); +/* Whether a StringToBigInt text with this many significant digits in radix + * 2, 8, 10 or 16 can fit the maximum BigInt size (no size arithmetic that + * can wrap; the parser's guard before it allocates). */ +bool scr_bigint_digits_fit(size_t digits, unsigned radix); +ScrBigInt *scr_bigint_from_mag64(uint64_t mag, bool negative); +ScrBigInt *scr_bigint_wrap_eval(const char *program, ScrBigInt *const *leaves, const double *numbers); +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); + +#endif /* SCR_BIGINT_H */ diff --git a/packages/runtime/src/scr_blob.h b/packages/runtime/src/scr_blob.h new file mode 100644 index 000000000..15f4a10d4 --- /dev/null +++ b/packages/runtime/src/scr_blob.h @@ -0,0 +1,83 @@ +/* scriptc runtime — Blob and File (scr_blob.c). Included by scr_runtime.h, + * which says where new declarations go. */ +#ifndef SCR_BLOB_H +#define SCR_BLOB_H + +#include "scr_dyn.h" + +/* ── Blob / File (scr_blob.c) ────────────────────────────────────────── + * Node 24's lib/internal/blob.js and file.js: immutable bytes copied from + * the sources at construction, shared by slices and structured clones. + * The constructors take the argument vector as one dyn array (borrowed) + * and validate it like Node; +1 handle, or NULL with the exception + * pending. slice takes its (start, end, contentType) vector the same way. + * The readers answer +1 promises (string, u8 bytes, and an ArrayBuffer + * dyn value) that settle on Node's microtask turn for the Blob's part + * count. scr_blob_is answers `v instanceof Blob` (kind 0) or `File` (1). */ +ScrDyn *scr_blob_new(ScrDyn *args); +ScrDyn *scr_file_new(ScrDyn *args); +ScrDyn *scr_blob_slice(ScrDyn *self, ScrDyn *args); +ScrPromise *scr_blob_text(ScrDyn *self); +ScrPromise *scr_blob_bytes(ScrDyn *self); +ScrPromise *scr_blob_array_buffer(ScrDyn *self); +bool scr_blob_is(const ScrDyn *value, double kind); + +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_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)); +ScrDyn *scr_console_native(void); +/* node:console Console instances: a native handle over the stdout/stderr + * targets — process stdio (fd 1/2) or a writable stream bound by + * scr_console_bind (scr_stream.c, which also installs the stream writer so + * console-only programs never link the stream unit) — and the + * ignoreErrors flag. new answers a +1 handle with both targets set to + * the given fds (0 = a stream is bound next); bind retains the stream. + * write prints `text\n` to the selected target: a stream receives ONE + * write, wrapped in Node's ignoreErrors handling (a noop 'error' listener + * around the call, a completion handler that swallows the write's own + * error, synchronous throws discarded); the global console handle writes + * the process streams like console.log/error. */ +typedef struct ScrConsoleInst { + size_t rc; + double fd[2]; /* 1/2 process stdio targets; 0 when stream[i] is bound */ + ScrStream *stream[2]; /* owned */ + ScrClosure *on_written[2]; /* owned: the per-stream completion handlers */ + bool ignore_errors; + void (*stream_write)(struct ScrConsoleInst *, int which, ScrStr *line); + void (*stream_drop)(struct ScrConsoleInst *); /* releases the bound streams */ +} ScrConsoleInst; +ScrDyn *scr_console_new(double out_fd, double err_fd, bool ignore_errors); +void scr_console_bind(ScrDyn *console, double which, ScrStream *stream); +void scr_console_inst_write(ScrDyn *console, ScrStr *text, bool to_stderr); +ScrDyn *scr_process_stdio(double fd); +bool scr_dyn_process_stdio_is(const ScrDyn *value); +double scr_dyn_process_stdio_fd(const ScrDyn *value, const ScrDynPath *path); + +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 throw "not supported yet" for it. +1, or NULL with a pending exception. */ +ScrDyn *scr_dyn_handle_key_get(const ScrDyn *d, const ScrStr *k); + +#endif /* SCR_BLOB_H */ diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h new file mode 100644 index 000000000..4634a2994 --- /dev/null +++ b/packages/runtime/src/scr_bytes.h @@ -0,0 +1,423 @@ +/* scriptc runtime — typed arrays and Buffer (scr_bytes.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_BYTES_H +#define SCR_BYTES_H + +#include "scr_events.h" + +/* ── 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 typed ELEMENT reads trap like arrays; invalid writes are + * ignored as in JavaScript. 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 ScrSharedBytes ScrSharedBytes; + +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 is_data_view; /* DataView is distinct from a numeric typed array. */ + bool external; /* Root aliases foreign memory; releasing it never frees data. */ + ScrSharedBytes *shared; /* Neutral backing, borrowed by views from their local owner. */ +} ScrBytes; + +/* Internal scoped locking is only for leaf memory operations. Never hold this + * guard across a script callback, blocking I/O, or a fiber suspension. */ +void scr_shared_enter(void); +void scr_shared_leave(void); +bool scr_shared_guard(const ScrBytes *, const ScrBytes *); +void scr_shared_guard_release(bool *); +#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) +#define SCR_SHARED_GUARD(a, b) bool scr_shared_held __attribute__((cleanup(scr_shared_guard_release))) = scr_shared_guard(a, b) +#else +#define SCR_SHARED_GUARD(a, b) ((void)0) +#endif +ScrSharedBytes *scr_shared_retain(ScrSharedBytes *); +void scr_shared_release(ScrSharedBytes *); +void scr_bytes_make_shared(ScrBytes *); +ScrBytes *scr_shared_wrap(ScrSharedBytes *); +void scr_bytes_read(const ScrBytes *, size_t, void *, size_t); +void scr_bytes_write(ScrBytes *, size_t, const void *, size_t); +ScrStr *scr_bytes_string(const ScrBytes *, size_t, size_t); +ScrBytes *scr_bytes_snapshot(const ScrBytes *); +void scr_bytes_snapshot_release(ScrBytes **); +ScrBytes *scr_bytes_local_copy(ScrBytes *); /* +1; shared inputs are copied */ +void scr_bytes_require_unshared(const ScrBytes *); +/* Snapshot before passing byte pointers to I/O or callback-capable consumers. + * The copy is local, so neither a lock nor shared raw memory escapes. */ +#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) +#define SCR_BYTES_SNAPSHOT(name) \ + ScrBytes *scr_snapshot_##name __attribute__((cleanup(scr_bytes_snapshot_release))) = scr_bytes_snapshot(name); \ + if (scr_snapshot_##name) name = scr_snapshot_##name +#else +#define SCR_BYTES_SNAPSHOT(name) ((void)0) +#endif +ScrDyn *scr_shared_array_buffer_new(ScrDyn *); +ScrDyn *scr_worker_data(void); +ScrDyn *scr_worker_parent_port(void); +ScrDyn *scr_worker_new(double root, ScrStr *filename, ScrDyn *options); +bool scr_shared_array_buffer_is(const ScrDyn *); +bool scr_buffer_storage_is(const ScrDyn *); +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); + +/* `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_constructor(void); +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 */ +ScrBytes *scr_bytes_take_data(uint8_t *data, size_t len); /* +1, consumes malloc storage */ + +/* Same-element-kind copy, preserving the raw representation. Cross-kind + * construction uses scr_bytes_convert. 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. Invalid typed reads trap; invalid writes are ignored. + * Checked reads preserve JavaScript's undefined result. Valid 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); +double scr_bytes_get_or_nan(const ScrBytes *b, double i); /* NaN when the index is invalid */ +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 */ +/* In-place numeric sort of a typed array: order 0 is the default + * TypedArray order (NaN last, -0 before +0); 1 or -1 is a recognized + * numeric comparator's, refused (false, nothing written) when a float + * element is NaN or -0. Runs no script code. */ +bool scr_bytes_sort_numeric(ScrBytes *b, int32_t order); +/* Comparator sort of a typed array with the comparison schedule Node + * uses; `f` is a borrowed closure of `arity` (0..2) number parameters. + * A comparator exception stays pending and nothing is written back. */ +void scr_bytes_sort_compare(ScrBytes *b, struct ScrClosure *f, uint32_t arity); +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 stays lone; 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); +double scr_text_decoder_encoding(const ScrDyn *label); +ScrStr *scr_text_decoder_name(double encoding); +ScrDyn *scr_text_encode_into(const ScrDyn *source, const ScrDyn *destination); +bool scr_buffer_is_ascii(const ScrDyn *input); +bool scr_buffer_is_utf8(const ScrDyn *input); +ScrBytes *scr_buffer_transcode(const ScrDyn *source, const ScrDyn *from, const ScrDyn *to); +ScrBytes *scr_bytes_buffer_source(const ScrDyn *value); +ScrStr *scr_text_decode_buffer_source(const ScrDyn *value); +ScrStr *scr_text_decode_options(const ScrBytes *b, bool fatal, bool ignore_bom); +ScrStr *scr_text_decode_stream(ScrDyn *state, const ScrBytes *b, double encoding, bool fatal, bool ignore_bom, bool stream); +ScrStr *scr_text_decode_legacy_options(const ScrBytes *b, double encoding, bool fatal, bool ignore_bom); + +/* 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); +ScrBytes *scr_bytes_from_str_checked(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); +double scr_buffer_byte_length_dyn(ScrDyn *value, 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 without an error; 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); +double scr_fs_timestamp_value(const ScrDyn *t, const char *name); +void scr_fs_utimes_checked(const ScrDyn *path, const ScrDyn *atime, const ScrDyn *mtime); +void scr_fs_lutimes_checked(const ScrDyn *path, const ScrDyn *atime, const ScrDyn *mtime); +void scr_fs_futimes_checked(const ScrDyn *fd, const ScrDyn *atime, const ScrDyn *mtime); +/* 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 */ + +#endif /* SCR_BYTES_H */ diff --git a/packages/runtime/src/scr_child.h b/packages/runtime/src/scr_child.h new file mode 100644 index 000000000..c87d97662 --- /dev/null +++ b/packages/runtime/src/scr_child.h @@ -0,0 +1,289 @@ +/* scriptc runtime — child_process (scr_child.c). Included by scr_runtime.h, + * which says where new declarations go. */ +#ifndef SCR_CHILD_H +#define SCR_CHILD_H + +#include "scr_lib.h" + +/* ── 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: [\n]"), spawn + * failure throws "spawnSync ENOENT", a timeout SIGTERMs the child + * and throws "spawnSync 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: \n" with the + * unconditional newline, "spawn " 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 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); +/* Runtime options use the same spawn core; validation and explicit + * unsupported-option refusals throw before starting a child. Borrows. */ +ScrChild *scr_spawn_dynamic(ScrStr *cmd, ScrArr *args, const ScrDyn *options); +void scr_child_on_spawn(ScrChild *child, ScrClosure *cb); /* moves zero-argument callback */ +/* 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); +void scr_child_ref(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); +void scr_child_on_dyn(ScrChild *child, ScrStr *event, ScrDyn *listener); /* borrowed */ +/* Optional native-stream bridge. The table avoids a stream dependency + * in programs that only use the child-process surface. */ +typedef struct ScrChildStreamOps { + ScrStream *(*readable)(void); + ScrStream *(*writable)(ScrClosure *, ScrStreamChunkInv, ScrClosure *, ScrStreamPlainInv); + void *(*retain)(void *); + void (*release)(void *); + ScrTraceFn trace; + bool (*push)(ScrStream *, ScrBytes *); + bool (*end)(ScrStream *); + void (*write_done)(ScrStream *, ScrError *); + void (*final_done)(ScrStream *, ScrError *); +} ScrChildStreamOps; +extern const ScrChildStreamOps scr_stream_child_ops; +ScrDyn *scr_stream_dyn_view(ScrStream *stream); /* borrowed; +1 live method/property view */ +void scr_child_dyn_streams_install(const ScrChildStreamOps *ops, + ScrDyn *(*readable_box)(void *), ScrDyn *(*writable_box)(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); + +#endif /* SCR_CHILD_H */ diff --git a/packages/runtime/src/scr_closure.h b/packages/runtime/src/scr_closure.h new file mode 100644 index 000000000..9644722ce --- /dev/null +++ b/packages/runtime/src/scr_closure.h @@ -0,0 +1,148 @@ +/* scriptc runtime — boxes and closures (scr_closure.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_CLOSURE_H +#define SCR_CLOSURE_H + +#include "scr_map.h" + +/* ── 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; + +/* A box can lie on a cycle only when its trace visits its payload: a + * closure, or a class instance that carries a cycle header. Every other box + * is a leaf of the object graph, so its releases buffer no candidate. The + * emitted inline release (llvm/shapes.ts, the box row) mirrors this test, + * reading `kind` and `obj_trace` at the asserted offsets. */ +static inline bool scr_box_traced(const ScrBox *b) { + return b->kind == SCR_BOX_FUNC || b->obj_trace != NULL; +} +_Static_assert(SCR_BOX_FUNC == 4, "the emitted box release compares kind against 4"); +#if UINTPTR_MAX == UINT64_MAX +_Static_assert(offsetof(ScrBox, kind) == 8 && offsetof(ScrBox, obj_trace) == 32, + "the emitted box release reads kind at 8 and obj_trace at 32"); +#else +_Static_assert(offsetof(ScrBox, kind) == 4 && offsetof(ScrBox, obj_trace) == 16, + "the emitted box release reads kind at 4 and obj_trace at 16"); +#endif + +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++; + } + 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; + uint32_t function_kind; /* low bits: 0 ordinary, 1 generator, 2 async, 3 async generator; bit 2: ordinary own prototype; bit 3: checked callable adapter in caps[0] */ + /* The root's `length` as boxed values report it; meaningful only when + * identity is set (the root's own boxes carry their arity). */ + uint32_t identity_length; + /* The JS function this closure stands for. NULL means the closure is + * its own identity. A signature adapter (the same function viewed + * through a different native ABI) points at the root of the function + * it adapts, so chains of adapters collapse to one root and `===`, + * SameValue, searches and own properties all observe one function. + * Borrowed: the adapter keeps its original alive through its captures. */ + struct ScrClosure *identity; + /* The same function with every reference argument borrowed (the caller + * keeps each one alive across the call and releases nothing extra), or + * NULL. Same signature as `fn`; only callers that hold their arguments + * for the whole call may enter through it. */ + void *fn_borrowed; + 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++; + } + return c; +} + +void scr_closure_release(ScrClosure *c); /* releases the boxes; NULL-tolerant */ + +/* The identity root of a function value (NULL-tolerant). */ +static inline ScrClosure *scr_closure_identity(ScrClosure *c) { + return c != NULL && c->identity != NULL ? c->identity : c; +} + +/* JS function identity: two closures are the same function when their + * identity roots match. */ +bool scr_closure_identity_equal(ScrClosure *a, ScrClosure *b); + +/* Mark `adapter` as a signature view of `original` (both borrowed); + * `length` is the original's arity when it is a root. */ +void scr_closure_adopt_identity(ScrClosure *adapter, ScrClosure *original, uint32_t length); + +/* Mark `adapter` as a typed view of a dynamic function value (both + * borrowed). Other callables keep the adapter as its own identity. */ +void scr_dyn_adopt_identity(ScrClosure *adapter, const struct ScrDyn *value); + +#endif /* SCR_CLOSURE_H */ diff --git a/packages/runtime/src/scr_console.h b/packages/runtime/src/scr_console.h new file mode 100644 index 000000000..10491d053 --- /dev/null +++ b/packages/runtime/src/scr_console.h @@ -0,0 +1,77 @@ +/* scriptc runtime — console (scr_console.c). Included by scr_runtime.h, which + * says where new declarations go. */ +#ifndef SCR_CONSOLE_H +#define SCR_CONSOLE_H + +#include "scr_http2.h" + +/* ── console ──────────────────────────────────────────────────────────── */ + +enum { + SCR_ARG_F64, /* a number argument: inspect rendering (-0 prints "-0") */ + SCR_ARG_STR, /* a string, verbatim */ + SCR_ARG_BOOL, /* true / false */ + SCR_ARG_NUM, /* a number inside a string concatenation: String() rendering + * (-0 prints "0") — scr_console_*_parts only */ + SCR_ARG_KIND = 0xff, + SCR_ARG_GLUE = 0x100, /* flag: this part continues the previous argument + * (no separating space) — scr_console_*_parts only */ +}; + +typedef struct { + int tag; /* SCR_ARG_KIND bits, optionally | SCR_ARG_GLUE */ + 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); +/* The same lines where the compiler passed a string-concatenation argument + * (template literal, `"a" + n`) as its parts — glued with SCR_ARG_GLUE and + * numbers as SCR_ARG_NUM — so the joined string is never materialized. + * Separate entry points: programs using these tags cannot link against a + * runtime that does not understand them. */ +void scr_console_log_parts(size_t n, const ScrLogArg *args); +void scr_console_error_parts(size_t n, const ScrLogArg *args); +/* A Console instance's line on fd 1 or 2, without the global console's + * group indentation. */ +void scr_console_write_fd(int fd, size_t n, const ScrLogArg *args); +/* The global console's line rewrite while a group is open (scr_console.c; + * installed by scr_console_group): the indented heap copy of a rendered + * line, its length in *used. */ +extern SCR_TL char *(*scr_console_indenter)(const char *line, size_t *used); + +/* The global console's remaining members (scr_console_native.c). Arguments + * are borrowed; string results are +1. */ +ScrStr *scr_console_label(ScrDyn *label); +void scr_console_count(ScrStr *label); +ScrStr *scr_console_count_reset(ScrDyn *label); +ScrStr *scr_console_time(ScrStr *label); +ScrStr *scr_console_time_end(ScrStr *label); +ScrStr *scr_console_time_log(ScrStr *label); +void scr_console_group(double delta); +void scr_console_clear(void); +void scr_console_table(ScrDyn *data, ScrDyn *properties); +ScrStr *scr_console_dir_dyn(ScrDyn *value, double depth); +/* console.table's cell rendering (scr_inspect.c): Node's _inspect — depth + * 0 (-1 for objects with more than two keys), maxArrayLength 3, breakLength + * Infinity. +1. */ +ScrStr *scr_insp_table_cell(ScrDyn *value); +/* Node's getStringWidth over UTF-8 text (the non-ICU tables). */ +size_t scr_insp_display_width(const char *data, size_t len); +/* process.emitWarning(message) with the default Warning type, emitted on + * the next tick like Node's; an empty message emits nothing + * (scr_async_dyn.c). Borrows. */ +void scr_process_emit_warning_text(ScrStr *message); + +bool scr_fetch_web_is(const ScrDyn *value, const ScrStr *name); +bool scr_fetch_stream_is(const ScrDyn *value); + +#endif /* SCR_CONSOLE_H */ diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h new file mode 100644 index 000000000..1ec0fa740 --- /dev/null +++ b/packages/runtime/src/scr_core.h @@ -0,0 +1,399 @@ +/* scriptc runtime — libc shims, runtime instance state, process entry, the + * trap funnel, library mode and host-callback channels (scr_console.c, + * scr_library.c, scr_context.c). Included by scr_runtime.h, which says where + * new declarations go. */ +#ifndef SCR_CORE_H +#define SCR_CORE_H + +#include /* va_list in the emitter listener adapters */ +#include +#include +#include +#include /* memcpy in the inline slot accessors */ +#include /* 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_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 + +/* ── runtime instance 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. Worker executables use the same isolation for their + * collectors, caches and scheduler state. Expands to nothing for ordinary + * executables and classic libraries. Immutable tables stay shared. */ +#if (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) || defined(SCR_WORKERS) +#define SCR_TL _Thread_local +#else +#define SCR_TL +#endif + +/* Entry points that only run when a JS exception is being raised. noinline + * keeps the throw machinery out of every caller: the LLVM helper never + * imports noinline runtime bodies into program code, so a compiled function + * whose only slow path throws stays small enough to inline and needs no + * callee-saved registers on its fast path. cold tells the C compiler that + * runtime paths reaching them are unlikely. */ +#if defined(__GNUC__) || defined(__clang__) +#define SCR_COLD __attribute__((cold, noinline)) +#else +#define SCR_COLD +#endif + +/* Digest compression functions start on a cache line. The release + * runtime keeps the compiler's default function alignment, so without it + * their speed moves with the size of unrelated code before them. WASM + * code has no addresses to align. */ +#if (defined(__GNUC__) || defined(__clang__)) && !defined(__wasm__) +#define SCR_DIGEST_ALIGNED __attribute__((aligned(64))) +#else +#define SCR_DIGEST_ALIGNED +#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_v20(void); +void scr_runtime_workers_v8(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); + +/* glibc added arc4random_buf in 2.36. Older GNU targets use the same kernel + * CSPRNG as the musl shim, with no global state or persistent descriptor. */ +#ifdef __GLIBC__ +#if !__GLIBC_PREREQ(2, 36) +#include +#include +#include +static inline void scr_glibc_random_buf(void *buf, size_t n) { + unsigned char *p = buf; + while (n > 0) { + ssize_t got = getrandom(p, n, 0); + if (got > 0) { + p += (size_t)got; + n -= (size_t)got; + } else if (got < 0 && errno == EINTR) { + continue; + } else { + scr_trap("scriptc: getrandom failed\n"); + } + } +} +#define arc4random_buf scr_glibc_random_buf +#endif +#endif +_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 unchanged. */ +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: + * + * (*)(void *ctx, ) + * + * 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 unchanged — 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 — rounding + * without a diagnostic 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) +#elif defined(SCR_WORKERS) +/* Executable workers drain their own cleanup stack before their TLS dies. + * Registering their callbacks with libc would execute them on the process + * exit thread against a different runtime instance. */ +int scr_context_atexit(void (*fn)(void)); +void scr_context_cleanup(void); +void scr_context_enter(uint64_t thread_id); +uint64_t scr_context_thread_id(void); +void scr_context_stop_flag(const void *flag); +void scr_context_stop(int code); +bool scr_context_stopping(void); +bool scr_context_checkpoint(void); +bool scr_context_quiet(void); +#if !defined(__APPLE__) && !defined(_WIN32) && !defined(__CYGWIN__) && !defined(__wasi__) +/* ELF worker executables (the LLVM emitter's executableTls targets): every + * script thread owns an exact pending word in the executable's static TLS + * block, which emitted pending checks load with one thread-pointer-relative + * access and no out-of-line slow path (scr_exception.c). */ +#define SCR_EXC_FLAG 1 +#define SCR_EXC_FLAG_PENDING 1u /* the active cell's kind is not NONE */ +#define SCR_EXC_FLAG_STOP 2u /* the context must stop (never cleared) */ +extern SCR_TL _Atomic uint32_t scr_exc_flag; +/* Requests the stop of the thread that owns `flag` (scr_worker.c holds the + * owner's word only while its script runs). */ +void scr_exc_flag_request_stop(_Atomic uint32_t *flag); +#else +/* The process-wide exception alert that emitted pending checks of other + * worker executables test inline (scr_exception.c): nonzero while some + * script thread may have an exception pending in its active cell or must + * stop. */ +extern _Atomic uint32_t scr_exc_alert; +void scr_exc_alert_add(int delta); /* worker.terminate() references */ +#endif +void scr_exc_alert_thread_exit(void); +void scr_loop_context_shutdown(void); +extern SCR_TL void (*scr_context_report_error)(void); +typedef struct ScrContextEnv ScrContextEnv; +ScrContextEnv *scr_context_env_capture(void); +void scr_context_env_free(ScrContextEnv *environment); +void scr_context_env_enter(ScrContextEnv *environment); +bool scr_context_env_set(const char *name, const char *value); +const char *scr_context_getenv(const char *name); +struct ScrArr *scr_context_env_pairs(void); +#define scr_atexit(fn) scr_context_atexit(fn) +#else +#define scr_atexit(fn) atexit(fn) +#endif /* SCR_LIB */ + +/* Also used by ordinary event-loop builds, where this always returns true. */ +bool scr_context_is_main(void); + +/* Native stack guard. Compiled functions that can recurse compare their + * stack pointer against scr_stack_guard on entry and throw Node's catchable + * RangeError below it. The guard sits a reserve above the lowest usable + * address of the running stack (thread or async fiber), leaving room for the + * throw, unwinding, finally blocks, catch handlers and the uncaught-error + * report. Zero (the default) disables the check on that stack. */ +extern SCR_TL uintptr_t scr_stack_guard; +void scr_stack_guard_init(void); +SCR_COLD void scr_stack_overflow(void); +#if defined(SCR_WORKERS) && !defined(SCR_LIB) && defined(__APPLE__) && \ + (defined(__aarch64__) || defined(__x86_64__)) +/* Darwin worker executables: reading a thread-local variable calls its TLV + * thunk, so the emitted entry checks read the running stack's guard from a + * pthread thread-specific-data slot instead, addressed from the thread + * pointer (TPIDRRO_EL0 on arm64, %gs on x86-64) plus this byte offset. + * scr_stack_guard_init validates the slot on first use; zero means the + * direct read is unavailable and the checks call scr_stack_guard_get. */ +#define SCR_STACK_GUARD_TSD 1 +extern uintptr_t scr_stack_guard_tsd; +uintptr_t scr_stack_guard_get(void); +void scr_stack_guard_set(uintptr_t guard); +#define SCR_STACK_GUARD_SET(guard) scr_stack_guard_set(guard) +#else +#define SCR_STACK_GUARD_SET(guard) (scr_stack_guard = (guard)) +#endif +#if defined(__has_feature) +#if __has_feature(address_sanitizer) +#define SCR_STACK_RESERVE_MAX ((size_t)2 << 20) +#endif +#endif +#ifndef SCR_STACK_RESERVE_MAX +#define SCR_STACK_RESERVE_MAX ((size_t)256 << 10) +#endif +static inline uintptr_t scr_stack_guard_for(uintptr_t low, size_t size) { + size_t reserve = size / 4; + if (reserve > SCR_STACK_RESERVE_MAX) reserve = SCR_STACK_RESERVE_MAX; + return low + reserve; +} +double scr_context_thread_number(void); +double scr_worker_root(void); +int scr_worker_argc(void); +char **scr_worker_argv(void); + +#ifdef SCR_WORKERS +#define scr_getenv(name) scr_context_getenv(name) +#else +#define scr_getenv(name) getenv(name) +#endif + +#endif /* SCR_CORE_H */ diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h new file mode 100644 index 000000000..c5ce66d0a --- /dev/null +++ b/packages/runtime/src/scr_crypto.h @@ -0,0 +1,157 @@ +/* scriptc runtime — node:crypto (scr_lib.c, scr_crypto_async.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_CRYPTO_H +#define SCR_CRYPTO_H + +#include "scr_lib.h" + +/* ── 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); +ScrDyn *scr_crypto_native(void); +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); +/* The option and any-value forms of createHash, copy, update, digest and + * crypto.hash, with Node 24.22's validation and errors. */ +ScrCryptoHash *scr_crypto_hash_new_opts(ScrStr *alg, ScrDyn *options); +ScrCryptoHash *scr_crypto_hash_copy_opts(ScrCryptoHash *h, ScrDyn *options); +ScrCryptoHash *scr_crypto_hmac_new_dyn(ScrStr *alg, ScrDyn *key, ScrDyn *options); +ScrCryptoHash *scr_crypto_hash_update_enc(ScrCryptoHash *h, ScrStr *data, ScrStr *enc); +ScrCryptoHash *scr_crypto_hash_update_dyn(ScrCryptoHash *h, ScrDyn *data, ScrDyn *enc); +ScrDyn *scr_crypto_hash_digest_dyn(ScrCryptoHash *h, ScrDyn *enc); +ScrDyn *scr_crypto_hash_oneshot(ScrStr *alg, ScrDyn *input, ScrDyn *options); +bool scr_crypto_timing_safe_equal(ScrBytes *left, ScrBytes *right); +bool scr_crypto_timing_safe_equal_dyn(ScrDyn *left, ScrDyn *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); +double scr_crypto_random_int_max(double max); +ScrDyn *scr_crypto_random_fill_sync_dyn(ScrDyn *buf, ScrDyn *offset, ScrDyn *size); +double scr_crypto_random_fill_args(ScrBytes *view, bool typed_array, double offset, double size, + int form); +void scr_crypto_random_int_refill(void); +bool scr_crypto_random_int_check(double min, double max, bool min_given, double *range); +bool scr_crypto_random_int_try(double min, double range, double *out); +ScrStr *scr_crypto_random_uuid_opts(ScrDyn *options); +ScrStr *scr_crypto_random_uuid_v7(ScrDyn *options); +ScrBytes *scr_crypto_pbkdf2(ScrBytes *password, ScrBytes *salt, + double iterations, double keylen, ScrStr *digest); +ScrBytes *scr_crypto_pbkdf2_job(ScrBytes *password, ScrBytes *salt, double iterations, + double keylen, ScrStr *digest, const char **failure); +ScrBytes *scr_crypto_hkdf_bytes(ScrStr *digest, ScrBytes *ikm, ScrBytes *salt, + ScrBytes *info, double keylen); +ScrDyn *scr_crypto_hkdf(ScrStr *digest, ScrBytes *ikm, ScrBytes *salt, + ScrBytes *info, double keylen); +ScrBytes *scr_crypto_scrypt(ScrBytes *password, ScrBytes *salt, double keylen, ScrDyn *options); +ScrBytes *scr_crypto_scrypt_derive(ScrBytes *password, ScrBytes *salt, double keylen, + double n, double r, double p, double maxmem); +typedef void (*ScrCryptoBytesFn)(ScrClosure *cb, ScrError *error /* borrowed */, ScrBytes *value /* moves */); +void scr_crypto_defer_bytes(ScrBytes *value /* moves */, ScrError *error /* moves */, ScrClosure *cb /* moves */, + ScrCryptoBytesFn fn); +void scr_crypto_defer_dyn(ScrDyn *value /* moves */, ScrError *error /* moves */, ScrClosure *cb /* moves */, + ScrCryptoBytesFn fn); +/* randomFill and randomInt with callbacks (scr_crypto_async.c). `form` is + * randomFill's argument count before the callback. */ +void scr_crypto_random_fill_async(ScrBytes *buf, double offset, double size, double form, + ScrClosure *cb, ScrCryptoBytesFn fn); +void scr_crypto_random_fill_async_dyn(ScrDyn *buf, ScrDyn *offset, ScrDyn *size, double form, + ScrClosure *cb, ScrCryptoBytesFn fn); +typedef void (*ScrCryptoIntFn)(ScrClosure *cb, double value); +void scr_crypto_random_int_async(double min, double max, double min_given, ScrClosure *cb, + ScrCryptoIntFn 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); +void scr_crypto_hkdf_async(ScrStr *digest, ScrBytes *ikm, ScrBytes *salt, + ScrBytes *info, double keylen, ScrClosure *cb, ScrCryptoBytesFn fn); +void scr_crypto_scrypt_async(ScrBytes *password, ScrBytes *salt, double keylen, + ScrDyn *options, ScrClosure *cb, ScrCryptoBytesFn fn); + +/* The digest primitives beyond MD5/SHA-1/SHA-2 (scr_hash.c). Each final + * reads a copy of its context. */ +typedef struct ScrKeccakCtx { + uint64_t s[25]; + unsigned char buf[200]; + size_t rate; /* bytes absorbed per permutation */ + size_t len; /* buffered bytes */ + unsigned char pad; /* domain bits: 0x06 SHA-3, 0x01 Keccak, 0x1f SHAKE, 0x04 KECCAK-KMAC */ +} ScrKeccakCtx; +void scr_keccak_init(ScrKeccakCtx *ctx, size_t rate, unsigned char pad); +void scr_keccak_update(ScrKeccakCtx *ctx, const unsigned char *data, size_t len); +void scr_keccak_final(const ScrKeccakCtx *ctx, unsigned char *out, size_t len); +typedef struct ScrRipemd160Ctx { + uint32_t h[5]; + uint64_t bytes; + unsigned char buf[64]; + size_t len; +} ScrRipemd160Ctx; +void scr_ripemd160_init(ScrRipemd160Ctx *ctx); +void scr_ripemd160_update(ScrRipemd160Ctx *ctx, const unsigned char *data, size_t len); +void scr_ripemd160_final(const ScrRipemd160Ctx *ctx, unsigned char out[20]); +typedef struct ScrBlake2bCtx { + uint64_t h[8]; + uint64_t t[2]; + unsigned char buf[128]; + size_t len; +} ScrBlake2bCtx; +void scr_blake2b_init(ScrBlake2bCtx *ctx); +void scr_blake2b_update(ScrBlake2bCtx *ctx, const unsigned char *data, size_t len); +void scr_blake2b_final(const ScrBlake2bCtx *ctx, unsigned char out[64]); +typedef struct ScrBlake2sCtx { + uint32_t h[8]; + uint32_t t[2]; + unsigned char buf[64]; + size_t len; +} ScrBlake2sCtx; +void scr_blake2s_init(ScrBlake2sCtx *ctx); +void scr_blake2s_update(ScrBlake2sCtx *ctx, const unsigned char *data, size_t len); +void scr_blake2s_final(const ScrBlake2sCtx *ctx, unsigned char out[32]); +typedef struct ScrSm3Ctx { + uint32_t h[8]; + uint64_t bytes; + unsigned char buf[64]; + size_t len; +} ScrSm3Ctx; +void scr_sm3_init(ScrSm3Ctx *ctx); +void scr_sm3_update(ScrSm3Ctx *ctx, const unsigned char *data, size_t len); +void scr_sm3_final(const ScrSm3Ctx *ctx, unsigned char out[32]); + +size_t scr_crypto_digest_raw(const char *alg, const unsigned char *data, size_t len, + unsigned char out[64]); +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[64]); +/* 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); + +#endif /* SCR_CRYPTO_H */ diff --git a/packages/runtime/src/scr_dc.h b/packages/runtime/src/scr_dc.h new file mode 100644 index 000000000..2cd27a87e --- /dev/null +++ b/packages/runtime/src/scr_dc.h @@ -0,0 +1,79 @@ +/* scriptc runtime — node:diagnostics_channel (scr_dc.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_DC_H +#define SCR_DC_H + +#include "scr_lib.h" + +/* ── 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::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); + +#endif /* SCR_DC_H */ diff --git a/packages/runtime/src/scr_dgram.h b/packages/runtime/src/scr_dgram.h new file mode 100644 index 000000000..2f2d7e642 --- /dev/null +++ b/packages/runtime/src/scr_dgram.h @@ -0,0 +1,72 @@ +/* scriptc runtime — node:dgram and node:dns (scr_dgram.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_DGRAM_H +#define SCR_DGRAM_H + +#include "scr_http2.h" + +/* ── 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)); + +#endif /* SCR_DGRAM_H */ diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h new file mode 100644 index 000000000..47e1875e9 --- /dev/null +++ b/packages/runtime/src/scr_dyn.h @@ -0,0 +1,1071 @@ +/* scriptc runtime — dynamic values: the checked-dynamic tree, native handles, + * dyn promises, the ambient receiver and listener adapters (scr_json.c, + * scr_dyn_handle.c, scr_async_dyn.c). Included by scr_runtime.h, which says + * where new declarations go. */ +#ifndef SCR_DYN_H +#define SCR_DYN_H + +#include "scr_regex.h" +#include "scr_url.h" +#include "scr_symbol.h" +#include "scr_bigint.h" + +/* ── 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 throw the + * "not supported yet" Error instead of returning a wrong result. typeof + * answers "object", truthiness is true, String() renders + * "[object Object]" (Object.prototype.toString — Node's answer for + * these classes), JSON serialization and util.inspect throw the + * "not supported yet" Error. */ + 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 + * 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 throws + * the "not supported yet" Error. 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, + /* 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 = 12, /* tag 11 is retired; preserve the native ABI */ + /* 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 */ + SCR_DYN_SYMBOL, /* native symbol identity, 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, /* ScrEvent — new Event() and AbortSignal's abort events */ + 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, 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_MAP, /* native Map, shared backing */ + SCR_DYNH_DATE, /* JavaScript Date with stable native identity */ + SCR_DYNH_ITERATOR, /* live native array/string/byte iterator */ + SCR_DYNH_CONSOLE, /* stored global console value */ + SCR_DYNH_SEARCH_PARAMS, /* live WHATWG URLSearchParams */ + SCR_DYNH_FETCH_REQUEST, + SCR_DYNH_WORKER, + SCR_DYNH_MESSAGE_PORT, + SCR_DYNH_SHARED_ARRAY_BUFFER, + SCR_DYNH_CONSOLE_INSTANCE, /* node:console Console instance */ + SCR_DYNH_EVENT_TARGET, /* ScrEventTarget — new EventTarget() */ + SCR_DYNH_CUSTOM_EVENT, /* ScrEvent with detail — new CustomEvent() */ + SCR_DYNH_BLOB, /* ScrBlob — new Blob() and Blob#slice() */ + SCR_DYNH_FILE, /* ScrBlob with a name — new File() */ + 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 *); + bool traced; + struct ScrDynTypedCast *next; +} ScrDynTypedCast; + +/* 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 own shared immutable 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 : 1; + /* Snapshot written by trace before visiting children. Collected teardown + * must not inspect the value: an earlier cycle member may already be freed. + * Ordinary mutations need not maintain this collector-only decision. */ + bool value_traced : 1; + /* Ordinary object insertion sets all three attributes true. */ + bool writable; + bool enumerable; + bool configurable; +} ScrDynEntry; + +_Static_assert(sizeof(ScrDynEntry) == (sizeof(void *) == 8 ? 48 : 24), + "LLVM checked-object readers use the target entry stride"); +_Static_assert(offsetof(ScrDynEntry, value) == 2 * sizeof(void *), + "LLVM checked-object readers use the third pointer-sized field"); + +/* Element access for a live typed reference to a native ScrArr, emitted per + * array type by the compiler. The runtime decides presence and length from + * the native array itself; these hooks only convert one element. + * get: +1 dyn view of the element at a dense index whose state is + * SCR_ARR_VALUE, built exactly as the whole-array snapshot builds it. + * set: stores `value` at index (index <= length) when it fits the element + * type and returns true; returns false with no side effects when it + * does not, so the caller can take the snapshot path and report the + * mismatch with its usual diagnostic. */ +typedef struct ScrDynTypedArrayOps { + ScrDyn *(*get)(void *arr, double index); + bool (*set)(void *arr, double index, const ScrDyn *value); +} ScrDynTypedArrayOps; + +/* Field access for a live typed reference to a declared-field record, + * emitted per record shape by the compiler. `names` lists the fields in + * the order the record's snapshot defines them; the compiler emits these + * hooks only for shapes whose names are all ordinary (no array-index + * spellings, no `__proto__`), so that order is also JS own-key order. + * has: whether field `index` is an own property (an absent optional + * field is not); + * get: +1 dyn view of field `index`, built exactly as the snapshot + * builds it. Callers check `has` first. */ +typedef struct ScrDynTypedRecordOps { + size_t count; + ScrStr *const *names; /* immortal field-name strings */ + bool (*has)(void *rec, size_t index); + ScrDyn *(*get)(void *rec, size_t index); + /* Every field is required: `has` always answers true and no key of the + * record can be deleted. */ + bool fixed; + /* The field names as an immortal string[], built on first use by + * for-in over a record with every field present (atomic slot). */ + ScrArr *keys; +} ScrDynTypedRecordOps; + +/* Prototype members of a compiled class seen as `unknown`, emitted per + * class by the compiler: the string names of methods, getters and setters + * declared on the class and its bases, and `constructor`. The checked + * view of an instance holds only its own fields; `in` answers inherited + * members from this table. + * exact: the table of the instance's own class when it may be a + * subclass of the capsule's static class; NULL otherwise. */ +typedef struct ScrDynTypedClassOps { + const struct ScrDynTypedClassOps *(*exact)(void *instance); + size_t count; + ScrStr *const *names; /* immortal member-name strings */ + ScrStr *class_name; /* immortal constructor name; NULL for an anonymous class */ +} ScrDynTypedClassOps; + +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) */ + /* Callbacks keep the optional symbol unit out of symbol-free binaries. */ + struct { ScrSym *value; void (*release)(ScrSym *); ScrStr *(*render)(ScrSym *); ScrStr *(*description)(ScrSym *); } symbol; + struct { size_t len; size_t cap; ScrDyn **items; struct ScrDyn *properties; bool sealed; bool frozen; unsigned char *presence; } arr; /* owned; NULL presence means dense */ + 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); + struct ScrDynObjectIndex *index; /* optional wide-object lookup; owns no values */ + /* On a runtime-built class prototype (e.g. url.Url.prototype): the + * constructor name (static text) util.inspect prints for objects + * inheriting from it (scr_dyn_class_name). NULL otherwise. Only + * prototypes that live until their thread exits set it, and they + * clear it before release, so freelist reuse never sees it. */ + const char *class_name; + } 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 ownership edge is traced by the cycle collector. */ + 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; + ScrDyn *(*materialize)(void *); + void (*commit)(void *, const ScrDyn *); + /* Live native arrays: per-element access that bypasses the whole-array + * snapshot (scr_dyn_typed_ref_bind_array). Declared-field records + * keep their ScrDynTypedRecordOps here instead, flagged by `record` + * (scr_dyn_typed_ref_bind_record), and class instances their + * ScrDynTypedClassOps, flagged by `klass` + * (scr_dyn_typed_ref_bind_class). NULL for other referents. */ + const ScrDynTypedArrayOps *array; + /* 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; + /* One word keeps this, the largest payload, at its previous size. */ + uint32_t type_key_len : 28; + uint32_t traced : 1; /* native referent has a cycle header */ + uint32_t observed : 1; /* native release reports this identity's disposal */ + uint32_t record : 1; /* `array` holds ScrDynTypedRecordOps */ + uint32_t klass : 1; /* `array` holds ScrDynTypedClassOps */ + } 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, + * adapter promise otherwise). */ + ScrPromise *promise; + + 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; + /* Symbol properties have their own descriptor table. Its private string + * keys encode native identities; symbol_keys retains the actual keys in + * insertion order, never exposing those encodings to JavaScript. */ + struct ScrDyn *symbol_properties; + struct ScrDyn *symbol_keys; +}; +/* Compiled class dispatch reads a capsule's instance and key in place. */ +_Static_assert(offsetof(ScrDyn, v.typed_ref.ptr) == 16, "typed_ref.ptr offset"); +_Static_assert(offsetof(ScrDyn, v.typed_ref.type_key) == 16 + 3 * sizeof(void *), + "typed_ref.type_key offset"); + +static inline ScrDyn *scr_dyn_retain(ScrDyn *d) { + if (d->rc != SIZE_MAX) { + d->rc++; + } + return d; +} + +void scr_dyn_release(ScrDyn *d); /* releases the tree recursively; NULL-tolerant */ + +/* The runtime class an object inherits from (util.inspect's constructor + * name, found on its prototype chain), or NULL. */ +static inline const char *scr_dyn_class_name(const ScrDyn *d) { + for (const ScrDyn *p = d->prototype; p; p = p->prototype) + if (p->kind == SCR_DYN_OBJ && p->v.obj.class_name) return p->v.obj.class_name; + return NULL; +} + +/* The element hooks of a live array capsule, or NULL (including for record + * and class capsules, which keep their hooks in the same slot). */ +static inline const ScrDynTypedArrayOps *scr_dyn_typed_ref_array_ops(const ScrDyn *d) { + return d->v.typed_ref.record || d->v.typed_ref.klass ? NULL : d->v.typed_ref.array; +} + +/* 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); +/* for-in keys as a native string[] (+1); NULL with an exception pending. */ +ScrArr *scr_dyn_for_in_key_strs(const ScrDyn *v); +bool scr_dyn_for_in_stable(const ScrDyn *v); +/* Object.keys as a native string[] (+1); NULL with an exception pending. */ +ScrArr *scr_dyn_obj_key_strs(const ScrDyn *v); +/* Object.entries as owned (key, value) pairs: compiled code moves each + * pair into a native [string, any] row, then frees the snapshot with + * scr_dyn_entry_pairs_free. NULL with an exception pending. */ +typedef struct { + ScrStr *key; + ScrDyn *value; +} ScrDynEntryPair; +typedef struct { + size_t count; + ScrDynEntryPair pairs[]; +} ScrDynEntryPairs; +ScrDynEntryPairs *scr_dyn_obj_entry_pairs(const ScrDyn *v); +void scr_dyn_entry_pairs_free(ScrDynEntryPairs *pairs); +/* Object.values as a native array of owned checked values in own-key order + * (+1), for results compiled code only iterates, indexes or reads. NULL with + * an exception pending. */ +ScrArr *scr_dyn_obj_value_list(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_own_keys(const ScrDyn *value); +ScrDyn *scr_dyn_get_own_property_names(const ScrDyn *value); +ScrDyn *scr_dyn_get_own_property_symbols(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); +ScrDyn *scr_dyn_object_rest(const ScrDyn *source, const ScrDyn *excluded); +ScrDyn *scr_dyn_copy_property_descriptors(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); +ScrDyn *scr_dyn_native_map(ScrMap *value); /* borrowed backing, +1 box */ +bool scr_dyn_native_map_is(const ScrDyn *value); +void scr_regex_reset_last_index(ScrRegex *re, double index); +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_search_params(ScrSearchParams *value); +bool scr_dyn_native_search_params_is(const ScrDyn *value); +bool scr_dyn_native_date_is(const ScrDyn *value); +ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments); +double scr_dyn_native_date_value(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_symbol(ScrSym *value); /* borrows and retains value */ +ScrSym *scr_sym_well_known(ScrStr *name); +ScrDyn *scr_dyn_symbol_get(const ScrDyn *value, const ScrStr *key); +ScrDyn *scr_dyn_symbol_key_get(const ScrDyn *value, const ScrDyn *key, bool optional); +void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored); +void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key); +bool scr_dyn_has_key_computed(const ScrDyn *value, const ScrDyn *key); +bool scr_dyn_has_own_computed(const ScrDyn *value, const ScrDyn *key); +bool scr_dyn_property_is_enumerable_computed(const ScrDyn *value, const ScrDyn *key); +void scr_dyn_key_delete_computed(ScrDyn *value, const ScrDyn *key, bool strict); +ScrStr *scr_dyn_string_constructor(const ScrDyn *value); +ScrDyn *scr_dyn_property_key_value(const ScrDyn *value); +ScrDyn *scr_dyn_to_numeric(const ScrDyn *value); +ScrDyn *scr_dyn_increment(const ScrDyn *value, bool increment); +/* Internal constructor consumes a retained symbol with its native operations. */ +ScrDyn *scr_dyn_symbol_ref(ScrSym *value, void (*release)(ScrSym *), ScrStr *(*render)(ScrSym *), ScrStr *(*description)(ScrSym *)); +ScrDyn *scr_dyn_new_str(ScrStr *s); +ScrDyn *scr_dyn_new_arr(void); +ScrDyn *scr_dyn_array_constructor(void); +bool scr_dyn_arr_has_index(const ScrDyn *arr, size_t index); +void scr_dyn_arr_push_hole(ScrDyn *arr); +ScrDyn *scr_dyn_new_obj(void); +ScrDyn *scr_dyn_iterator(const ScrDyn *value, const ScrStr *spell); +ScrDyn *scr_dyn_iterator_optional(const ScrDyn *value); +void scr_dyn_install_iterator_symbol(ScrDyn *key); +void scr_dyn_install_async_iterator_symbol(ScrDyn *key); +ScrDyn *scr_dyn_array_values_function(void); +ScrDyn *scr_dyn_array_from_iterator(const ScrDyn *value); +ScrDyn *scr_dyn_iterator_result(ScrDyn *value); +/* Internal consumption of a captured builtin next method. Check can_step + * once before entering the loop. step returns an owned ordinary value; + * read step_done immediately afterward, before running any user code. */ +bool scr_dyn_iterator_can_step(const ScrDyn *iterator, const ScrDyn *next); +ScrDyn *scr_dyn_iterator_step(const ScrDyn *iterator); +bool scr_dyn_iterator_step_done(const ScrDyn *iterator); +/* 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); +bool scr_dyn_is_callable(const ScrDyn *value); +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_bytes_constructor(const ScrStr *name); +bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee); +bool scr_dyn_data_view_is(const ScrDyn *value); +ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what); +ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what); +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 *)); +ScrDyn *scr_dyn_new_typed_ref_observed( + 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 */ +/* What string conversion renders for a capsule (+1): its materialized + * view, or, for an EventTarget or Event subclass without its own + * toString, the "[object EventTarget]" / "[object Event]" string of its + * inherited @@toStringTag. */ +ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d); +void scr_dyn_typed_ref_commit(ScrDyn *d); +/* Attach per-element access to a live array capsule (idempotent). Indexed + * reads, `length`, in-range writes, `in`, own-property probes, and array + * iteration then touch one element instead of rebuilding the snapshot. */ +void scr_dyn_typed_ref_bind_array(ScrDyn *d, const ScrDynTypedArrayOps *ops); +/* Attach per-field access to a live record capsule (idempotent). While the + * capsule has no cached checked view (nothing has added properties or + * changed its prototype), keyed reads of declared fields, `in`/hasOwn, + * for-in, Object.keys/values/entries, typeof and Array.isArray answer from + * the native record, creating only the field values they return. */ +void scr_dyn_typed_ref_bind_record(ScrDyn *d, const ScrDynTypedRecordOps *ops); +/* Attach the prototype member names of a class instance capsule + * (idempotent), so `in` sees its methods and accessors. */ +void scr_dyn_typed_ref_bind_class(ScrDyn *d, const ScrDynTypedClassOps *ops); +/* The constructor name of a class instance capsule's own class, for + * util.inspect; NULL when the capsule is not a bound class instance, its + * class is anonymous, or its prototype was replaced. */ +const ScrStr *scr_dyn_typed_ref_class_name(const ScrDyn *d); +/* Fast keyed read on a live array capsule: +1 value for `length` and for + * present or explicitly-undefined elements; NULL when the answer needs the + * snapshot (holes, out-of-range, non-index names, or no bound ops). */ +ScrDyn *scr_dyn_typed_ref_key_get(const ScrDyn *d, const ScrStr *key); +/* +1 view of an own element of a bound live array capsule; NULL for a hole + * or an index at or past its length. */ +ScrDyn *scr_dyn_typed_ref_element(const ScrDyn *d, size_t index); +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 *), bool traced); +/* 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); +bool scr_dyn_typed_array_is(const ScrDyn *d, int elem); +bool scr_dyn_util_type_is(const ScrDyn *value, const ScrStr *probe); +ScrDyn *scr_dyn_util_type_value(const ScrDyn *value, const ScrStr *probe); +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 (worker + * executables keep the hook in scr_thread_hot). */ +#ifndef SCR_WORKERS +extern SCR_TL void (*scr_weak_dispose_hook)(void *); +#endif +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); +ScrDyn *scr_dyn_array_prototype(void); /* +1, installs native method adapters */ +ScrDyn *scr_dyn_array_prototype_base(void); /* +1, shared native array identity */ +/* 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); +/* The `in` operator: scr_dyn_has_key plus members inherited from builtin + * prototypes and compiled class prototypes. */ +bool scr_dyn_in(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 " 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); +ScrDyn *scr_dyn_object_prototype(void); +ScrDyn *scr_dyn_function_apply(void); +ScrDyn *scr_dyn_reflect_apply(ScrDyn *target, ScrDyn *receiver, ScrDyn *arguments_list); +ScrDyn *scr_dyn_apply_array_like(const ScrDyn *target, const ScrDyn *receiver, const ScrDyn *arguments, const char *what); +bool scr_dyn_same_value(const ScrDyn *left, const ScrDyn *right); +ScrDyn *scr_dyn_builtin_method(const ScrStr *prototype, const ScrStr *method); +ScrStr *scr_dyn_to_string_argument(const ScrDyn *d, const ScrDyn *argument, const ScrStr *what); +ScrStr *scr_num_to_string_radix(double value, const ScrDyn *radix); +/* 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); +ScrStr *scr_regex_replace_callback(ScrStr *subject, ScrRegex *regex, ScrDyn *callback, bool all); +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); +ScrBigInt *scr_dyn_bigint_constructor(const ScrDyn *d); +ScrBigInt *scr_dyn_bigint_coerce(const ScrDyn *d); +ScrDyn *scr_dyn_add(const ScrDyn *left, const ScrDyn *right); /* borrowed; +1 or NULL/pending */ +ScrDyn *scr_dyn_arithmetic(const ScrDyn *left, const ScrDyn *right, const ScrStr *operation); +bool scr_dyn_compare(const ScrDyn *left, const ScrDyn *right, const ScrStr *operation); +ScrDyn *scr_dyn_bitwise(const ScrDyn *left, const ScrDyn *right, const ScrStr *operation); + +/* `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); +void scr_error_register_dyn(const ScrVt *vt, ScrDyn *(*box)(void *)); +ScrDyn *scr_error_dyn_fields(const ScrError *e); +void scr_error_commit_dyn(ScrError *e, const ScrDyn *view); +/* 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 "not supported yet" Error; + * a name the kind's prototype lacks throws Node's " 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_prepare_method(ScrDyn *recv, const char *method); +ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); +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 "not supported yet" 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); +ScrDyn *scr_dyn_define_prototype_props(ScrDyn *target, ScrDyn *descs, ScrDyn *protected_keys); +/* 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_key(const ScrDyn *value, const ScrStr *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 + * " 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; +ScrSearchParams *scr_dyn_native_search_params_check(const ScrDyn *value, const ScrDynPath *path); +ScrSearchParams *scr_sp_new_checked(const ScrDyn *value); +ScrMap *scr_dyn_native_set_check(const ScrDyn *value, const ScrDynPath *path); +ScrMap *scr_dyn_native_map_check(const ScrDyn *value, const ScrDynPath *path); +bool scr_dyn_native_collection_is(const ScrDyn *value, int map, const char *type); +ScrMap *scr_dyn_native_collection_check(const ScrDyn *value, int map, const char *type, const ScrDynPath *path); +ScrStr *scr_url_checked_to_path(const ScrDyn *value); +ScrStr *scr_url_checked_to_path_options(const ScrDyn *value, const ScrDyn *options); +ScrBytes *scr_url_checked_to_path_buffer(const ScrDyn *value, const ScrDyn *options); +ScrUrl *scr_url_checked_from_path(const ScrDyn *value, const ScrDyn *options); +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 at , + * got " (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 "not supported yet" + * Error; names the class's surface never had throw Node's + * " 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 "not supported yet" Error 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 "not supported yet" Error — Node would take an + * expando, but a 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 throws the + * "not supported yet" Error). 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); + /* A live native iterator step; index advances only when a value is emitted. */ + ScrDyn *(*iter_step)(void *h, double *index, unsigned selection); + /* util.inspect renders ` ` for the dyn object this answers + * (+1); NULL = the class has no inspection, and util.inspect throws the + * "not supported yet" Error. */ + ScrDyn *(*inspect_view)(void *h); + /* structuredClone of the handle (+1 new box); NULL = DataCloneError. */ + ScrDyn *(*clone)(void *h); + /* JSON.stringify writes `{}` (the class keeps its state in internal + * slots behind prototype getters) instead of the unsupported fence. */ + bool json_empty_object; + /* Past the depth limit util.inspect prints `[cls]` (the class's + * inspector returns `this`) instead of the bare class name. */ + bool inspect_bracketed_past_depth; +} ScrDynHandleOps; +ScrDyn *scr_dyn_native_handle_iterator(ScrDyn *source, unsigned selection); + +/* ── 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 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 SCR_TL 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); +/* Promise.all / allSettled / any over a spread dyn array (borrowed); + * `reader` (borrowed) reads a program class instance's `then`. +1. */ +ScrPromise *scr_dyn_promise_all(const ScrDyn *entries, const ScrDyn *reader); +ScrPromise *scr_dyn_promise_all_settled(const ScrDyn *entries, const ScrDyn *reader); +ScrPromise *scr_dyn_promise_any(const ScrDyn *entries, const ScrDyn *reader); + +/* Native predicates for typed reference capsules and boxed errors. */ +bool scr_dyn_ref_typeof_is(const ScrDyn *d, const char *name); +bool scr_dyn_ref_is_array(const ScrDyn *d); +bool scr_dyn_ref_is_error(const ScrDyn *d); +bool scr_dyn_is_nullish(const ScrDyn *d); + +/* ── 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 without extra code. */ +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 */ +/* A class's typed-ref construction recipe, emitted once per class as a + * constant (kind: 0 plain, 1 observed, 2 traced capsule). `view` names the + * live view bound to the new capsule, as eager boxing binds it: 0 none, + * 1 array ops, 2 record ops, 3 class ops (`ops`). */ +typedef struct { + void *(*retain)(void *); + void (*release)(void *); + const char *type_key; + size_t type_key_len; + ScrDyn *(*materialize)(void *); + void (*commit)(void *, const ScrDyn *); + int kind; + int view; + const void *ops; +} ScrDynTypedRefRecipe; +/* A class instance as the window's receiver, boxed with its recipe only when + * read. ptr is BORROWED: the caller holds the instance across the window. */ +void scr_dyn_this_push_typed(void *ptr, const ScrDynTypedRefRecipe *recipe); +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); +ScrDyn *scr_gen_receiver(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 throws the "not + * supported yet" Error). */ +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. */ +SCR_COLD void scr_throw_arg_type(const ScrStr *argname, const ScrStr *expected, const ScrDyn *got); +SCR_COLD 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. */ +SCR_COLD void scr_throw_prop_type(const ScrStr *name, const ScrStr *expected, const ScrDyn *got); +SCR_COLD 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. */ +SCR_COLD 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); +ScrStr *scr_str_inspect_quoted(const ScrStr *s, bool truncate); +/* A ladder's post-validation refuse: throws the compiler-rendered SC2020 + * statement-fence text verbatim (Node's validation errors run first). */ +SCR_COLD 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); + +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 + +#endif /* SCR_DYN_H */ diff --git a/packages/runtime/src/scr_error.h b/packages/runtime/src/scr_error.h new file mode 100644 index 000000000..103a6ed49 --- /dev/null +++ b/packages/runtime/src/scr_error.h @@ -0,0 +1,265 @@ +/* scriptc runtime — class hierarchies, class objects, and the Error, + * DOMException and AggregateError objects (scr_error.c, scr_object.c). + * Included by scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_ERROR_H +#define SCR_ERROR_H + +#include "scr_string.h" + +/* ── 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; + +/* ── 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 *prototype_data; + struct ScrBox *static_data; + struct ScrBox *caps[]; +} ScrClassObj; + +static inline ScrClassObj *scr_classobj_retain(ScrClassObj *c) { + if (c && c->rc != SIZE_MAX) { + c->rc++; + } + return c; +} +void scr_classobj_release(ScrClassObj *c); +void scr_classobj_clear_properties(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); +struct ScrDyn *scr_dyn_class_prototype(struct ScrDyn *value, struct ScrDyn *base); +struct ScrDyn *scr_dyn_class_base_prototype(struct ScrDyn *constructor); +struct ScrDyn *scr_dyn_class_inherit(struct ScrDyn *constructor, struct ScrDyn *base); +void scr_dyn_class_super(struct ScrDyn *constructor, struct ScrDyn *receiver, struct ScrDyn *arguments); +struct ScrDyn *scr_dyn_assign_prototype(struct ScrDyn *target, struct ScrDyn *sources, struct ScrDyn *protected_keys); +struct ScrDyn *scr_dyn_bag_get(struct ScrDyn *bag, ScrStr *key, struct ScrDyn *receiver); +struct ScrDyn *scr_dyn_reflect_get(struct ScrDyn *target, struct ScrDyn *key, struct ScrDyn *receiver); +bool scr_dyn_reflect_set(struct ScrDyn *target, struct ScrDyn *key, struct ScrDyn *value, struct ScrDyn *receiver); +bool scr_dyn_reflect_define(struct ScrDyn *receiver, struct ScrDyn *key, struct ScrDyn *value); +bool scr_dyn_abstract_eq(const struct ScrDyn *left, const struct ScrDyn *right); +int scr_dyn_function_kind(const struct ScrDyn *value); +void scr_dyn_bag_set(struct ScrDyn *bag, ScrStr *key, struct ScrDyn *value, struct ScrDyn *receiver); +/* 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); + +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 */ + bool message_present; /* super() leaves the inherited message property visible */ + bool message_enumerable; /* assignment creates an enumerable own property */ + bool name_present; + bool name_enumerable; + ScrStr *stack_frames; /* captured native source frames */ + ScrStr *stack; /* lazily formatted, cached stack */ + double system_errno; /* present exactly when system_call is non-NULL */ + ScrStr *system_call; + ScrStr *system_path; + ScrStr *system_dest; +} 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_AGGREGATE = 8, /* AggregateError — ScrAggregateError, the wider layout */ + SCR_ERR_COUNT = 9, +}; + +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; + bool message_present; + bool message_enumerable; + bool name_present; + bool name_enumerable; + ScrStr *stack_frames; + ScrStr *stack; + double system_errno; + ScrStr *system_call; + ScrStr *system_path; + ScrStr *system_dest; + 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. */ +SCR_COLD void scr_throw_domex(const char *name, const char *message); +SCR_COLD void scr_throw_domex_str(const char *name, ScrStr *message); + +/* AggregateError: the ScrError prefix plus the `errors` list (a dyn + * array; NULL until set). Like DOMException's slots it is HIDDEN from + * the compiler's IR field list (user `extends AggregateError` is fenced) + * and read through scr_aggregate_errors. The dyn half (construction from + * an iterable's elements, the list itself) lives in scr_json.c; the + * teardown drops the list through the hook it installs. */ +typedef struct ScrAggregateError { + ScrError base; + struct ScrDyn *errors; +} ScrAggregateError; +ScrError *scr_aggregate_alloc(void); +void scr_aggregate_install_errors_drop(void (*fn)(void *obj)); +/* new AggregateError(errors, message?, options?) runs in spec order: + * scr_aggregate_new installs the message and cause (both borrowed, dyn + * undefined when absent; +1, NULL with an exception pending when the + * message conversion throws), then the caller iterates `errors` and + * hands the list to scr_aggregate_set_errors. */ +ScrError *scr_aggregate_new(const struct ScrDyn *message, const struct ScrDyn *options); +void scr_aggregate_set_errors(ScrError *e, struct ScrDyn *errors); /* both borrowed */ +struct ScrDyn *scr_aggregate_errors(ScrError *e); /* +1 */ + +/* 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. */ +SCR_COLD 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_define_cause(ScrError *e, struct ScrDyn *value); +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); +void scr_error_install_message_reader(ScrStr *(*reader)(ScrError *)); +void scr_error_install_name_reader(ScrStr *(*reader)(ScrError *)); +ScrStr *scr_error_default_name(const ScrError *e); +ScrStr *scr_error_stack(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 takes ownership of both strings and picks the + * builtin vtable whose class name matches `name` (Error otherwise). */ +SCR_COLD void scr_throw_error(int kind, ScrStr *message); +SCR_COLD void scr_throw_error_msg(int kind, const char *message, size_t len); +SCR_COLD void scr_throw_error_named(ScrStr *name, ScrStr *message); +/* A read of a declare-d const nothing defines: throws Node's catchable + * ReferenceError " is not defined". Borrows name; always throws. */ +SCR_COLD 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); +SCR_COLD 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. */ +SCR_COLD void scr_throw_node_coded(double kind, const ScrStr *code, const ScrStr *msg); + +#endif /* SCR_ERROR_H */ diff --git a/packages/runtime/src/scr_event_target.h b/packages/runtime/src/scr_event_target.h new file mode 100644 index 000000000..f218bbc9f --- /dev/null +++ b/packages/runtime/src/scr_event_target.h @@ -0,0 +1,164 @@ +/* scriptc runtime — EventTarget, Event and CustomEvent (scr_event_target.c). + * Included by scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_EVENT_TARGET_H +#define SCR_EVENT_TARGET_H + +#include "scr_dyn.h" + +/* ── EventTarget / Event / CustomEvent (scr_event_target.c) ─────────── + * Node's internal/event_target over checked-dynamic values. A target's + * listeners live in a ScrEventTargetCore: per event type, a linked list + * in registration order whose nodes keep their `next` link after removal, + * so a dispatch that cached a removed node continues where Node's does. + * Plain `new EventTarget()` values are SCR_DYNH_EVENT_TARGET handles; + * AbortSignal embeds a core of its own (MaxListeners 0, the `onabort` + * event-handler attribute). Events are ScrEvent handles (SCR_DYNH_EVENT, + * SCR_DYNH_CUSTOM_EVENT for CustomEvent). Listener exceptions, and + * rejections of a listener's returned promise or of a handleEvent call, + * are reported as uncaught exceptions from a nextTick, like Node. */ +typedef struct ScrEtRoot ScrEtRoot; +typedef struct ScrEtHandler ScrEtHandler; +typedef struct ScrEtHook ScrEtHook; +typedef struct ScrEventTargetCore { + ScrEtRoot *roots; /* per event type, in first-add order */ + ScrEtHandler *handlers; /* event-handler attributes (`onabort`) */ + ScrEtHook *hooks; /* options.signal removal hooks aimed here (weak) */ + /* A compiled subclass instance's own capsule (+1), kept from its first + * method call so every later `this` and event target is the same value. + * The instance holds the core, so this is a cycle the collector reclaims + * through the scr_event_target_state_* helpers. NULL otherwise. */ + ScrDyn *self; + double max_listeners; /* 0 = no leak warning (AbortSignal) */ + bool warned; + const char *cls; /* the warning's target name ("EventTarget") */ + bool tracked; /* in the exit teardown registry */ + struct ScrEventTargetCore *tracked_prev, *tracked_next; +} ScrEventTargetCore; + +typedef struct ScrEvent { + size_t rc; + ScrStr *type; + ScrDyn *target; /* +1, NULL until dispatched */ + ScrDyn *detail; /* CustomEvent's detail (+1); NULL for Event */ + double time_stamp; + bool custom, bubbles, cancelable, composed, trusted; + bool default_prevented, propagation_stopped, stop, dispatching, in_passive; + bool tracked; + struct ScrEvent *tracked_prev, *tracked_next; +} ScrEvent; + +void scr_et_core_init(ScrEventTargetCore *core, double max_listeners, const char *cls); +/* Drops every listener and handler (teardown and target destruction). */ +void scr_et_core_drop(ScrEventTargetCore *core); +/* The EventTarget methods over a core: `self` is the target's own dyn + * value (borrowed — `this` for listeners and the events' target), args + * borrowed. +1 result, or NULL with the exception pending. */ +ScrDyn *scr_et_add_listener(ScrEventTargetCore *core, ScrDyn *const *args, size_t argc); +ScrDyn *scr_et_remove_listener(ScrEventTargetCore *core, ScrDyn *const *args, size_t argc); +ScrDyn *scr_et_dispatch_event(ScrEventTargetCore *core, ScrDyn *self, ScrDyn *const *args, size_t argc); +/* Dispatches an event handle (borrowed) without the argument checks. */ +void scr_et_dispatch(ScrEventTargetCore *core, ScrDyn *self, ScrDyn *event); +/* `on` attributes (Node's defineEventHandler): get answers +1 (null + * when unset); set registers the wrapper listener on first assignment. */ +ScrDyn *scr_et_handler_get(ScrEventTargetCore *core, const char *type); +void scr_et_handler_set(ScrEventTargetCore *core, const char *type, const ScrDyn *value); +/* A trusted (isTrusted) Event handle of the given type, +1. */ +ScrDyn *scr_event_new_trusted(const char *type); +/* new EventTarget() / new Event(type, options) / new CustomEvent(type, + * options): `args` is the argument vector as one dyn array (borrowed). + * +1 handle, or NULL with the exception pending. */ +ScrDyn *scr_event_target_new(void); +ScrDyn *scr_event_new(ScrDyn *args); +ScrDyn *scr_custom_event_new(ScrDyn *args); +/* `v instanceof Event` (kind 0), `CustomEvent` (1), `EventTarget` (2). */ +bool scr_event_is(const ScrDyn *v, double kind); +/* The AbortSignal unit's view of its handles for options.signal: the + * signal's core and aborted flag, or NULL for a non-signal value. + * Installed by the AbortSignal unit; NULL when no signal can exist. */ +extern SCR_TL ScrEventTargetCore *(*scr_et_signal_core_fn)(const ScrDyn *d, bool *aborted); +/* The compiled `listener.handleEvent` read (dyn → dyn; the runtime keeps a + * reference), installed by %main when a program class declares + * handleEvent: dispatch reads a capsule listener's handleEvent through it, + * which reaches the class's compiled methods. Without it a capsule's own + * view answers. */ +void scr_event_handle_event_reader(ScrDyn *reader); +/* events.addAbortListener(signal, listener): +1 disposable, or NULL. */ +ScrDyn *scr_events_add_abort_listener(ScrDyn *signal, ScrDyn *listener); + +/* Compiled subclasses (`class Bus extends EventTarget`, `class Ping + * extends Event`): an instance lays out like a compiler-emitted hierarchy + * class (rc, vt, then this prefix, then the subclass fields — the + * ScrEmitter arrangement). super() creates the engine state behind the + * prefix pointer; `cls` is the concrete class's name, stamped by the + * emitted allocation (the leak warning and util.inspect print it). The + * instances' checked-dynamic capsules carry the RC adapters below, which + * is how the engine recognizes them: a target's capsule is the `this` of + * its listeners and the events' target, and an event's capsule is what + * its listeners receive. */ +typedef struct ScrEventTargetObj { + size_t rc; + const ScrVt *vt; + ScrEventTargetCore *core; /* NULL until super() */ + const char *cls; +} ScrEventTargetObj; + +typedef struct ScrEventObj { + size_t rc; + const ScrVt *vt; + ScrEvent *ev; /* NULL until super() */ + const char *cls; +} ScrEventObj; + +void *scr_event_target_obj_retain_v(void *o); +void scr_event_target_obj_release_v(void *o); /* dispatches through vt */ +void *scr_event_obj_retain_v(void *o); +void scr_event_obj_release_v(void *o); +void *scr_event_target_obj_retain(void *o); +void scr_event_target_obj_release(void *o); +void *scr_event_obj_retain(void *o); +void scr_event_obj_release(void *o); +/* super() / super(type, options): `args` is the argument vector as one + * dyn array (borrowed); a bad type or options value leaves Node's + * exception pending. */ +void scr_event_target_obj_init(void *obj); +void scr_event_obj_init(void *obj, ScrDyn *args); +void scr_event_obj_init_type(void *obj, ScrStr *type); /* super(type), type borrowed */ +/* The emitted teardown's and collector's prefix helpers (NULL-tolerant): + * trace visits the listener values, `on` handlers and the cached + * capsule of a target, and the target and detail of an event; gcfree + * frees the state without releasing those traced edges. */ +void scr_event_target_state_drop(ScrEventTargetCore *core); +void scr_event_target_state_trace(ScrEventTargetCore *core, ScrTraceVisit visit, void *ctx); +void scr_event_target_state_gcfree(ScrEventTargetCore *core); +void scr_event_state_drop(ScrEvent *ev); +void scr_event_state_trace(ScrEvent *ev, ScrTraceVisit visit, void *ctx); +void scr_event_state_gcfree(ScrEvent *ev); +/* The inherited members on typed receivers: `method`/`key` name an + * EventTarget or Event member, `args` is the argument vector as one dyn + * array, `self` the target's own capsule (borrowed). +1 results, or NULL + * with the exception pending. */ +ScrDyn *scr_event_target_obj_call(void *obj, ScrDyn *self, ScrStr *method, ScrDyn *args); +/* dispatchEvent(event): the bool result, false with the exception pending. */ +bool scr_event_target_obj_dispatch(void *obj, ScrDyn *self, ScrDyn *event); +ScrDyn *scr_event_obj_call(void *obj, ScrStr *method, ScrDyn *args); +ScrDyn *scr_event_obj_get(void *obj, ScrStr *key); +void scr_event_obj_set(void *obj, ScrStr *key, ScrDyn *value); + +/* The same members through a capsule (property reads and writes, method + * calls, util.inspect, string conversion), installed by the EventTarget + * unit with its first value and NULL before. get answers +1 or NULL (not + * an inherited member); set and invoke answer whether they handled the + * member. */ +typedef struct ScrEventRefHooks { + ScrDyn *(*get)(const ScrDyn *d, const ScrStr *key); + bool (*set)(ScrDyn *d, const ScrStr *key, const ScrDyn *value); + bool (*invoke)(ScrDyn *d, const char *method, ScrDyn *const *args, size_t argc, + const char *what, ScrDyn **out); + ScrDyn *(*inspect_view)(const ScrDyn *d, const char **cls); + /* Object.prototype.toString's answer with the inherited @@toStringTag + * ("[object EventTarget]", "[object Event]"), or NULL. */ + const char *(*string_tag)(const ScrDyn *d); +} ScrEventRefHooks; +extern SCR_TL const ScrEventRefHooks *scr_event_ref_hooks; + +#endif /* SCR_EVENT_TARGET_H */ diff --git a/packages/runtime/src/scr_events.h b/packages/runtime/src/scr_events.h new file mode 100644 index 000000000..7dcf9cda0 --- /dev/null +++ b/packages/runtime/src/scr_events.h @@ -0,0 +1,56 @@ +/* scriptc runtime — the events unit: signals, exit listeners and stdin + * (scr_events.c). Included by scr_runtime.h, which says where new + * declarations go. */ +#ifndef SCR_EVENTS_H +#define SCR_EVENTS_H + +#include "scr_child.h" +#include "scr_async.h" + +/* ── 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); + +#endif /* SCR_EVENTS_H */ diff --git a/packages/runtime/src/scr_events_emitter.h b/packages/runtime/src/scr_events_emitter.h new file mode 100644 index 000000000..7078823da --- /dev/null +++ b/packages/runtime/src/scr_events_emitter.h @@ -0,0 +1,149 @@ +/* scriptc runtime — node:events EventEmitter (scr_events_emitter.c). Included + * by scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_EVENTS_EMITTER_H +#define SCR_EVENTS_EMITTER_H + +#include "scr_closure.h" + +/* ── 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 */ +/* Internal owner hook; observes listener presence without running script. */ +void scr_emitter_observe(ScrEmitter *em, + void (*observe)(void *, const ScrStr *, bool), void *context); +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. + * The fixed shims cover the common short tuples. Longer adapters use the + * opaque cursor interface below without depending on the va_list ABI. */ +#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); +/* Invoke cb->fn(cb, cursor). The cursor borrows a private va_list copy for + * this call; each next operation reads one borrowed pointer argument. */ +void scr_ee_inv_args(ScrClosure *cb, va_list ap); +void *scr_ee_arg_next(void *cursor); +/* 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_flex(ScrEmitter *em, ScrDyn *error); +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[] */ +/* events.errorMonitor's event key (immortal). Every name parameter above + * accepts it; the registry matches it by identity, so no string name, + * whatever its bytes, reaches the errorMonitor listeners. */ +ScrStr *scr_emitter_error_monitor(void); +/* 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 SCR_TL void (*scr_emitter_on_hook)(ScrEmitter *em, ScrStr *name); + +#endif /* SCR_EVENTS_EMITTER_H */ diff --git a/packages/runtime/src/scr_exception.h b/packages/runtime/src/scr_exception.h new file mode 100644 index 000000000..f0f706ef8 --- /dev/null +++ b/packages/runtime/src/scr_exception.h @@ -0,0 +1,199 @@ +/* scriptc runtime — exceptions and catch bindings (scr_exception.c). Included + * by scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_EXCEPTION_H +#define SCR_EXCEPTION_H + +#include "scr_stream.h" + +/* ── 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, + /* Noncatchable context shutdown. Owns no payload; generated unwinding + * releases locals while bypassing user catch and finally handlers. */ + SCR_EXC_TERMINATE, +} 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 ScrStackFrame { + struct ScrStackFrame *previous; + const char *text; +} ScrStackFrame; + +void scr_stack_enter(ScrStackFrame *frame, const char *text); +void scr_stack_leave(ScrStackFrame *frame); +ScrStr *scr_stack_capture(void); /* +1, NULL when no source frames exist */ +double scr_stack_limit_get(void); +void scr_stack_limit_set(double limit); + +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; + ScrStackFrame *stack; /* execution state; never moves with an exception */ +} ScrExcCell; + +/* Runtime-internal (fiber machinery in scr_async.c) — never emitted. */ +ScrExcCell *scr_exc_swap_cell(ScrExcCell *cell); /* NULL = main's cell */ +/* The active cell, read inline by generated exception polls (thread-local + * builds use NULL for the main cell until scr_init installs it). */ +extern SCR_TL ScrExcCell *scr_exc_active; +ScrExcCell *scr_exc_current_cell(void); /* the ACTIVE cell */ +/* Runtime code that moved a payload into or out of the active cell (or a + * cell that may be active) without the throw/take/clear entry points. */ +void scr_exc_cell_changed(void); + +bool scr_exc_pending(void); + +#if !((defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) || defined(SCR_WORKERS)) +/* Runtime units test the active cell's kind inline, as emitted exception + * polls do (scr_exc_active is never NULL outside thread-instanced libraries + * and worker executables, which also poll cross-thread termination in + * scr_exc_pending). The out-of-line definition in scr_exception.c stays + * for those lanes and for emitted code's slow paths. */ +static inline bool scr_exc_pending_inline(void) { return scr_exc_active->kind != SCR_EXC_NONE; } +#define scr_exc_pending() scr_exc_pending_inline() +#endif +_Static_assert(offsetof(ScrExcCell, kind) == 0 && sizeof(ScrExcKind) == 4 && SCR_EXC_NONE == 0, + "the LLVM emitter's inline pending check loads an i32 kind at offset 0"); + +SCR_COLD void scr_throw_f64(double v); +SCR_COLD void scr_throw_bool(bool v); +SCR_COLD 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. */ +SCR_COLD 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. */ +SCR_COLD void scr_throw_ref_classified(void *v, void *(*retain)(void *), void (*release)(void *), + ScrTraceFn trace, bool object); +SCR_COLD 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. */ +SCR_COLD 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 + * participates in cycle collection because a closure may capture the + * binding and its payload may retain that closure. */ +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 */ +void scr_caught_trace_v(void *c, ScrTraceVisit visit, void *ctx); +/* 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 " 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 SCR_TL 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); + +#endif /* SCR_EXCEPTION_H */ diff --git a/packages/runtime/src/scr_ffi.h b/packages/runtime/src/scr_ffi.h new file mode 100644 index 000000000..170ef0d7e --- /dev/null +++ b/packages/runtime/src/scr_ffi.h @@ -0,0 +1,103 @@ +/* scriptc runtime — outbound FFI retained callbacks (scr_ffi.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_FFI_H +#define SCR_FFI_H + +#include "scr_closure.h" + +/* ── 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)); + +#endif /* SCR_FFI_H */ diff --git a/packages/runtime/src/scr_file_handle.h b/packages/runtime/src/scr_file_handle.h new file mode 100644 index 000000000..527c9b5ed --- /dev/null +++ b/packages/runtime/src/scr_file_handle.h @@ -0,0 +1,62 @@ +/* scriptc runtime — file descriptors and fs.promises FileHandle + * (scr_file_handle.c). Included by scr_runtime.h, which says where new + * declarations go. */ +#ifndef SCR_FILE_HANDLE_H +#define SCR_FILE_HANDLE_H + +#include "scr_lib.h" + +/* 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); +/* Engine callers supply the target OS's flags, rather than the frontend's + * portable constant mask. Argument validation happens before this seam. */ +double scr_fs_open_native(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); +ScrPromise *scr_file_handle_sync_promise(ScrFileHandle *h); +ScrPromise *scr_file_handle_datasync_promise(ScrFileHandle *h); +ScrPromise *scr_file_handle_truncate_promise(ScrFileHandle *h, double length); +ScrPromise *scr_file_handle_chmod_promise(ScrFileHandle *h, double mode); +double scr_file_handle_readv(ScrFileHandle *h, ScrArr *buffers, double position); +double scr_file_handle_writev(ScrFileHandle *h, ScrArr *buffers, double position); +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); +double scr_fs_readv_sync(double fd, ScrArr *buffers, double position); +double scr_fs_writev_sync(double fd, ScrArr *buffers, double position); +/* Unlike scalar readSync's zero-length fast path, a nonempty vector of + * empty views still tests the descriptor and positioned-read capability. */ +double scr_fs_read_zero(double fd, double position); +void scr_fs_close(double fd); + +#endif /* SCR_FILE_HANDLE_H */ diff --git a/packages/runtime/src/scr_generator.h b/packages/runtime/src/scr_generator.h new file mode 100644 index 000000000..f528b8b8f --- /dev/null +++ b/packages/runtime/src/scr_generator.h @@ -0,0 +1,184 @@ +/* scriptc runtime — sync and async generators (scr_async.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_GENERATOR_H +#define SCR_GENERATOR_H + +#include "scr_events.h" + +/* ── 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 *)); +/* A synchronous generator whose body the compiler emitted as an LLVM + * switched coroutine: no fiber stack; resumes call into the coroutine frame + * on the resumer's stack. The body registers its frame with + * scr_coro_started and ends in scr_gen_coro_finish (the trampoline + * epilogue of the fiber form). Native targets only. */ +ScrGen *scr_gen_new_stackless(void (*entry)(ScrFiber *, void *), void *argpack, + void (*drop_args)(void *)); +void scr_coro_started(void *handle); +/* The OUT stores of a coroutine body's yield (its suspension follows). */ +void scr_coro_yield_f64(ScrFiber *self, double v); +void scr_coro_yield_bool(ScrFiber *self, bool v); +void scr_coro_yield_ref(ScrFiber *self, void *v, void (*release)(void *)); /* moves */ +void scr_gen_coro_finish(ScrFiber *self); +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); +/* A valueless next() that answers done (the compiler's own drives). */ +bool scr_gen_step(ScrGen *g); +/* IteratorClose: a valueless return() whose result is dropped. */ +void scr_gen_close(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. */ +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 */ +ScrDyn *scr_gen_take_in_dyn(void); /* dyn channel: +1; NONE → undefined */ +/* yield* captures next/return/throw before ordinary yield unwinding. Ref + * converters borrow their typed payload and return an owned dyn value. */ +ScrDyn *scr_gen_delegate_resume(ScrDyn *(*next_ref)(void *), + ScrDyn *(*return_ref)(void *), + ScrDyn *(*caught_ref)(const ScrCaught *)); + +/* yield* over a typed generator delegate. start runs the first valueless + * next(); flag 1 marks a typed next channel, which a suspended delegate + * cannot take undefined into (the SC1071 fence), and flag 2 a completion + * read as a return type that cannot hold undefined. forward runs right + * after the delegating body's yield resumes and passes the consumer's + * next(v), return(v), or throw(e) to the delegate; next is its form for a + * resume with nothing pending. `modes` packs the next, return, and + * completion channel transfers (two bits each) and the typed-completion + * flag (64; 4 for next); the converters box a borrowed ref payload into + * an owned dyn. All answer whether the delegate is done. */ +bool scr_gen_delegate_start(ScrGen *d, unsigned flags); +bool scr_gen_delegate_next(ScrGen *d, unsigned flags, ScrDyn *(*to_dyn)(void *)); +bool scr_gen_delegate_forward(ScrGen *d, unsigned modes, ScrDyn *(*next_dyn)(void *), + ScrDyn *(*ret_dyn)(void *), ScrDyn *(*done_dyn)(void *)); + +/* 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 + +void scr_fetch_install(void); +ScrPromise *scr_fetch_input(ScrDyn *input, ScrDyn *init); +ScrDyn *scr_fetch_function(void); +void scr_global_fetch_install(ScrDyn *(*get)(void)); +ScrDyn *scr_fetch_request_new(ScrDyn *input, ScrDyn *init); +ScrDyn *scr_fetch_headers_new(ScrDyn *init); +ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init); /* +1 promise */ +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 */ +ScrPromise *scr_fetch_response_text(ScrDyn *response); /* +1 promise */ +ScrPromise *scr_fetch_response_array_buffer(ScrDyn *response); /* +1 promise */ +ScrPromise *scr_fetch_response_bytes(ScrDyn *response); /* +1 promise */ +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 */ + +#endif /* SCR_GENERATOR_H */ diff --git a/packages/runtime/src/scr_http.h b/packages/runtime/src/scr_http.h new file mode 100644 index 000000000..378f4bdb1 --- /dev/null +++ b/packages/runtime/src/scr_http.h @@ -0,0 +1,349 @@ +/* scriptc runtime — node:http (scr_http.c). Included by scr_runtime.h, which + * says where new declarations go. */ +#ifndef SCR_HTTP_H +#define SCR_HTTP_H + +#include "scr_tls.h" + +/* ── 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*/); +void scr_http_res_on_end_callback(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 an implicit scheme switch. 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 + +#endif /* SCR_HTTP_H */ diff --git a/packages/runtime/src/scr_http2.h b/packages/runtime/src/scr_http2.h new file mode 100644 index 000000000..7e8d944de --- /dev/null +++ b/packages/runtime/src/scr_http2.h @@ -0,0 +1,207 @@ +/* scriptc runtime — node:http2 (scr_http2.c). Included by scr_runtime.h, + * which says where new declarations go. */ +#ifndef SCR_HTTP2_H +#define SCR_HTTP2_H + +#include "scr_http.h" + +/* ── 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); + +#endif /* SCR_HTTP2_H */ diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h new file mode 100644 index 000000000..16c295b65 --- /dev/null +++ b/packages/runtime/src/scr_inspect.h @@ -0,0 +1,128 @@ +/* scriptc runtime — util.inspect (scr_inspect.c). Included by scr_runtime.h, + * which says where new declarations go. */ +#ifndef SCR_INSPECT_H +#define SCR_INSPECT_H + +#include "scr_bytes.h" + +/* ── 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); /* , 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); +void scr_insp_begin(double recurse); +void scr_insp_entry(ScrStr *s, bool is_num); +/* Circular references, Node-exact ( / [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 " " + * 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); +/* A nested util.inspect inside a custom rendering (PerformanceEntry's): + * begin sets a fresh inspect context aside the caller's; end restores it + * and answers `prefix inner` with inner's newlines re-indented at the + * caller's level (+1; borrows both). */ +void scr_insp_nested_begin(void); +ScrStr *scr_insp_nested_end(ScrStr *prefix, ScrStr *inner); + +/* 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 nullish/empty encodings, a decoded string otherwise — or NULL + * with the exception pending. */ +ScrDyn *scr_fs_read_file_sync_dyn(ScrStr *path, const ScrDyn *enc); +ScrStr *scr_fs_read_file_encoded(ScrStr *path, ScrStr *encoding); +ScrStr *scr_fs_read_fd_encoded(double fd, ScrStr *encoding); +ScrDyn *scr_fs_callback_value(ScrStr *member); +ScrDyn *scr_fs_callback_call(ScrStr *member, const ScrDyn *args); +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); + +#endif /* SCR_INSPECT_H */ diff --git a/packages/runtime/src/scr_json.h b/packages/runtime/src/scr_json.h new file mode 100644 index 000000000..85b2c029c --- /dev/null +++ b/packages/runtime/src/scr_json.h @@ -0,0 +1,135 @@ +/* scriptc runtime — JSON parsing and the JSON output buffer (scr_json.c). + * Included by scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_JSON_H +#define SCR_JSON_H + +#include "scr_dyn.h" + +/* 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); + +/* Compiler-emitted description of a statically known JSON.parse target + * (`JSON.parse(text) as T`). Constant data in the program image; the LLVM + * backend mirrors this layout ({size, size, ptr, ptr, ptr, ptr, ptr} and + * {ptr, size, size, ptr}). Records list their declared fields (at most + * 64), each with its byte offset in the record struct; bool fields are one + * byte, f64 fields doubles, every other kind a +1 pointer. */ +enum { + SCR_JSCHEMA_F64 = 0, + SCR_JSCHEMA_BOOL = 1, + SCR_JSCHEMA_STR = 2, + SCR_JSCHEMA_REC = 3, + SCR_JSCHEMA_ARR = 4, +}; +typedef struct ScrJsonSchema ScrJsonSchema; +typedef struct { + const char *name; + size_t name_len; + size_t offset; + const ScrJsonSchema *type; +} ScrJsonSchemaField; +struct ScrJsonSchema { + size_t kind; + size_t nfields; + const ScrJsonSchemaField *fields; /* records */ + const ScrJsonSchema *elem; /* arrays */ + void *(*rec_new)(void); /* records: zeroed instance, rc 1 */ + ScrArr *(*arr_new)(size_t); /* arrays: empty array of the element kind */ + void (*release)(void *); /* records and arrays */ +}; + +/* One-pass parse straight into the native layout. Returns the +1 record + * or array, or NULL WITHOUT throwing whenever the checked-dynamic route + * (scr_json_parse + dynCheck) might fail or differ; the caller then runs + * that route, which reports the exact error. */ +void *scr_json_parse_schema(const ScrStr *text, const ScrJsonSchema *schema); + +/* 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. An array replacer selects the object + * keys to serialize. 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); +ScrDyn *scr_json_stringify_value(const ScrDyn *value, const ScrDyn *replacer, const ScrDyn *space); + +/* 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; +/* Emitted JSON writers append constant text inline (llvm/walkers.ts + * appendBytes) through this prefix. */ +_Static_assert(offsetof(ScrJsonBuf, data) == 0 && offsetof(ScrJsonBuf, len) == sizeof(char *) && + offsetof(ScrJsonBuf, cap) == sizeof(char *) + sizeof(size_t), + "LLVM backend expects ScrJsonBuf to start with { data, len, cap }"); + +void scr_jb_init(ScrJsonBuf *b); +/* Append a borrowed byte span verbatim, including embedded NUL bytes. */ +void scr_jb_write(ScrJsonBuf *b, const char *s, size_t n); +/* scr_jb_write for text that holds no lone surrogate (fixed text, JSON + * tokens), which cannot complete a surrogate pair at the seam. */ +void scr_jb_write_utf8(ScrJsonBuf *b, const char *s, size_t n); +/* 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); +/* Append String(number), including NaN/Infinity, without a temporary string. */ +void scr_jb_put_number(ScrJsonBuf *b, double v); +/* Quoted + escaped JSON string: \" \\ \n \r \t \b \f, other control chars + * as \u00XX, lone surrogates as \udXXX, everything else (UTF-8 included) + * verbatim — exactly the JS JSON.stringify escape set. */ +void scr_jb_put_json_str(ScrJsonBuf *b, const ScrStr *s); + +/* 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 */ +/* Reformat compiler-produced compact JSON with an already resolved gap. + * Borrows both inputs; gap_len is a byte length, including embedded NUL. + * Returns a new owned string, or retains compact when the gap is empty. */ +ScrStr *scr_json_indent(const ScrStr *compact, const char *gap, size_t gap_len); + +#endif /* SCR_JSON_H */ diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h new file mode 100644 index 000000000..da0703170 --- /dev/null +++ b/packages/runtime/src/scr_lib.h @@ -0,0 +1,416 @@ +/* scriptc runtime — the standard library: process, modules, performance + * timing and node:fs (scr_lib.c). Included by scr_runtime.h, which says where + * new declarations go. */ +#ifndef SCR_LIB_H +#define SCR_LIB_H + +#include "scr_exception.h" + +/* ── 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); + +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) */ + +bool scr_stdio_write(int fd, const void *data, size_t len); +bool scr_stdio_write_str(int fd, const ScrStr *s); +int scr_stdio_write_raw(int fd, const void *data, size_t len); +/* `wide` marks string bytes that may hold lone surrogates, which become + * U+FFFD when they reach the stream itself. */ +extern SCR_TL bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len, bool wide); +/* 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.: +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 */ +/* NODE_DEBUG as the process started (NULL when unset), for util.debuglog. */ +const char *scr_node_debug_initial(void); +/* 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); +/* Nonzero hands the address-space teardown after scr_process_exit to a + * helper process (Linux, `--detached-exit`). scr_lib.c defines a weak 0; + * the program object of such an executable defines it as 1. */ +extern const unsigned char scr_detached_exit; +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); +/* perf_hooks user timing (scr_perf.c): mark, measure, the getEntries + * family and the clears take the call's dyn argument vector (borrowed) + * and apply Node's validation and errors. mark and measure answer 1 and + * the getters the entry count; the answered entries stay readable through + * the scr_perf_entry_* accessors (index into the answer) until the next + * timeline call. which: 0 getEntries, 1 getEntriesByName, 2 + * getEntriesByType. Accessor results are +1. */ +double scr_perf_mark(ScrDyn *args); +double scr_perf_measure(ScrDyn *args); +double scr_perf_entries(ScrDyn *args, double which); +void scr_perf_clear_marks(ScrDyn *args); +void scr_perf_clear_measures(ScrDyn *args); +ScrStr *scr_perf_entry_name(double i); +ScrStr *scr_perf_entry_type(double i); +double scr_perf_entry_start(double i); +double scr_perf_entry_duration(double i); +ScrDyn *scr_perf_entry_detail(double i); +/* The entry objects themselves: entry_keep stores the program's object + * for the last mark or measure (borrowed, retained), entry_ref answers + * entry i's stored object (+1), so the getters return the same objects + * mark() and measure() did. */ +void scr_perf_entry_keep(ScrDyn *ref); +ScrDyn *scr_perf_entry_ref(double i); +uint64_t scr_hrtime_ns(void); +ScrDyn *scr_process_hrtime_value(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); +/* process.memoryUsage(): take samples rss and the runtime heap once; + * field(k) reads that sample (k: 0 rss, 1 heapTotal, 2 heapUsed, + * 3 external, 4 arrayBuffers). heapTotal is the bytes the runtime heap + * holds from the system, heapUsed the bytes in live allocations, and + * external/arrayBuffers are 0 (byte storage lives in the same heap). + * memoryUsage.rss() is the resident set alone. An unreadable resident + * set throws Node's uv_resident_set_memory error. */ +void scr_process_memory_usage_take(void); +double scr_process_memory_usage_field(double k); +double scr_process_rss(void); +/* The runtime heap's footprint (scr_alloc.c): bytes held from the system + * and bytes in live allocations, for the calling thread's allocator plus + * the system allocator where it reports them. */ +void scr_heap_stats(double *total, double *used); +/* process.ppid: the parent's pid at the time of the read. */ +double scr_process_ppid(void); +/* process.title: libuv's process title — argv[0] until set; a set + * truncates to the original argument area (POSIX) and becomes the name + * ps shows, or the console title (Windows). "node" without arguments + * (library sessions). +1 / borrowed. */ +ScrStr *scr_process_title(void); +void scr_process_set_title(const ScrStr *title); +/* process.argv0: the original argv[0] the binary was started with. +1. */ +ScrStr *scr_process_argv0(void); +/* process.release: the stable { name, lts, sourceUrl, headersUrl[, + * libUrl] } object of the compatibility target's release. +1. */ +ScrDyn *scr_process_release(void); +/* geteuid(2) / getegid(2) / getgroups(2) (with the effective gid added + * when missing, like Node). Windows has no such members: the calls throw + * the TypeError Node's missing member does. */ +double scr_process_geteuid(void); +double scr_process_getegid(void); +ScrArr *scr_process_getgroups(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); + +/* Sequentially consistent integer operations and shared Int32 wait queues. */ +typedef struct ScrBytes ScrBytes; +ScrStr *scr_atomics_wait(ScrBytes *, double, double, double); +double scr_atomics_notify(ScrBytes *, double, double); +double scr_atomics_op(ScrBytes *, double, double, double, double); + +/* 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_fdatasync(double fd); +void scr_fs_ftruncate(double fd, double length); +void scr_fs_utimes(ScrStr *path, double atime, double mtime, bool nofollow); +void scr_fs_invalid_path(const ScrDyn *value, const ScrStr *path); +void scr_fs_invalid_named_path(const ScrDyn *value, const ScrStr *path, const char *name); +void scr_fs_futimes(double fd, double atime, double mtime); +void scr_fs_link(ScrStr *source, ScrStr *dest); +void scr_fs_symlink(ScrStr *target, ScrStr *destination, int kind); +int scr_fs_symlink_infer(ScrStr *absolute); +ScrBytes *scr_fs_readlink_bytes(ScrStr *path); +void scr_fs_link_checked(const ScrDyn *existing, const ScrDyn *destination); +void scr_fs_symlink_checked(const ScrDyn *target, const ScrDyn *destination, const ScrDyn *type); +void scr_fs_symlink_promise_checked(const ScrDyn *target, const ScrDyn *destination, const ScrDyn *type); +ScrDyn *scr_fs_readlink_checked(const ScrDyn *path, const ScrDyn *options, bool promise); +ScrDyn *scr_fs_readlink_dyn(const ScrDyn *path, const ScrDyn *options); +ScrDyn *scr_fs_statfs(ScrStr *path, bool bigint); +ScrDyn *scr_fs_statfs_checked(const ScrDyn *path, const ScrDyn *options); +ScrPromise *scr_fsp_statfs(const ScrDyn *path, const ScrDyn *options); +ScrStr *scr_fs_readlink_str(const ScrDyn *path, const ScrDyn *options); +ScrBytes *scr_fs_readlink_buffer(const ScrDyn *path, const ScrDyn *options); +ScrPromise *scr_fsp_link(const ScrDyn *existing, const ScrDyn *destination); +ScrPromise *scr_fsp_symlink(const ScrDyn *target, const ScrDyn *destination, const ScrDyn *type); +ScrPromise *scr_fsp_readlink_dyn(const ScrDyn *path, const ScrDyn *options); +ScrPromise *scr_fsp_readlink_str(const ScrDyn *path, const ScrDyn *options); +ScrPromise *scr_fsp_readlink_buffer(const ScrDyn *path, const ScrDyn *options); +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_utimes(const ScrDyn *path, const ScrDyn *atime, const ScrDyn *mtime); +ScrPromise *scr_fsp_lutimes(const ScrDyn *path, const ScrDyn *atime, const ScrDyn *mtime); +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_utimes_promise(ScrFileHandle *h, const ScrDyn *atime, const ScrDyn *mtime); +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); + +#endif /* SCR_LIB_H */ diff --git a/packages/runtime/src/scr_map.h b/packages/runtime/src/scr_map.h new file mode 100644 index 000000000..cc0322609 --- /dev/null +++ b/packages/runtime/src/scr_map.h @@ -0,0 +1,313 @@ +/* scriptc runtime — Map and Set (scr_map.c). Included by scr_runtime.h, which + * says where new declarations go. */ +#ifndef SCR_MAP_H +#define SCR_MAP_H + +#include "scr_array.h" + +/* ── maps (scr_map.c) ─────────────────────────────────────────────── + * ES Map 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). + * + * Primitive keys compare by value and reference keys by identity. Numbers + * use SameValueZero: NaN equals NaN and -0 is stored as +0. Union wrappers + * are representations, never identities; their arm descriptors select + * the primitive or reference comparison, including null and undefined. + * + * 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, + SCR_MAP_KEY_BIGINT, SCR_MAP_KEY_BOOL, SCR_MAP_KEY_UNION_VALUE, + SCR_MAP_KEY_NULL, SCR_MAP_KEY_UNDEFINED /* tag-only union arms */ +} 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 */ + uint64_t hash; /* nonzero cached hash; zero = tombstone (key/val released) */ +} ScrMapEntry; + +/* Checked views use the original storage. Slot converters borrow on reads + * and return owned references on writes, just like typed Map operations. */ +typedef struct { + const char *type; + struct ScrDyn *(*key_box)(uint64_t); + uint64_t (*key_unbox)(const struct ScrDyn *); + bool (*key_matches)(const struct ScrDyn *); + struct ScrDyn *(*val_box)(uint64_t); + uint64_t (*val_unbox)(const struct ScrDyn *); +} ScrMapDynOps; + +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; + /* Reference-represented keys, including bigint and union values. */ + 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) */ + /* Two bits per inserted entry hash (scr_map.c scr_map_filter_bits): a + * lookup whose bits are not all set is absent without touching the index + * or the entries. Deletions leave their bits; compaction recomputes it. */ + uint64_t filter; + size_t ecap; /* entries capacity */ + ScrMapEntry *entries; + size_t nbuckets; /* zero for small linear maps; otherwise power of two >= 2 * nentries */ + void *buckets; /* compact tagged indices, or size_t indices for large tables */ + size_t iter_depth; /* > 0: an iteration is active — no compaction */ + const ScrMapDynOps *dyn_ops; + const uint8_t *union_keys; /* immutable key-kind table indexed by union tag */ + /* Direct index for number keys (SCR_MAP_KEY_F64 only). While ndense is + * nonzero every live key is an integer in [0, ndense) and dense[k] holds + * its entry index + 1 (0 = absent); the map then keeps no bucket table. + * A key outside that shape moves the map to hashing; whenever the bucket + * table would grow, compact integer keys move it back. Iteration order is + * always the entries array's. */ + uint32_t *dense; + size_t ndense; + /* Every entry below this index is a tombstone. A walk that starts at or + * below it advances it to the first live entry (scr_map_iter_skip); + * compaction and clear reset it. A map that deletes from the front (a + * queue, an LRU cache) then finds its first live entry without walking + * the whole deleted prefix again. */ + size_t head; +} ScrMap; +/* The LLVM backend's inline retain/release read both trace slots through + * its %ScrMapRc prefix type (fields 5 and 8), mirroring scr_map_retain and + * scr_map_release's `key_trace || val_trace` header test. */ +_Static_assert(offsetof(ScrMap, val_trace) == sizeof(size_t) + 2 * sizeof(uint32_t) + 2 * sizeof(void *), + "inline RC fast paths read ScrMap.val_trace as %ScrMapRc field 5"); +_Static_assert(offsetof(ScrMap, key_trace) == offsetof(ScrMap, val_trace) + 3 * sizeof(void *), + "inline RC fast paths read ScrMap.key_trace as %ScrMapRc field 8"); +/* The LLVM backend's inline number-key lookup (sc_map_entry_f64) reads + * entries, dense and ndense through %ScrMapIx fields 13, 19 and 20, and + * entries as %ScrMapEntry { key, val, hash }; %ScrMapIx field 11 is the + * i64 filter. */ +_Static_assert(offsetof(ScrMap, filter) == + (offsetof(ScrMap, nlive) + sizeof(size_t) + _Alignof(uint64_t) - 1) / _Alignof(uint64_t) * _Alignof(uint64_t), + "%ScrMapIx field 11 is ScrMap.filter, directly after nlive"); +_Static_assert(offsetof(ScrMap, nlive) == sizeof(size_t) + 2 * sizeof(uint32_t) + 6 * sizeof(void *) + sizeof(size_t), + "%ScrMapIx field 10 is ScrMap.nlive"); +/* The inline iteration steps (sc_map_iter_entry) read the entry count as + * %ScrMapIx field 9 and test each entry's hash, the third field. */ +_Static_assert(offsetof(ScrMap, nentries) + sizeof(size_t) == offsetof(ScrMap, nlive), + "%ScrMapIx field 9 is ScrMap.nentries, directly before nlive"); +_Static_assert(offsetof(ScrMapEntry, hash) == 16, "inline iteration reads ScrMapEntry.hash at byte 16"); +_Static_assert(offsetof(ScrMap, entries) == offsetof(ScrMap, filter) + sizeof(uint64_t) + sizeof(size_t), + "inline number-key lookups read ScrMap.entries as %ScrMapIx field 13"); +_Static_assert(offsetof(ScrMap, dense) == offsetof(ScrMap, entries) + 6 * sizeof(void *), + "inline number-key lookups read ScrMap.dense as %ScrMapIx field 19"); +_Static_assert(offsetof(ScrMap, ndense) == offsetof(ScrMap, dense) + sizeof(void *), + "inline number-key lookups read ScrMap.ndense as %ScrMapIx field 20"); +_Static_assert(sizeof(ScrMapEntry) == 24 && offsetof(ScrMapEntry, val) == 8, + "inline number-key lookups read ScrMapEntry.val at byte 8"); +/* Entry for a number key, or NULL when absent. The pointer is valid until + * the next mutation; generated code reads the value slot through it. */ +const ScrMapEntry *scr_map_entry_f64(const ScrMap *m, double key); +/* Entry for a string key, or NULL when absent, on the same terms. Typed + * reference reads retain the value with its static type's retain instead + * of the collection's adapter. */ +const ScrMapEntry *scr_map_entry_str(const ScrMap *m, const ScrStr *key); + +/* Called once on a fresh UNION_VALUE collection, before its first insert. */ +void scr_map_union_keys(ScrMap *map, const uint8_t *kinds); +/* String-key lookups borrow an exact byte span. Reference results return + * +1; scalar outputs are written only on a hit, like the string getters. */ +bool scr_map_get_span_f64(const ScrMap *m, const char *bytes, size_t length, double *out); +bool scr_map_get_span_bool(const ScrMap *m, const char *bytes, size_t length, bool *out); +void *scr_map_get_span_ref(const ScrMap *m, const char *bytes, size_t length); + +/* Fresh shallow copies with compact storage and independent mutation. + * keys_only selects a Set; checked views materialize their boxed values. */ +ScrMap *scr_map_clone(const ScrMap *source, bool keys_only); +ScrMap *scr_map_clone_dyn(const ScrMap *source, bool keys_only); +void scr_map_values_into(ScrMap *set, const ScrMap *source, bool checked); + +void scr_map_dyn_attach(ScrMap *map, const ScrMapDynOps *ops); +struct ScrDyn *scr_map_dyn_key(const ScrMap *map, double index); +struct ScrDyn *scr_map_dyn_value(const ScrMap *map, double index); +struct ScrDyn *scr_map_dyn_get(ScrMap *map, const struct ScrDyn *key); +bool scr_map_dyn_has(ScrMap *map, const struct ScrDyn *key, bool remove); +void scr_map_dyn_set(ScrMap *map, const struct ScrDyn *key, const struct ScrDyn *value, bool set); + +/* 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_bool(const ScrMap *m, bool 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_bool(ScrMap *m, bool 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_bool_f64(ScrMap *m, bool key, double v); +void scr_map_set_bool_bool(ScrMap *m, bool key, bool v); +void scr_map_set_bool_ref(ScrMap *m, bool 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_bool_f64(const ScrMap *m, bool key, double *out); +bool scr_map_get_bool_bool(const ScrMap *m, bool key, bool *out); +void *scr_map_get_bool_ref(const ScrMap *m, bool 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); +/* The first live index at or after i, or iter_count when none is left. + * iter_start is the first live index of the map (for-of starts there). */ +double scr_map_iter_skip(ScrMap *m, double i); +double scr_map_iter_start(ScrMap *m); +double scr_map_iter_key_f64(const ScrMap *m, double i); +bool scr_map_iter_key_bool(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_bool(const ScrMap *s); +ScrArr *scr_set_to_arr_str(const ScrMap *s); +ScrArr *scr_set_to_arr_ref(const ScrMap *s, const ScrArrOps *ops); +ScrArr *scr_set_to_arr_dyn(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, as for arrays (scr_arr_new_ops). */ +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); +/* Publication makes objects immortal and never frees them, so the RC audit + * stops counting them (scr_message.c). */ +void scr_str_live_forget(void); +void scr_dyn_live_forget(void); +void scr_arr_live_forget(void); +void scr_map_live_forget(void); +void scr_union_live_forget(void); +#endif + +/* For the same reason LeakSanitizer must not report a published object + * whose last reference is dropped (scr_message.c, scr_publish.c). Ignored + * chunks are leak-scan roots, so everything an immortal object reaches is + * covered and the verdict on the rest of the heap is unchanged. Weak: ASan + * runtimes without a leak checker leave it NULL; other builds compile the + * hook away. */ +#if defined(__has_feature) +#if __has_feature(address_sanitizer) +#define SCR_LSAN_IMMORTAL 1 +void __lsan_ignore_object(const void *p) __attribute__((weak)); +#endif +#endif +static inline void scr_lsan_immortal(const void *obj) { +#ifdef SCR_LSAN_IMMORTAL + if (__lsan_ignore_object) __lsan_ignore_object(obj); +#else + (void)obj; +#endif +} + +#endif /* SCR_MAP_H */ diff --git a/packages/runtime/src/scr_net.h b/packages/runtime/src/scr_net.h new file mode 100644 index 000000000..668d3014c --- /dev/null +++ b/packages/runtime/src/scr_net.h @@ -0,0 +1,342 @@ +/* scriptc runtime — node:net (scr_net.c) and the socket transport hooks. + * Included by scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_NET_H +#define SCR_NET_H + +#include "scr_bytes.h" + +/* ── 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 throws the "not + * supported yet" Error). */ +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 throw the "not + * supported yet" Error; + * 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); +/* Several protocol-layer slices in order as one write: a single vectored + * syscall when the bytes can go out immediately, else the same buffering + * as consecutive scr_net_sock_write_native calls. */ +typedef struct { + const char *data; + size_t len; +} ScrNetSlice; +void scr_net_sock_writev_native(ScrNetSocket *s, const ScrNetSlice *slices, 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 */ +ScrNetSocket *scr_net_connect_checked(double port, ScrStr *host /*borrowed, nullable*/, ScrClosure *cb /*moves, nullable*/); /* +1 */ +bool scr_net_host_chk(const ScrStr *host /*borrowed, nullable*/); +/* 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. */ + +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 tlsClientError + * default, which tears down without an 'error' event). `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 without an 'error' + * event, 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); + +#endif /* SCR_NET_H */ diff --git a/packages/runtime/src/scr_number.h b/packages/runtime/src/scr_number.h new file mode 100644 index 000000000..50b270587 --- /dev/null +++ b/packages/runtime/src/scr_number.h @@ -0,0 +1,150 @@ +/* scriptc runtime — numbers, String.fromCharCode, Number statics, Date and + * numeric coercion (scr_number.c, scr_lib.c). Included by scr_runtime.h, + * which says where new declarations go. */ +#ifndef SCR_NUMBER_H +#define SCR_NUMBER_H + +#include "scr_events.h" + +/* ── 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); + +/* Clinger's exact fast path for a VALIDATED decimal span ([+-]? digits + * with optional fraction and exponent; no whitespace, no Infinity): sets + * *out and returns true when the value is m × 10^e with m <= 2^53 and a + * small |e| (one correctly rounded multiply/divide, bit-identical to + * strtod); false means the caller must take its strtod path. */ +bool scr_decimal_fast(const char *p, size_t n, double *out); +/* The same fast path with whole-span validation folded into its scan: -1 + * when p[0..n) is not such a decimal literal, 1 with *out set when exact, + * 0 when valid but the caller must use strtod. */ +int scr_decimal_scan(const char *p, size_t n, double *out); + +/* Number.prototype.toFixed's digit string for finite |x| < 1e21 and + * 0 <= f <= 100: the spec's n (closest to |x| × 10^f on the EXACT binary + * value, ties up) with the point placed f digits from the right and "-" + * for x < 0. Writes NUL-terminated text into buf (>= 128 bytes); returns + * the length. */ +size_t scr_f64_to_fixed(double x, int f, char *buf); + +/* 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, other surrogates stay lone. 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. + * ToUint32 and the bitwise operations live in scr_number.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); + +#endif /* SCR_NUMBER_H */ diff --git a/packages/runtime/src/scr_object.h b/packages/runtime/src/scr_object.h new file mode 100644 index 000000000..900920dae --- /dev/null +++ b/packages/runtime/src/scr_object.h @@ -0,0 +1,34 @@ +/* scriptc runtime — class instance accounting (scr_object.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_OBJECT_H +#define SCR_OBJECT_H + +#include "scr_core.h" + +/* ── 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); +/* Emitted by the RC-audit lane's ProgramLifetime constructors, after the + * allocation note: such instances are never freed (the exit audit skips its + * leak report, and LeakSanitizer scans them as roots). */ +void scr_obj_lifetime_note(const void *obj); +#ifdef SCR_RC_AUDIT +long scr_obj_live_count(void); +long scr_obj_lifetime_count(void); +#else +/* Only the RC audit lane counts live objects. Other runtimes keep the + * out-of-line no-op symbols for direct-emission objects that still call + * them (scr_object.c), while runtime-internal call sites compile away. */ +#define scr_obj_alloc_note() ((void)0) +#define scr_obj_free_note() ((void)0) +#endif +#ifdef SCR_RC_AUDIT +/* A released lifetime region freed `n` program-lifetime instances. */ +void scr_obj_lifetime_forget(long n); +#endif + +#endif /* SCR_OBJECT_H */ diff --git a/packages/runtime/src/scr_os.h b/packages/runtime/src/scr_os.h new file mode 100644 index 000000000..5d720a1b9 --- /dev/null +++ b/packages/runtime/src/scr_os.h @@ -0,0 +1,81 @@ +/* scriptc runtime — node:os (scr_lib.c, scr_os.c). Included by scr_runtime.h, + * which says where new declarations go. */ +#ifndef SCR_OS_H +#define SCR_OS_H + +#include "scr_exception.h" + +/* ── 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 */ +/* node:os host facts (scr_os.c), libuv's answers: uname machine/version, + * the host name, "LE"/"BE", free memory in bytes, uptime in seconds and + * the 1/5/15-minute load averages (+1 fresh results; never throw). */ +ScrStr *scr_os_machine(void); +ScrStr *scr_os_version(void); +ScrStr *scr_os_hostname(void); +ScrStr *scr_os_endianness(void); +double scr_os_freemem(void); +double scr_os_uptime(void); +ScrArr *scr_os_loadavg(void); +/* os.getPriority(pid): validateInt32, then the scheduling priority; + * failures throw Node's RangeError or SystemError (ERR_SYSTEM_ERROR). */ +double scr_os_get_priority(double pid); +/* os.cpus(): take answers the CPU count of a fresh snapshot; model and + * field (0 speed, 1 user, 2 nice, 3 sys, 4 idle, 5 irq) read it. */ +double scr_os_cpus_take(void); +ScrStr *scr_os_cpus_model(double i); +double scr_os_cpus_field(double i, double field); +/* os.constants: the per-thread null-prototype constants object (+1). */ +ScrDyn *scr_os_constants(void); +double scr_os_available_parallelism(void); /* uv_available_parallelism: CPUs this process may use, >= 1 */ +/* 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 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/, 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); + +#endif /* SCR_OS_H */ diff --git a/packages/runtime/src/scr_path.h b/packages/runtime/src/scr_path.h new file mode 100644 index 000000000..7bf45e55c --- /dev/null +++ b/packages/runtime/src/scr_path.h @@ -0,0 +1,65 @@ +/* scriptc runtime — node:path (scr_path.c) and path globs (scr_glob.c). + * Included by scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_PATH_H +#define SCR_PATH_H + +#include "scr_array.h" + +/* ── 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 exactly + * (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); +/* path.parse / path.format for both families: parse answers a fresh + * string[] [root, dir, base, ext, name]; format takes the pathObject + * fields with undefined spelled "" (Node reads each through `||`). */ +ScrArr *scr_path_parse(ScrStr *path); +ScrArr *scr_path_win32_parse(ScrStr *path); +ScrStr *scr_path_format(ScrStr *root, ScrStr *dir, ScrStr *base, ScrStr *name, ScrStr *ext); +ScrStr *scr_path_win32_format(ScrStr *root, ScrStr *dir, ScrStr *base, ScrStr *name, + ScrStr *ext); +/* path.matchesGlob (scr_glob.c): Node 24's minimatch with matchGlobPattern's + * options, case-insensitive on macOS and Windows hosts like Node. Borrows + * both; a brace sequence over 2^32 - 1 elements throws Node's RangeError, a + * pattern over 65536 units its TypeError, and a segment regex invalid under + * the u flag its SyntaxError. scr_glob_match is the same with the family + * and case mode explicit (the oracle tests use it). */ +bool scr_path_matches_glob(ScrStr *path, ScrStr *pattern); +bool scr_path_win32_matches_glob(ScrStr *path, ScrStr *pattern); +bool scr_glob_match(ScrStr *path, ScrStr *pattern, bool windows, bool nocase); + +#endif /* SCR_PATH_H */ diff --git a/packages/runtime/src/scr_qs.h b/packages/runtime/src/scr_qs.h new file mode 100644 index 000000000..3d00dfae6 --- /dev/null +++ b/packages/runtime/src/scr_qs.h @@ -0,0 +1,120 @@ +/* scriptc runtime — node:querystring (scr_qs.c), the node:util helpers + * (scr_util.c), URI and base64 codecs. Included by scr_runtime.h, which says + * where new declarations go. */ +#ifndef SCR_QS_H +#define SCR_QS_H + +#include "scr_string_methods.h" + +/* ── 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_qs_escape beside the component encoder (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); +/* querystring.parse(qs) with the default separators and maxKeys, as a + * fresh checked-dynamic null-prototype object (url.parse's parsed query). + * Borrows qs; +1; never throws. */ +struct ScrDyn *scr_qs_parse_dyn(const ScrStr *qs); + +/* 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); +ScrStr *scr_util_style_text(const struct ScrDyn *format, const struct ScrDyn *text, const struct ScrDyn *options); +/* Node's terminal color decision (FORCE_COLOR, NO_COLOR, TERM, CI hosts) + * once the stream is a terminal (scr_util_style.c). */ +bool scr_util_terminal_colors(void); +/* util.inherits, util.deprecate, util.callbackify and util.debuglog + * (scr_util_callable.c). Arguments are borrowed; function results are +1 + * native dyn functions, or NULL with Node's TypeError pending. */ +void scr_util_inherits(struct ScrDyn *ctor, struct ScrDyn *super_ctor); +struct ScrDyn *scr_util_deprecate(struct ScrDyn *fn, struct ScrDyn *msg, struct ScrDyn *code, + struct ScrDyn *options); +struct ScrDyn *scr_util_callbackify(struct ScrDyn *original); +struct ScrDyn *scr_util_debuglog(struct ScrDyn *section, struct ScrDyn *callback); + +/* Node/libuv system error utilities. Arguments are checked without coercion; + * entries is a fresh array of [number, [name, message]] pairs (+1). */ +ScrStr *scr_util_system_error_name(const struct ScrDyn *error); +ScrStr *scr_util_system_error_message(const struct ScrDyn *error); +struct ScrDyn *scr_util_system_error_entries(void); + +/* Node 24's dotenv grammar, shared by util.parseEnv and process.loadEnvFile. + * Arguments are borrowed; parseEnv returns a fresh ordinary object (+1). + * Validation and file failures leave a coded exception pending. */ +struct ScrDyn *scr_util_parse_env(const struct ScrDyn *content); +/* Checked primitives, ordinary records/arrays and byte views. Opaque native + * objects and accessor descriptors retain explicit catchable refusals. */ +bool scr_util_is_deep_strict_equal(const struct ScrDyn *a, const struct ScrDyn *b, + const struct ScrDyn *skip_prototype); +void scr_process_load_env_file(const struct ScrDyn *path); + +/* 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; a lone surrogate throws URIError [ERR_INVALID_URI] (NULL). */ +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). Throws the same + * URIError on a lone surrogate. 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); + +#endif /* SCR_QS_H */ diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h new file mode 100644 index 000000000..fdab2d0ab --- /dev/null +++ b/packages/runtime/src/scr_rc.h @@ -0,0 +1,493 @@ +/* scriptc runtime — cycle collection (scr_cycle.c), the small-object + * allocator (scr_alloc.c) and cross-thread publication (scr_publish.c). + * Included by scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_RC_H +#define SCR_RC_H + +#include "scr_core.h" + +/* ── 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 8-byte header directly BEFORE the object: the + * registry id of its type's descriptor (a trace function enumerating the + * object's cycle-capable children, and a teardown function freeing the + * object WITHOUT releasing traced children) and the trial-deletion + * bookkeeping (color, generation and 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; gather: 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 one + * generation (nursery, mature, old) so later restricted 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. + * SCR_CYCLE_GROWTH_CAP lowers how far past its size the heap may grow + * between passes at an older level that find nothing (16 times its size by + * default; see scr_cycle.c). + * 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); + +/* Colors belong to the collector alone: retains and releases never write + * them (a buffered entry is a candidate until a pass drains it). DOOMED + * identifies the gathered dead set during re-buffering and weak observer + * teardown. WHITE candidates may still become black when a later outside + * root restores them; only the settled white set becomes DOOMED. */ +enum { SCR_CYC_BLACK = 0, SCR_CYC_GRAY = 1, SCR_CYC_WHITE = 2, SCR_CYC_DOOMED = 3 }; + +/* 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; a pass at + * SCR_CYC_OLD is a full pass. */ +enum { SCR_CYC_NURSERY = 0, SCR_CYC_MATURE = 1, SCR_CYC_OLD = 2, SCR_CYC_NGENS = 3 }; + +/* A headered type's collector entry points, shared by all its objects: the + * header names its descriptor by a registry id (`id`, assigned on the type's + * first allocation) instead of carrying a pointer, which keeps the header at + * 8 bytes. Emitted shapes define one per traced type (llvm/shapes.ts); + * runtime families define their own. Descriptors are writable (the id) and + * must outlive every object of the type. */ +typedef struct ScrCycType { + ScrTraceFn trace; + ScrCycFreeFn free_fn; + /* Index of the type's per-thread collector statistics (scr_cycle.c's + * nursery bypass): a runtime family's SCR_CYC_KIND_*, or for emitted + * descriptors their number counting from SCR_CYC_KIND_EMITTED. 0 keeps + * no statistics. Statistics only steer + * when a candidate is walked, never what a pass frees, so two types that + * share an index (separately emitted code in one process) are merely + * judged together. */ + uint32_t kind; + /* Registry id (scr_cyc_types), 0 until scr_cyc_register assigns it. + * Emitted allocation paths read it and take the slow path while it is 0. */ + uint32_t id; +} ScrCycType; +/* Runtime families' kinds. Emitted descriptors count from + * SCR_CYC_KIND_EMITTED (llvm/alloc.ts FIRST_EMITTED_CYC_KIND). */ +enum { + SCR_CYC_KIND_NONE = 0, + SCR_CYC_KIND_ARRAY, + SCR_CYC_KIND_PROMISE, + SCR_CYC_KIND_BOX, + SCR_CYC_KIND_CLOSURE, + SCR_CYC_KIND_ERROR, + SCR_CYC_KIND_DOMEXCEPTION, + SCR_CYC_KIND_EMITTER, + SCR_CYC_KIND_CAUGHT, + SCR_CYC_KIND_DYN, + SCR_CYC_KIND_WEAK_MAP, + SCR_CYC_KIND_MAP, + SCR_CYC_KIND_CLASS_OBJECT, + SCR_CYC_KIND_STREAM, + SCR_CYC_KIND_UNION, + SCR_CYC_KIND_WORKER_VIEW, + SCR_CYC_KIND_EMITTED = 32 +}; +/* Eight bytes in front of the object: the descriptor's registry id, the + * collector color (SCR_CYC_*) and generation (SCR_CYC_NURSERY..SCR_CYC_OLD, + * the walk filter), and `buf`: SCR_CYC_BUFFERED_BIT while the object sits in + * its generation's candidate buffer, with its position there below it (O(1) + * removal when rc hits 0). */ +typedef struct ScrCycHdr { + uint16_t id; + uint8_t color; + uint8_t gen; + uint32_t buf; +} ScrCycHdr; +#define SCR_CYC_BUFFERED_BIT 0x80000000u + +/* This layout is an ABI, not an implementation detail. The emitted + * allocation stores the id and zeroes the rest; every emitted release of a + * headered object (llvm/shapes.ts cycleRootLines, used by the record, class + * and inline RC helpers) mirrors scr_cyc_on_release's early return by + * testing the sign of `buf` as an i32 at obj-4, and the emitted inline free + * reads the generation as an i8 at obj-5. Hence the assertions. */ +_Static_assert(sizeof(ScrCycHdr) == 8, "LLVM backend expects an 8-byte cycle header"); +_Static_assert(offsetof(ScrCycHdr, id) == 0 && sizeof(((ScrCycHdr *)0)->id) == 2, + "the emitted allocation stores the registry id as an i16 at the block start"); +_Static_assert(sizeof(ScrCycHdr) - offsetof(ScrCycHdr, buf) == 4, + "the emitted release fast path tests `buf` as an i32 at obj-4"); +_Static_assert(sizeof(ScrCycHdr) - offsetof(ScrCycHdr, gen) == 5, + "LLVM backend's inline free reads the generation as an i8 at obj-5"); +_Static_assert(sizeof(ScrCycHdr) % _Alignof(double) == 0, + "cycle-headered payloads must preserve double alignment"); + +static inline ScrCycHdr *scr_cyc_hdr(void *obj) { return (ScrCycHdr *)obj - 1; } + +/* Registry of descriptors by id (copies, so a header reaches its entry + * points in one step); ids are assigned on first allocation and shared by + * every thread. */ +#define SCR_CYC_MAX_TYPES ((uint32_t)1 << 15) +extern ScrCycType scr_cyc_types[SCR_CYC_MAX_TYPES]; +uint32_t scr_cyc_register(ScrCycType *type); +static inline uint32_t scr_cyc_type_id(ScrCycType *type) { + /* Acquire pairs with scr_cyc_register's publishing compare-exchange, so + * a thread that sees the id also sees its scr_cyc_types entry. */ + uint32_t id = __atomic_load_n(&type->id, __ATOMIC_ACQUIRE); + return id ? id : scr_cyc_register(type); +} + +static inline const ScrCycType *scr_cyc_type(const ScrCycHdr *h) { return &scr_cyc_types[h->id]; } +static inline unsigned scr_cyc_color(const ScrCycHdr *h) { return h->color; } +static inline void scr_cyc_set_color(ScrCycHdr *h, unsigned color) { h->color = (uint8_t)color; } +static inline unsigned scr_cyc_gen(const ScrCycHdr *h) { return h->gen; } +static inline void scr_cyc_set_gen(ScrCycHdr *h, unsigned gen) { h->gen = (uint8_t)gen; } +static inline bool scr_cyc_is_buffered(const ScrCycHdr *h) { + return (h->buf & SCR_CYC_BUFFERED_BIT) != 0; +} +static inline uint32_t scr_cyc_buf_index(const ScrCycHdr *h) { + return h->buf & ~SCR_CYC_BUFFERED_BIT; +} +/* Buffered at position `index` of its generation's buffer, or not at all. */ +static inline void scr_cyc_set_buffered(ScrCycHdr *h, uint32_t index) { + h->buf = SCR_CYC_BUFFERED_BIT | index; +} +static inline void scr_cyc_clear_buffered(ScrCycHdr *h) { h->buf = 0; } + +/* Zeroed allocation with a cycle header in front; returns the OBJECT + * pointer (header at scr_cyc_hdr). `type` must outlive the object (a + * static descriptor). Aborts on OOM. */ +void *scr_cyc_alloc(size_t size, ScrCycType *type); +void scr_cyc_free(void *obj); /* frees the block, header included */ + +/* ── small-object allocator (scr_alloc.c) ─────────────────────────────── + * Runtime objects are small, short-lived and freed by the thread that made + * them — the pattern a general-purpose malloc pays the most for. Blocks of + * up to SCR_SA_MAX bytes come from per-size-class free lists refilled by a + * bump pointer; the fast paths below inline into every runtime unit. + * + * Contracts match malloc/calloc/realloc/free (NULL on OOM), except for + * alignment: blocks are 8-byte aligned, and 16-byte aligned when the size + * is a multiple of 16, which covers every type whose size is a multiple of + * its alignment. scr_mem_free and scr_mem_realloc accept ANY pointer — a + * block outside the allocator's reservation goes back to the system — so a + * free site can be switched without proving where its pointers came from. + * The converse does not hold: a block from scr_mem_alloc/calloc/realloc + * must never reach the system free/realloc. Emitted code reaches the same + * allocator through scr_rt_calloc/scr_rt_free. + * + * Compiled to the plain system calls under SCR_RC_AUDIT and AddressSanitizer + * (the audit lane must see every logical free as a real free), on targets + * without a cheap address-space reservation (wasm32, Windows, 32-bit), and + * in thread-instanced library archives, where several runtime instances + * run on embedder threads the runtime does not manage. Elsewhere the + * allocator state is unsynchronized: runtime objects are only allocated + * and freed on their runtime thread (native worker jobs never touch them). + * + * Worker executables (SCR_WORKERS) give every script thread its own + * allocator: `scr_sa` is thread-local and describes the thread's ARENA, one + * of SCR_SA_ARENAS equal slices of a single process-wide reservation, so + * the owning arena of any block is address arithmetic. A thread claims an + * arena on its first allocation and parks it (free lists and bump pointers + * intact) when it exits; the next thread to claim it continues from there. + * On Linux an exiting thread that carved much first returns the pages of + * its arena that hold only free blocks (scr_alloc.c). Script objects stay on their thread, so a free + * normally hits the caller's own arena inline. A block freed by another + * thread goes to the owning arena's lock-free remote list, which the owner + * drains on its slow path; published (immortal) objects are never freed, + * so they stay valid after their allocating thread exits. Threads beyond + * SCR_SA_ARENAS, or a refused reservation, use the system allocator. */ +#include +#if defined(SCR_RC_AUDIT) || defined(__SANITIZE_ADDRESS__) || defined(__wasi__) || \ + defined(__wasm__) || defined(_WIN32) || UINTPTR_MAX != UINT64_MAX || \ + (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) +#define SCR_SMALL_ALLOC 0 +#elif defined(__has_feature) +#if __has_feature(address_sanitizer) +#define SCR_SMALL_ALLOC 0 +#else +#define SCR_SMALL_ALLOC 1 +#endif +#else +#define SCR_SMALL_ALLOC 1 +#endif + +void *scr_mem_realloc(void *p, size_t n); +void *scr_rt_calloc(size_t n); /* emitted-code entry points */ +void scr_rt_free(void *p); +/* Asks the allocator's pre-fault helper (scr_alloc.c) to fault the pages + * of [lo, hi), aligned to 256 KiB, in ahead of use; builds without the + * helper ignore it. */ +void scr_mem_prefault(void *lo, void *hi); + +/* Size classes: requests up to 128 bytes take a class per 8-byte step + * (small objects often sit just under a 16-byte boundary), larger ones a + * class per 16-byte step up to 512. Class c below SCR_SA_FINE_MAX / 8 + * holds (c + 1) * 8-byte blocks, except that the class of 49 to 56-byte + * requests (SCR_SA_WIDE) holds 64-byte blocks: a 56-byte block straddles a + * cache line in 3 of every 4 slots, which costs more on scattered access + * to records of that size than the smaller block saves. Widening the block + * rather than sharing the 64-byte class keeps the class lookup free of a + * special case. Slices start page-aligned, so a class whose block size is + * a multiple of 16 keeps its blocks 16-byte aligned. */ +#define SCR_SA_FINE_MAX 128u /* the largest request with an 8-byte-step class */ +#define SCR_SA_WIDE 6u /* the class of 49 to 56-byte requests */ +#define SCR_SA_NCLASS 40u +#define SCR_SA_MAX 512u + +/* The class of a block of n bytes, 1 <= n <= SCR_SA_MAX. */ +static inline unsigned scr_sa_class(size_t n) { + unsigned k = (unsigned)((n - 1) >> 3); + return k < SCR_SA_FINE_MAX / 8 ? k : (k + SCR_SA_FINE_MAX / 8) >> 1; +} +/* The block size of class c. */ +static inline size_t scr_sa_class_size(unsigned c) { + if (c < SCR_SA_FINE_MAX / 8) return (size_t)(c + 1 + (c == SCR_SA_WIDE)) * 8; + return (size_t)(c + 1 - SCR_SA_FINE_MAX / 16) * 16; +} + +typedef struct ScrSaBlock { + struct ScrSaBlock *next; +} ScrSaBlock; + +/* One reservation split into SCR_SA_NCLASS equal slices, one per class, so + * a block's class is its slice index: (p - base) >> shift. span is 0 until + * the first refill reserves (and stays 0 if the host refuses). + * + * scr_sa exists in EVERY build, and its layout is an ABI: the LLVM backend + * inlines the fast paths into emitted constructors and releases + * (llvm/alloc.ts) as `{ i64, i64, i32, [40 x ptr], [40 x ptr], [40 x ptr] }` + * on 64-bit targets. Where SCR_SMALL_ALLOC is 0 the state is never written, + * so the emitted fast paths see no free block, no bump room and an empty + * span, and always take their out-of-line slow path (scr_rt_calloc / + * scr_rt_free → the system allocator) — the sanitizer and RC-audit lanes + * therefore see every logical allocation and free without the compiler + * knowing which runtime it links against. The emitted fast paths also skip + * scr_obj_alloc_note/scr_obj_free_note, which is sound because those are + * no-ops whenever the allocator is live (SCR_RC_AUDIT implies + * SCR_SMALL_ALLOC == 0). */ +typedef struct ScrSaState { + uintptr_t base, span; + unsigned shift; + ScrSaBlock *free[SCR_SA_NCLASS]; + char *bump[SCR_SA_NCLASS], *lim[SCR_SA_NCLASS]; +} ScrSaState; +/* Worker executables keep one allocator per script thread (see above). Its + * state shares one thread-local block with the collector's live counts and + * the weak dispose hook (ScrThreadHot below): on Darwin every distinct + * thread-local costs a call per function that touches it, and allocation + * and free paths touch all four. */ +#ifdef SCR_WORKERS +/* The per-thread state of the allocation and free fast paths, in one + * thread-local block; its layout is an ABI (llvm/alloc.ts addresses the + * fields at fixed offsets from one llvm.threadlocal.address). */ +typedef struct ScrThreadHot { + ScrSaState sa; + size_t cyc_live; + size_t cyc_old_freed; + void (*weak_dispose_hook)(void *); +} ScrThreadHot; +extern _Thread_local ScrThreadHot scr_thread_hot; +#define scr_sa (scr_thread_hot.sa) +#define scr_cyc_live (scr_thread_hot.cyc_live) +#define scr_cyc_old_freed (scr_thread_hot.cyc_old_freed) +#define scr_weak_dispose_hook (scr_thread_hot.weak_dispose_hook) +#if UINTPTR_MAX == UINT64_MAX +_Static_assert(offsetof(ScrThreadHot, sa) == 0 && offsetof(ScrThreadHot, cyc_live) == 984 && + offsetof(ScrThreadHot, cyc_old_freed) == 992 && + offsetof(ScrThreadHot, weak_dispose_hook) == 1000, + "llvm/alloc.ts addresses ScrThreadHot fields at these offsets"); +#endif +#else +extern ScrSaState scr_sa; +#endif +#if UINTPTR_MAX == UINT64_MAX +_Static_assert(offsetof(ScrSaState, span) == 8 && offsetof(ScrSaState, shift) == 16 && + offsetof(ScrSaState, free) == 24 && offsetof(ScrSaState, bump) == 344 && + offsetof(ScrSaState, lim) == 664 && sizeof(ScrSaState) == 984, + "llvm/alloc.ts inlines the small-object allocator against this layout"); +#endif +#if defined(SCR_RC_AUDIT) && SCR_SMALL_ALLOC +#error "emitted allocation fast paths skip the RC-audit object notes" +#endif + +#ifdef SCR_WORKERS +/* A block outside the calling thread's arena: another arena's (remote + * free) or the system's. */ +void scr_sa_free_foreign(void *p); +/* The calling script thread is done with its arena (worker exit). */ +void scr_sa_thread_park(void); +/* Waits until the arenas of exited threads are trimmed (tests). */ +void scr_sa_wait_trims(void); +#define SCR_SA_FREE_FOREIGN(p) scr_sa_free_foreign(p) +#else +#define SCR_SA_FREE_FOREIGN(p) free(p) +#endif + +#if SCR_SMALL_ALLOC +/* Refill miss: reserves on first use, falls back to the system allocator + * for large blocks, n == 0, an exhausted slice, or a refused reservation. */ +void *scr_sa_slow(size_t n, bool zero); + +static inline void *scr_sa_take(size_t n, bool zero) { + if (n - 1 < SCR_SA_MAX) { /* n == 0 wraps to the slow path */ + unsigned c = scr_sa_class(n); + ScrSaBlock *b = scr_sa.free[c]; + if (b) { + scr_sa.free[c] = b->next; + if (zero) memset(b, 0, n); /* recycled blocks are dirty */ + return b; + } + size_t sz = scr_sa_class_size(c); + char *p = scr_sa.bump[c]; + if ((size_t)(scr_sa.lim[c] - p) >= sz) { /* fresh mmap memory is zero */ + scr_sa.bump[c] = p + sz; + return p; + } + } + return scr_sa_slow(n, zero); +} + +static inline void *scr_mem_alloc(size_t n) { return scr_sa_take(n, false); } +static inline void *scr_mem_calloc(size_t n) { return scr_sa_take(n, true); } + +static inline void scr_mem_free(void *p) { + uintptr_t off = (uintptr_t)p - scr_sa.base; + if (off < scr_sa.span) { + ScrSaBlock *b = p; + unsigned c = (unsigned)(off >> scr_sa.shift); + b->next = scr_sa.free[c]; + scr_sa.free[c] = b; + return; + } + SCR_SA_FREE_FOREIGN(p); +} +#else +static inline void *scr_mem_alloc(size_t n) { return malloc(n); } +static inline void *scr_mem_calloc(size_t n) { return calloc(1, n); } +static inline void scr_mem_free(void *p) { free(p); } +#endif + +/* Inline twins of scr_cyc_alloc/scr_cyc_free for hot runtime allocation + * sites (union boxes, arrays, boxes, closures, maps, dyn nodes). With a + * constant size the size class and the zeroing fold at compile time. The + * live count is exported for these and for the emitted inline paths + * (llvm/alloc.ts), which do the same three things: header (the registry + * id; the rest starts zero: SCR_CYC_BLACK, SCR_CYC_NURSERY, unbuffered), + * live count, block. */ +#ifndef SCR_WORKERS +extern SCR_TL size_t scr_cyc_live; +extern SCR_TL size_t scr_cyc_old_freed; +extern SCR_TL void (*scr_weak_dispose_hook)(void *); +#endif +static inline void *scr_cyc_alloc_inline(size_t size, ScrCycType *type) { + if (size > SIZE_MAX - sizeof(ScrCycHdr)) scr_trap("scriptc: out of memory\n"); + uint32_t id = scr_cyc_type_id(type); + ScrCycHdr *h = (ScrCycHdr *)scr_mem_calloc(sizeof(ScrCycHdr) + size); + if (!h) scr_trap("scriptc: out of memory\n"); + h->id = (uint16_t)id; + scr_cyc_live++; + return h + 1; +} +static inline void scr_cyc_free_inline(void *obj) { + if (scr_weak_dispose_hook) scr_weak_dispose_hook(obj); + scr_cyc_live--; + if (scr_cyc_gen(scr_cyc_hdr(obj)) == SCR_CYC_OLD) scr_cyc_old_freed++; + scr_mem_free(scr_cyc_hdr(obj)); +} +_Static_assert(SCR_CYC_NURSERY == 0, "zeroed cycle headers start in the nursery"); +_Static_assert(sizeof(((ScrCycHdr *)0)->gen) == 1 && SCR_CYC_OLD == 2, + "LLVM backend's inline free compares an i8 generation against 2"); + +/* Dispose an object whose reference count already reached zero. The caller + * removes its cycle candidate first. Nested disposals keep bounded stack + * depth; deeper objects wait until the outer disposal drains the worklist. + * The callback owns the object and must release its children and free it. */ +void scr_rc_destroy(void *obj, void (*destroy)(void *)); + +/* 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. Retains need no hook: a candidate + * re-retained after it was buffered simply stays a candidate, and the pass + * that walks it finds it externally referenced. */ +void scr_cyc_on_release(void *obj); +void scr_cyc_on_dead(void *obj); + +/* Inline halves of the hooks for runtime release paths, mirroring the + * emitted ones (llvm/shapes.ts): a release of an already-buffered + * object, and the death of an object that was never buffered, need no call. + * Most releases of long-lived containers take the first early return. */ +static inline void scr_cyc_released(void *obj) { + if (!scr_cyc_is_buffered(scr_cyc_hdr(obj))) scr_cyc_on_release(obj); +} +static inline void scr_cyc_dead(void *obj) { + if (scr_cyc_is_buffered(scr_cyc_hdr(obj))) scr_cyc_on_dead(obj); +} + +/* 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); +void scr_cyc_context_cleanup(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); + +/* Publication (@scriptc/threads publish; scr_cycle.c): makes everything a + * root reaches immortal and immutable. Compiler-emitted per-type walkers + * mark their object (scr_pub_mark: false when already immortal) and push + * their references; containers, strings and dyn subgraphs have runtime + * walkers. scr_publish throws a TypeError, leaving the graph unchanged, + * when it reaches an unpublishable dyn value. */ +typedef struct ScrPub ScrPub; +typedef void (*ScrPubFn)(void *v, ScrPub *ctx); +/* What a marked object is, for the RC audit's live counts (scr_pub_mark). */ +enum { + SCR_PUB_KIND_LEAF = 0, /* not counted (bigints) */ + SCR_PUB_KIND_STRING = 1, + SCR_PUB_KIND_ARRAY = 2, + SCR_PUB_KIND_MAP = 3, + SCR_PUB_KIND_OBJECT = 4, + SCR_PUB_KIND_UNION = 5, + SCR_PUB_KIND_DYN = 6, +}; +void scr_publish(void *root, ScrPubFn fn); +void scr_pub_push(ScrPub *ctx, void *v, ScrPubFn fn); +bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind); +void scr_pub_refuse(ScrPub *ctx, const char *message); +void scr_pub_refuse_at(ScrPub *ctx, const char *message, const char *detail); +void scr_pub_str_fn(void *v, ScrPub *ctx); +void scr_pub_leaf_fn(void *v, ScrPub *ctx); +void scr_pub_dyn_fn(void *v, ScrPub *ctx); +void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem); +void scr_pub_map(void *v, ScrPub *ctx, ScrPubFn key, ScrPubFn val); +void scr_publish_refuse(const char *message); +/* Writes into published objects (emitted guards): Node's frozen-object + * TypeErrors. scr_throw_published_array answers false (and throws nothing) + * when Node's operation would not write the array. */ +struct ScrArr; +SCR_COLD void scr_throw_published_field(const char *prop, const char *owner); +bool scr_throw_published_array(int op, const struct ScrArr *a, double index, double n); +SCR_COLD void scr_throw_published_collection(bool set); + +#endif /* SCR_RC_H */ diff --git a/packages/runtime/src/scr_readline.h b/packages/runtime/src/scr_readline.h new file mode 100644 index 000000000..006f03d98 --- /dev/null +++ b/packages/runtime/src/scr_readline.h @@ -0,0 +1,113 @@ +/* scriptc runtime — node:readline (scr_readline.c) and the event-loop entry + * points (scr_async.c). Included by scr_runtime.h, which says where new + * declarations go. */ +#ifndef SCR_READLINE_H +#define SCR_READLINE_H + +#include "scr_events.h" + +/* ── node:readline and node:readline/promises (scr_readline.c, linked + * under the events gate) ───────────────────────────────────────────── + * The non-terminal Interface (the file header has the model). Interfaces + * are f64 handles. A stream input or output goes through ScrRlIo, which + * scr_stream.c provides (scr_stream_rl_open), so stdin-only programs + * never link the stream unit; the stream side feeds input through + * scr_rl_input_*. Signal-taking entry points get addAbortListener as + * `add` (scr_events_add_abort_listener), passed only by programs that + * link the event-target unit. Listener event numbers: 0 'line', + * 1 'close', 2 'pause', 3 'resume', 4 'error'. */ +typedef struct ScrRlIo { + void (*pause)(void *s); + void (*resume)(void *s); + void (*detach)(void *s, double id); /* removes the interface's input listeners */ + void (*write)(void *s, ScrStr *text); /* borrows text */ + void (*release)(void *s); +} ScrRlIo; +double scr_rl_open(const ScrRlIo *io, void *in /*+1 or NULL: stdin*/, void *out /*+1 or NULL*/, + bool out_stdout, double crlf_delay, ScrStr *prompt, ScrDyn *signal, + ScrAbortAddFn add); +void scr_rl_input_bytes(double id, ScrBytes *chunk); +void scr_rl_input_str(double id, ScrStr *chunk); +void scr_rl_input_end(double id); +void scr_rl_input_error(double id, ScrError *err); +/* question: throws ERR_USE_AFTER_CLOSE on a closed interface (may-throw); + * fn(cb, +1 answer). The promise form never throws. */ +void scr_rl_question(double id, ScrStr *query, ScrClosure *cb /*moves*/, + void (*fn)(ScrClosure *, ScrStr *)); +ScrPromise *scr_rl_question_promise(double id, ScrStr *query, ScrDyn *signal, ScrAbortAddFn add); +void scr_rl_close(double id); +void scr_rl_pause(double id); /* may throw ERR_USE_AFTER_CLOSE */ +void scr_rl_resume(double id); /* may throw ERR_USE_AFTER_CLOSE */ +void scr_rl_prompt(double id); /* may throw ERR_USE_AFTER_CLOSE */ +void scr_rl_set_prompt(double id, ScrStr *prompt); +ScrStr *scr_rl_get_prompt(double id); /* +1 */ +void scr_rl_write(double id, ScrStr *data); /* may throw ERR_USE_AFTER_CLOSE */ +void scr_rl_on(double id, double ev, ScrClosure *cb /*moves*/, void *fn, bool once); +void scr_rl_iter_open(double id); +ScrPromise *scr_rl_iter_next(double id); /* +1 promise: the line, or undefined */ +void scr_rl_iter_return(double id); +/* Listener adapters: line/answer (fn(cb, +1 text)) and error listeners. */ +void scr_rl_answer_thunk0(ScrClosure *cb, ScrStr *answer); +void scr_rl_answer_thunk_str(ScrClosure *cb, ScrStr *answer); +void scr_rl_error_thunk(ScrClosure *cb, ScrError *err); +void scr_rl_error_thunk0(ScrClosure *cb, ScrError *err); +void scr_rl_answer_thunk_dyn(ScrClosure *cb, ScrStr *answer); +void scr_rl_error_thunk_dyn(ScrClosure *cb, ScrError *err); +/* createInterface with a stream input and/or output (scr_stream.c): in is + * the input stream or NULL for process.stdin; out the output stream or + * NULL (out_stdout picks process.stdout). Both borrowed, like prompt + * (NULL: the default "> "). */ +double scr_stream_rl_open(ScrStream *in, ScrStream *out, bool out_stdout, double crlf_delay, + ScrStr *prompt, ScrDyn *signal, ScrAbortAddFn add); +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 SCR_TL void (*scr_process_exit_hook)(double code); +extern SCR_TL 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); +void scr_loop_commonjs_entry(void); +bool scr_report_unhandled_rejections(void); /* true = exit 1 */ +void scr_discard_unhandled_rejections(void); +long scr_abandoned_fiber_count(void); + + +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); + +#endif /* SCR_READLINE_H */ diff --git a/packages/runtime/src/scr_regex.h b/packages/runtime/src/scr_regex.h new file mode 100644 index 000000000..e34e877de --- /dev/null +++ b/packages/runtime/src/scr_regex.h @@ -0,0 +1,98 @@ +/* scriptc runtime — regular expressions (scr_regex.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_REGEX_H +#define SCR_REGEX_H + +#include "scr_array.h" + +typedef int32_t (*ScrRegexMatcherFn)(const uint8_t *s, int32_t len, int32_t start, + uint8_t **capture); + +typedef struct ScrRegex { + size_t rc; /* SIZE_MAX = immortal literal template */ + 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 */ + double last_index; + struct ScrRegex *literal; /* immutable bytecode owner for literal instances */ + void *native; /* one-byte-subject matcher built with bc (NULL: lre_exec only) */ + /* Build-time matcher of a literal template (NULL: none). Searches the + * ASCII bytes s[0..len) from start (one anchored attempt when sticky); + * on a match stores 2 * ncap pointers into s (NULL when unset) and + * returns 1, otherwise 0. A negative len returns ncap. */ + ScrRegexMatcherFn matcher; +} ScrRegex; + +/* Backtrack stack of build-time regex matchers: grow *stack (initially the + * matcher's inline buffer) to at least `need` words; traps on OOM. */ +void scr_regex_stack_grow(int32_t **stack, int32_t *cap, int32_t *inline_buf, int32_t need); +void scr_regex_stack_free(int32_t *stack); + +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); +ScrRegex *scr_regex_literal(ScrRegex *literal); /* fresh state, shared cached bytecode */ +double scr_regex_last_index(ScrRegex *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); /* exec state transitions */ +/* s.match(re): +1 string[] of whole matches under /g, otherwise + * [whole, ...captures] (nonparticipating captures hold ""), or NULL for + * no match. Non-global /y uses exec state transitions. Borrows both. */ +ScrArr *scr_regex_match(ScrStr *s, ScrRegex *re); +/* re.exec(s), string-first ABI: match slice with global/sticky lastIndex + * transitions. Borrows both; +1 or NULL. */ +ScrArr *scr_regex_exec(ScrStr *s, ScrRegex *re); +/* exec for a result read only through constant indices and null tests: + * bit k of `mask` (k < 53) selects the captures that are materialized; the + * rest, and the index/input properties, are left out. Same lastIndex + * transitions and NULL result as scr_regex_exec. Borrows both; +1 or NULL. */ +ScrArr *scr_regex_exec_groups(ScrStr *s, ScrRegex *re, double mask); +/* scr_regex_exec_groups whose captures in `lazy` (a subset of `mask`) are + * read only through scr_regex_group_eq/_number/_parse_int: they may stay + * spans of the subject instead of strings. */ +ScrArr *scr_regex_exec_spans(ScrStr *s, ScrRegex *re, double mask, double lazy); +/* s.match(re) read only through constant indices and null tests: a global + * regex answers scr_regex_match; otherwise scr_regex_exec_spans. */ +ScrArr *scr_regex_match_groups(ScrStr *s, ScrRegex *re, double mask, double lazy); +/* Reads of a lazy capture k of such a result (borrowed): `capture === lit`, + * Number(capture) and parseInt(capture, radix). */ +bool scr_regex_group_eq(ScrArr *m, double k, ScrStr *lit); +double scr_regex_group_number(ScrArr *m, double k); +double scr_regex_group_parse_int(ScrArr *m, double k, double radix); +/* 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); + +#endif /* SCR_REGEX_H */ diff --git a/packages/runtime/src/scr_region.h b/packages/runtime/src/scr_region.h new file mode 100644 index 000000000..fa50d64ee --- /dev/null +++ b/packages/runtime/src/scr_region.h @@ -0,0 +1,111 @@ +/* scriptc runtime — counted lifetime regions (scr_region.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_REGION_H +#define SCR_REGION_H + +#include "scr_core.h" + +/* ── counted lifetime regions (scr_region.c) ─────────────────────────── + * Programs that construct a LifetimeRegion (@scriptc/experimental) allocate + * every ProgramLifetime instance in the thread's CURRENT region instead of + * the small-object allocator. A region owns SCR_RGN_CHUNK-aligned chunks; + * each class packs its instances into segments of a chunk, so a region's + * objects are enumerable without a header. Instead of a per-object count, + * the class's retain and release adjust the chunk's first word (`count`); + * with the first reference each object starts with, it gives the number of + * references to the chunk's objects. + * + * release() condemns a region. A condemned region is freed only when a walk + * of its objects, and of the ordinary objects they own, explains every + * counted reference: trial deletion restricted to the region. References + * the walk cannot see (locals, globals, objects outside the walked graph, + * shapes it does not know) stay unexplained and keep the region. Freeing + * releases every reference field of every object, which tears the owned + * ordinary objects down through their normal paths, runs the cycle + * collector for the owned cycles, and then requires every count to be 0. + * Region 0 of each thread (the default) is never freed: programs that do + * not use regions keep ProgramLifetime's behavior. + * + * Only references to objects of regions that can be freed are counted: + * until the process creates its first region, scr_region_counting is 0 and + * retains and releases touch nothing, and chunks of the default regions + * have no owner, so references to their objects are never counted. Every + * reference to an object of a non-default region is created after that + * region exists, so its counts are exact. + * + * The layouts below are an ABI with the emitted code (llvm/regions.ts): + * the inline allocation fast path bumps the class's ScrRgnCursor, and the + * retain/release bodies test scr_region_counting, then the chunk's owner, + * then adjust its count. */ +#define SCR_RGN_CHUNK_SHIFT 21 +#define SCR_RGN_CHUNK ((size_t)1 << SCR_RGN_CHUNK_SHIFT) + +typedef struct ScrRgnWalk ScrRgnWalk; + +/* One per ProgramLifetime class, emitted as a mutable global: `slot` is + * assigned process-wide on the first allocation (-1 until then). */ +typedef struct ScrRgnClass { + size_t size; /* instance size (the stride rounds it to 8) */ + void (*visit)(void *obj, ScrRgnWalk *w); /* reports each reference field */ + void (*release_fields)(void *obj); /* releases each reference field */ + const char *name; + int32_t slot; +} ScrRgnClass; + +/* The calling thread's allocation cursor for one class in its current + * region: emitted per class (thread-local in worker executables). NULL + * fields send the next allocation to scr_region_alloc. */ +typedef struct ScrRgnCursor { + char *bump, *limit; +} ScrRgnCursor; + +/* The emitted type table (terminated by a NULL release): the release entry + * point a container stores for a type (shapes.ts vAdapters), and the walker + * of its objects. A NULL walker marks a ProgramLifetime class. */ +typedef struct ScrRgnType { + void (*release)(void *); + void (*walk)(void *obj, ScrRgnWalk *w); +} ScrRgnType; + +typedef struct ScrRgnChunk { + int64_t count; /* word 0: references to this chunk's objects, less one per object */ + void *owner; /* word 1: the owning thread; NULL in default regions */ + struct ScrRegion *region; + struct ScrRgnChunk *next; + char *bump; /* the next free segment byte */ + int64_t found; /* references the last walk saw into this chunk */ +} ScrRgnChunk; +_Static_assert(offsetof(ScrRgnChunk, count) == 0 && offsetof(ScrRgnChunk, owner) == sizeof(int64_t), + "llvm/regions.ts addresses the chunk count at 0 and the owner at 8"); + +static inline ScrRgnChunk *scr_rgn_chunk_of(const void *obj) { + return (ScrRgnChunk *)((uintptr_t)obj & ~(uintptr_t)(SCR_RGN_CHUNK - 1)); +} + +/* 1 once any thread has created a region (never reset); emitted retains + * and releases count nothing while it is 0. */ +extern _Atomic uint8_t scr_region_counting; + +/* The allocation slow path: a zeroed instance of `cls` in the current + * region. Refills `cursor`. */ +void *scr_region_alloc(ScrRgnCursor *cursor, ScrRgnClass *cls); +/* new LifetimeRegion(): a fresh region id (ids are never reused). `types` + * is the program's constant type table. Retries condemned regions. */ +double scr_region_create(const ScrRgnType *types); +/* run(fn): makes region `id` current until the matching leave. An id this + * thread does not own (another thread's, or a freed region's) selects the + * thread's default region. Calls nest. */ +void scr_region_enter(double id); +void scr_region_leave(void); +/* release(): condemns region `id` and tries to free it. */ +void scr_region_release(double id); + +/* Walk callbacks for emitted visits and walkers. scr_region_ref reports a + * reference to a ProgramLifetime instance; scr_region_slot reports a + * reference of the type whose container release entry point is `release` + * (unknown entry points are not walked, which keeps their references + * unexplained). Both tolerate NULL. */ +void scr_region_ref(ScrRgnWalk *w, void *obj); +void scr_region_slot(ScrRgnWalk *w, void *obj, void (*release)(void *)); + +#endif /* SCR_REGION_H */ diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index c2b208ac2..3cb2de978 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -4,9125 +4,73 @@ * 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. + * + * This file includes the per-module headers below in dependency order and + * holds no declarations. The first comment of each module header says what + * it declares and names the main runtime units (.c files) behind it. A module + * header includes the headers whose names it uses and compiles on its own, so + * a .c file may include only the module headers it needs. + * + * - A new declaration goes in the header of the unit that defines it: + * scr_.h for scr_.c, or the header whose first comment names + * the unit (scr_lib.c's process and fs entry points are in scr_lib.h, its + * crypto entry points in scr_crypto.h). + * - A declaration must come after every type and macro it uses. When it uses + * a name from a header that is not yet included, add that #include to the + * module header. A header may include only headers listed above it here. + * - A new module header gets its own include guard and an entry in this list + * after every header it includes. */ #ifndef SCR_RUNTIME_H #define SCR_RUNTIME_H -#include /* va_list in the emitter listener adapters */ -#include -#include -#include -#include /* memcpy in the inline slot accessors */ -#include /* 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_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 - -/* ── runtime instance 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. Worker executables use the same isolation for their - * collectors, caches and scheduler state. Expands to nothing for ordinary - * executables and classic libraries. Immutable tables stay shared. */ -#if (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) || defined(SCR_WORKERS) -#define SCR_TL _Thread_local -#else -#define SCR_TL -#endif - -/* Entry points that only run when a JS exception is being raised. noinline - * keeps the throw machinery out of every caller: the LLVM helper never - * imports noinline runtime bodies into program code, so a compiled function - * whose only slow path throws stays small enough to inline and needs no - * callee-saved registers on its fast path. cold tells the C compiler that - * runtime paths reaching them are unlikely. */ -#if defined(__GNUC__) || defined(__clang__) -#define SCR_COLD __attribute__((cold, noinline)) -#else -#define SCR_COLD -#endif - -/* Digest compression functions start on a cache line. The release - * runtime keeps the compiler's default function alignment, so without it - * their speed moves with the size of unrelated code before them. WASM - * code has no addresses to align. */ -#if (defined(__GNUC__) || defined(__clang__)) && !defined(__wasm__) -#define SCR_DIGEST_ALIGNED __attribute__((aligned(64))) -#else -#define SCR_DIGEST_ALIGNED -#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_v20(void); -void scr_runtime_workers_v8(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); - -/* glibc added arc4random_buf in 2.36. Older GNU targets use the same kernel - * CSPRNG as the musl shim, with no global state or persistent descriptor. */ -#ifdef __GLIBC__ -#if !__GLIBC_PREREQ(2, 36) -#include -#include -#include -static inline void scr_glibc_random_buf(void *buf, size_t n) { - unsigned char *p = buf; - while (n > 0) { - ssize_t got = getrandom(p, n, 0); - if (got > 0) { - p += (size_t)got; - n -= (size_t)got; - } else if (got < 0 && errno == EINTR) { - continue; - } else { - scr_trap("scriptc: getrandom failed\n"); - } - } -} -#define arc4random_buf scr_glibc_random_buf -#endif -#endif -_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 unchanged. */ -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: - * - * (*)(void *ctx, ) - * - * 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 unchanged — 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 — rounding - * without a diagnostic 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) -#elif defined(SCR_WORKERS) -/* Executable workers drain their own cleanup stack before their TLS dies. - * Registering their callbacks with libc would execute them on the process - * exit thread against a different runtime instance. */ -int scr_context_atexit(void (*fn)(void)); -void scr_context_cleanup(void); -void scr_context_enter(uint64_t thread_id); -uint64_t scr_context_thread_id(void); -void scr_context_stop_flag(const void *flag); -void scr_context_stop(int code); -bool scr_context_stopping(void); -bool scr_context_checkpoint(void); -bool scr_context_quiet(void); -#if !defined(__APPLE__) && !defined(_WIN32) && !defined(__CYGWIN__) && !defined(__wasi__) -/* ELF worker executables (the LLVM emitter's executableTls targets): every - * script thread owns an exact pending word in the executable's static TLS - * block, which emitted pending checks load with one thread-pointer-relative - * access and no out-of-line slow path (scr_exception.c). */ -#define SCR_EXC_FLAG 1 -#define SCR_EXC_FLAG_PENDING 1u /* the active cell's kind is not NONE */ -#define SCR_EXC_FLAG_STOP 2u /* the context must stop (never cleared) */ -extern SCR_TL _Atomic uint32_t scr_exc_flag; -/* Requests the stop of the thread that owns `flag` (scr_worker.c holds the - * owner's word only while its script runs). */ -void scr_exc_flag_request_stop(_Atomic uint32_t *flag); -#else -/* The process-wide exception alert that emitted pending checks of other - * worker executables test inline (scr_exception.c): nonzero while some - * script thread may have an exception pending in its active cell or must - * stop. */ -extern _Atomic uint32_t scr_exc_alert; -void scr_exc_alert_add(int delta); /* worker.terminate() references */ -#endif -void scr_exc_alert_thread_exit(void); -void scr_loop_context_shutdown(void); -extern SCR_TL void (*scr_context_report_error)(void); -typedef struct ScrContextEnv ScrContextEnv; -ScrContextEnv *scr_context_env_capture(void); -void scr_context_env_free(ScrContextEnv *environment); -void scr_context_env_enter(ScrContextEnv *environment); -bool scr_context_env_set(const char *name, const char *value); -const char *scr_context_getenv(const char *name); -struct ScrArr *scr_context_env_pairs(void); -#define scr_atexit(fn) scr_context_atexit(fn) -#else -#define scr_atexit(fn) atexit(fn) -#endif /* SCR_LIB */ - -/* Also used by ordinary event-loop builds, where this always returns true. */ -bool scr_context_is_main(void); - -/* Native stack guard. Compiled functions that can recurse compare their - * stack pointer against scr_stack_guard on entry and throw Node's catchable - * RangeError below it. The guard sits a reserve above the lowest usable - * address of the running stack (thread or async fiber), leaving room for the - * throw, unwinding, finally blocks, catch handlers and the uncaught-error - * report. Zero (the default) disables the check on that stack. */ -extern SCR_TL uintptr_t scr_stack_guard; -void scr_stack_guard_init(void); -SCR_COLD void scr_stack_overflow(void); -#if defined(SCR_WORKERS) && !defined(SCR_LIB) && defined(__APPLE__) && \ - (defined(__aarch64__) || defined(__x86_64__)) -/* Darwin worker executables: reading a thread-local variable calls its TLV - * thunk, so the emitted entry checks read the running stack's guard from a - * pthread thread-specific-data slot instead, addressed from the thread - * pointer (TPIDRRO_EL0 on arm64, %gs on x86-64) plus this byte offset. - * scr_stack_guard_init validates the slot on first use; zero means the - * direct read is unavailable and the checks call scr_stack_guard_get. */ -#define SCR_STACK_GUARD_TSD 1 -extern uintptr_t scr_stack_guard_tsd; -uintptr_t scr_stack_guard_get(void); -void scr_stack_guard_set(uintptr_t guard); -#define SCR_STACK_GUARD_SET(guard) scr_stack_guard_set(guard) -#else -#define SCR_STACK_GUARD_SET(guard) (scr_stack_guard = (guard)) -#endif -#if defined(__has_feature) -#if __has_feature(address_sanitizer) -#define SCR_STACK_RESERVE_MAX ((size_t)2 << 20) -#endif -#endif -#ifndef SCR_STACK_RESERVE_MAX -#define SCR_STACK_RESERVE_MAX ((size_t)256 << 10) -#endif -static inline uintptr_t scr_stack_guard_for(uintptr_t low, size_t size) { - size_t reserve = size / 4; - if (reserve > SCR_STACK_RESERVE_MAX) reserve = SCR_STACK_RESERVE_MAX; - return low + reserve; -} -double scr_context_thread_number(void); -double scr_worker_root(void); -int scr_worker_argc(void); -char **scr_worker_argv(void); - -#ifdef SCR_WORKERS -#define scr_getenv(name) scr_context_getenv(name) -#else -#define scr_getenv(name) getenv(name) -#endif - -/* ── 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 8-byte header directly BEFORE the object: the - * registry id of its type's descriptor (a trace function enumerating the - * object's cycle-capable children, and a teardown function freeing the - * object WITHOUT releasing traced children) and the trial-deletion - * bookkeeping (color, generation and 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; gather: 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 one - * generation (nursery, mature, old) so later restricted 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. - * SCR_CYCLE_GROWTH_CAP lowers how far past its size the heap may grow - * between passes at an older level that find nothing (16 times its size by - * default; see scr_cycle.c). - * 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); - -/* Colors belong to the collector alone: retains and releases never write - * them (a buffered entry is a candidate until a pass drains it). DOOMED - * identifies the gathered dead set during re-buffering and weak observer - * teardown. WHITE candidates may still become black when a later outside - * root restores them; only the settled white set becomes DOOMED. */ -enum { SCR_CYC_BLACK = 0, SCR_CYC_GRAY = 1, SCR_CYC_WHITE = 2, SCR_CYC_DOOMED = 3 }; - -/* 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; a pass at - * SCR_CYC_OLD is a full pass. */ -enum { SCR_CYC_NURSERY = 0, SCR_CYC_MATURE = 1, SCR_CYC_OLD = 2, SCR_CYC_NGENS = 3 }; - -/* A headered type's collector entry points, shared by all its objects: the - * header names its descriptor by a registry id (`id`, assigned on the type's - * first allocation) instead of carrying a pointer, which keeps the header at - * 8 bytes. Emitted shapes define one per traced type (llvm/shapes.ts); - * runtime families define their own. Descriptors are writable (the id) and - * must outlive every object of the type. */ -typedef struct ScrCycType { - ScrTraceFn trace; - ScrCycFreeFn free_fn; - /* Index of the type's per-thread collector statistics (scr_cycle.c's - * nursery bypass): a runtime family's SCR_CYC_KIND_*, or for emitted - * descriptors their number counting from SCR_CYC_KIND_EMITTED. 0 keeps - * no statistics. Statistics only steer - * when a candidate is walked, never what a pass frees, so two types that - * share an index (separately emitted code in one process) are merely - * judged together. */ - uint32_t kind; - /* Registry id (scr_cyc_types), 0 until scr_cyc_register assigns it. - * Emitted allocation paths read it and take the slow path while it is 0. */ - uint32_t id; -} ScrCycType; -/* Runtime families' kinds. Emitted descriptors count from - * SCR_CYC_KIND_EMITTED (llvm/alloc.ts FIRST_EMITTED_CYC_KIND). */ -enum { - SCR_CYC_KIND_NONE = 0, - SCR_CYC_KIND_ARRAY, - SCR_CYC_KIND_PROMISE, - SCR_CYC_KIND_BOX, - SCR_CYC_KIND_CLOSURE, - SCR_CYC_KIND_ERROR, - SCR_CYC_KIND_DOMEXCEPTION, - SCR_CYC_KIND_EMITTER, - SCR_CYC_KIND_CAUGHT, - SCR_CYC_KIND_DYN, - SCR_CYC_KIND_WEAK_MAP, - SCR_CYC_KIND_MAP, - SCR_CYC_KIND_CLASS_OBJECT, - SCR_CYC_KIND_STREAM, - SCR_CYC_KIND_UNION, - SCR_CYC_KIND_WORKER_VIEW, - SCR_CYC_KIND_EMITTED = 32 -}; -/* Eight bytes in front of the object: the descriptor's registry id, the - * collector color (SCR_CYC_*) and generation (SCR_CYC_NURSERY..SCR_CYC_OLD, - * the walk filter), and `buf`: SCR_CYC_BUFFERED_BIT while the object sits in - * its generation's candidate buffer, with its position there below it (O(1) - * removal when rc hits 0). */ -typedef struct ScrCycHdr { - uint16_t id; - uint8_t color; - uint8_t gen; - uint32_t buf; -} ScrCycHdr; -#define SCR_CYC_BUFFERED_BIT 0x80000000u - -/* This layout is an ABI, not an implementation detail. The emitted - * allocation stores the id and zeroes the rest; every emitted release of a - * headered object (llvm/shapes.ts cycleRootLines, used by the record, class - * and inline RC helpers) mirrors scr_cyc_on_release's early return by - * testing the sign of `buf` as an i32 at obj-4, and the emitted inline free - * reads the generation as an i8 at obj-5. Hence the assertions. */ -_Static_assert(sizeof(ScrCycHdr) == 8, "LLVM backend expects an 8-byte cycle header"); -_Static_assert(offsetof(ScrCycHdr, id) == 0 && sizeof(((ScrCycHdr *)0)->id) == 2, - "the emitted allocation stores the registry id as an i16 at the block start"); -_Static_assert(sizeof(ScrCycHdr) - offsetof(ScrCycHdr, buf) == 4, - "the emitted release fast path tests `buf` as an i32 at obj-4"); -_Static_assert(sizeof(ScrCycHdr) - offsetof(ScrCycHdr, gen) == 5, - "LLVM backend's inline free reads the generation as an i8 at obj-5"); -_Static_assert(sizeof(ScrCycHdr) % _Alignof(double) == 0, - "cycle-headered payloads must preserve double alignment"); - -static inline ScrCycHdr *scr_cyc_hdr(void *obj) { return (ScrCycHdr *)obj - 1; } - -/* Registry of descriptors by id (copies, so a header reaches its entry - * points in one step); ids are assigned on first allocation and shared by - * every thread. */ -#define SCR_CYC_MAX_TYPES ((uint32_t)1 << 15) -extern ScrCycType scr_cyc_types[SCR_CYC_MAX_TYPES]; -uint32_t scr_cyc_register(ScrCycType *type); -static inline uint32_t scr_cyc_type_id(ScrCycType *type) { - /* Acquire pairs with scr_cyc_register's publishing compare-exchange, so - * a thread that sees the id also sees its scr_cyc_types entry. */ - uint32_t id = __atomic_load_n(&type->id, __ATOMIC_ACQUIRE); - return id ? id : scr_cyc_register(type); -} - -static inline const ScrCycType *scr_cyc_type(const ScrCycHdr *h) { return &scr_cyc_types[h->id]; } -static inline unsigned scr_cyc_color(const ScrCycHdr *h) { return h->color; } -static inline void scr_cyc_set_color(ScrCycHdr *h, unsigned color) { h->color = (uint8_t)color; } -static inline unsigned scr_cyc_gen(const ScrCycHdr *h) { return h->gen; } -static inline void scr_cyc_set_gen(ScrCycHdr *h, unsigned gen) { h->gen = (uint8_t)gen; } -static inline bool scr_cyc_is_buffered(const ScrCycHdr *h) { - return (h->buf & SCR_CYC_BUFFERED_BIT) != 0; -} -static inline uint32_t scr_cyc_buf_index(const ScrCycHdr *h) { - return h->buf & ~SCR_CYC_BUFFERED_BIT; -} -/* Buffered at position `index` of its generation's buffer, or not at all. */ -static inline void scr_cyc_set_buffered(ScrCycHdr *h, uint32_t index) { - h->buf = SCR_CYC_BUFFERED_BIT | index; -} -static inline void scr_cyc_clear_buffered(ScrCycHdr *h) { h->buf = 0; } - -/* Zeroed allocation with a cycle header in front; returns the OBJECT - * pointer (header at scr_cyc_hdr). `type` must outlive the object (a - * static descriptor). Aborts on OOM. */ -void *scr_cyc_alloc(size_t size, ScrCycType *type); -void scr_cyc_free(void *obj); /* frees the block, header included */ - -/* ── small-object allocator (scr_alloc.c) ─────────────────────────────── - * Runtime objects are small, short-lived and freed by the thread that made - * them — the pattern a general-purpose malloc pays the most for. Blocks of - * up to SCR_SA_MAX bytes come from per-size-class free lists refilled by a - * bump pointer; the fast paths below inline into every runtime unit. - * - * Contracts match malloc/calloc/realloc/free (NULL on OOM), except for - * alignment: blocks are 8-byte aligned, and 16-byte aligned when the size - * is a multiple of 16, which covers every type whose size is a multiple of - * its alignment. scr_mem_free and scr_mem_realloc accept ANY pointer — a - * block outside the allocator's reservation goes back to the system — so a - * free site can be switched without proving where its pointers came from. - * The converse does not hold: a block from scr_mem_alloc/calloc/realloc - * must never reach the system free/realloc. Emitted code reaches the same - * allocator through scr_rt_calloc/scr_rt_free. - * - * Compiled to the plain system calls under SCR_RC_AUDIT and AddressSanitizer - * (the audit lane must see every logical free as a real free), on targets - * without a cheap address-space reservation (wasm32, Windows, 32-bit), and - * in thread-instanced library archives, where several runtime instances - * run on embedder threads the runtime does not manage. Elsewhere the - * allocator state is unsynchronized: runtime objects are only allocated - * and freed on their runtime thread (native worker jobs never touch them). - * - * Worker executables (SCR_WORKERS) give every script thread its own - * allocator: `scr_sa` is thread-local and describes the thread's ARENA, one - * of SCR_SA_ARENAS equal slices of a single process-wide reservation, so - * the owning arena of any block is address arithmetic. A thread claims an - * arena on its first allocation and parks it (free lists and bump pointers - * intact) when it exits; the next thread to claim it continues from there. - * On Linux an exiting thread that carved much first returns the pages of - * its arena that hold only free blocks (scr_alloc.c). Script objects stay on their thread, so a free - * normally hits the caller's own arena inline. A block freed by another - * thread goes to the owning arena's lock-free remote list, which the owner - * drains on its slow path; published (immortal) objects are never freed, - * so they stay valid after their allocating thread exits. Threads beyond - * SCR_SA_ARENAS, or a refused reservation, use the system allocator. */ -#include -#if defined(SCR_RC_AUDIT) || defined(__SANITIZE_ADDRESS__) || defined(__wasi__) || \ - defined(__wasm__) || defined(_WIN32) || UINTPTR_MAX != UINT64_MAX || \ - (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) -#define SCR_SMALL_ALLOC 0 -#elif defined(__has_feature) -#if __has_feature(address_sanitizer) -#define SCR_SMALL_ALLOC 0 -#else -#define SCR_SMALL_ALLOC 1 -#endif -#else -#define SCR_SMALL_ALLOC 1 -#endif - -void *scr_mem_realloc(void *p, size_t n); -void *scr_rt_calloc(size_t n); /* emitted-code entry points */ -void scr_rt_free(void *p); -/* Asks the allocator's pre-fault helper (scr_alloc.c) to fault the pages - * of [lo, hi), aligned to 256 KiB, in ahead of use; builds without the - * helper ignore it. */ -void scr_mem_prefault(void *lo, void *hi); - -/* Size classes: requests up to 128 bytes take a class per 8-byte step - * (small objects often sit just under a 16-byte boundary), larger ones a - * class per 16-byte step up to 512. Class c below SCR_SA_FINE_MAX / 8 - * holds (c + 1) * 8-byte blocks, except that the class of 49 to 56-byte - * requests (SCR_SA_WIDE) holds 64-byte blocks: a 56-byte block straddles a - * cache line in 3 of every 4 slots, which costs more on scattered access - * to records of that size than the smaller block saves. Widening the block - * rather than sharing the 64-byte class keeps the class lookup free of a - * special case. Slices start page-aligned, so a class whose block size is - * a multiple of 16 keeps its blocks 16-byte aligned. */ -#define SCR_SA_FINE_MAX 128u /* the largest request with an 8-byte-step class */ -#define SCR_SA_WIDE 6u /* the class of 49 to 56-byte requests */ -#define SCR_SA_NCLASS 40u -#define SCR_SA_MAX 512u - -/* The class of a block of n bytes, 1 <= n <= SCR_SA_MAX. */ -static inline unsigned scr_sa_class(size_t n) { - unsigned k = (unsigned)((n - 1) >> 3); - return k < SCR_SA_FINE_MAX / 8 ? k : (k + SCR_SA_FINE_MAX / 8) >> 1; -} -/* The block size of class c. */ -static inline size_t scr_sa_class_size(unsigned c) { - if (c < SCR_SA_FINE_MAX / 8) return (size_t)(c + 1 + (c == SCR_SA_WIDE)) * 8; - return (size_t)(c + 1 - SCR_SA_FINE_MAX / 16) * 16; -} - -typedef struct ScrSaBlock { - struct ScrSaBlock *next; -} ScrSaBlock; - -/* One reservation split into SCR_SA_NCLASS equal slices, one per class, so - * a block's class is its slice index: (p - base) >> shift. span is 0 until - * the first refill reserves (and stays 0 if the host refuses). - * - * scr_sa exists in EVERY build, and its layout is an ABI: the LLVM backend - * inlines the fast paths into emitted constructors and releases - * (llvm/alloc.ts) as `{ i64, i64, i32, [40 x ptr], [40 x ptr], [40 x ptr] }` - * on 64-bit targets. Where SCR_SMALL_ALLOC is 0 the state is never written, - * so the emitted fast paths see no free block, no bump room and an empty - * span, and always take their out-of-line slow path (scr_rt_calloc / - * scr_rt_free → the system allocator) — the sanitizer and RC-audit lanes - * therefore see every logical allocation and free without the compiler - * knowing which runtime it links against. The emitted fast paths also skip - * scr_obj_alloc_note/scr_obj_free_note, which is sound because those are - * no-ops whenever the allocator is live (SCR_RC_AUDIT implies - * SCR_SMALL_ALLOC == 0). */ -typedef struct ScrSaState { - uintptr_t base, span; - unsigned shift; - ScrSaBlock *free[SCR_SA_NCLASS]; - char *bump[SCR_SA_NCLASS], *lim[SCR_SA_NCLASS]; -} ScrSaState; -/* Worker executables keep one allocator per script thread (see above). Its - * state shares one thread-local block with the collector's live counts and - * the weak dispose hook (ScrThreadHot below): on Darwin every distinct - * thread-local costs a call per function that touches it, and allocation - * and free paths touch all four. */ -#ifdef SCR_WORKERS -/* The per-thread state of the allocation and free fast paths, in one - * thread-local block; its layout is an ABI (llvm/alloc.ts addresses the - * fields at fixed offsets from one llvm.threadlocal.address). */ -typedef struct ScrThreadHot { - ScrSaState sa; - size_t cyc_live; - size_t cyc_old_freed; - void (*weak_dispose_hook)(void *); -} ScrThreadHot; -extern _Thread_local ScrThreadHot scr_thread_hot; -#define scr_sa (scr_thread_hot.sa) -#define scr_cyc_live (scr_thread_hot.cyc_live) -#define scr_cyc_old_freed (scr_thread_hot.cyc_old_freed) -#define scr_weak_dispose_hook (scr_thread_hot.weak_dispose_hook) -#if UINTPTR_MAX == UINT64_MAX -_Static_assert(offsetof(ScrThreadHot, sa) == 0 && offsetof(ScrThreadHot, cyc_live) == 984 && - offsetof(ScrThreadHot, cyc_old_freed) == 992 && - offsetof(ScrThreadHot, weak_dispose_hook) == 1000, - "llvm/alloc.ts addresses ScrThreadHot fields at these offsets"); -#endif -#else -extern ScrSaState scr_sa; -#endif -#if UINTPTR_MAX == UINT64_MAX -_Static_assert(offsetof(ScrSaState, span) == 8 && offsetof(ScrSaState, shift) == 16 && - offsetof(ScrSaState, free) == 24 && offsetof(ScrSaState, bump) == 344 && - offsetof(ScrSaState, lim) == 664 && sizeof(ScrSaState) == 984, - "llvm/alloc.ts inlines the small-object allocator against this layout"); -#endif -#if defined(SCR_RC_AUDIT) && SCR_SMALL_ALLOC -#error "emitted allocation fast paths skip the RC-audit object notes" -#endif - -#ifdef SCR_WORKERS -/* A block outside the calling thread's arena: another arena's (remote - * free) or the system's. */ -void scr_sa_free_foreign(void *p); -/* The calling script thread is done with its arena (worker exit). */ -void scr_sa_thread_park(void); -/* Waits until the arenas of exited threads are trimmed (tests). */ -void scr_sa_wait_trims(void); -#define SCR_SA_FREE_FOREIGN(p) scr_sa_free_foreign(p) -#else -#define SCR_SA_FREE_FOREIGN(p) free(p) -#endif - -#if SCR_SMALL_ALLOC -/* Refill miss: reserves on first use, falls back to the system allocator - * for large blocks, n == 0, an exhausted slice, or a refused reservation. */ -void *scr_sa_slow(size_t n, bool zero); - -static inline void *scr_sa_take(size_t n, bool zero) { - if (n - 1 < SCR_SA_MAX) { /* n == 0 wraps to the slow path */ - unsigned c = scr_sa_class(n); - ScrSaBlock *b = scr_sa.free[c]; - if (b) { - scr_sa.free[c] = b->next; - if (zero) memset(b, 0, n); /* recycled blocks are dirty */ - return b; - } - size_t sz = scr_sa_class_size(c); - char *p = scr_sa.bump[c]; - if ((size_t)(scr_sa.lim[c] - p) >= sz) { /* fresh mmap memory is zero */ - scr_sa.bump[c] = p + sz; - return p; - } - } - return scr_sa_slow(n, zero); -} - -static inline void *scr_mem_alloc(size_t n) { return scr_sa_take(n, false); } -static inline void *scr_mem_calloc(size_t n) { return scr_sa_take(n, true); } - -static inline void scr_mem_free(void *p) { - uintptr_t off = (uintptr_t)p - scr_sa.base; - if (off < scr_sa.span) { - ScrSaBlock *b = p; - unsigned c = (unsigned)(off >> scr_sa.shift); - b->next = scr_sa.free[c]; - scr_sa.free[c] = b; - return; - } - SCR_SA_FREE_FOREIGN(p); -} -#else -static inline void *scr_mem_alloc(size_t n) { return malloc(n); } -static inline void *scr_mem_calloc(size_t n) { return calloc(1, n); } -static inline void scr_mem_free(void *p) { free(p); } -#endif - -/* Inline twins of scr_cyc_alloc/scr_cyc_free for hot runtime allocation - * sites (union boxes, arrays, boxes, closures, maps, dyn nodes). With a - * constant size the size class and the zeroing fold at compile time. The - * live count is exported for these and for the emitted inline paths - * (llvm/alloc.ts), which do the same three things: header (the registry - * id; the rest starts zero: SCR_CYC_BLACK, SCR_CYC_NURSERY, unbuffered), - * live count, block. */ -#ifndef SCR_WORKERS -extern SCR_TL size_t scr_cyc_live; -extern SCR_TL size_t scr_cyc_old_freed; -extern SCR_TL void (*scr_weak_dispose_hook)(void *); -#endif -static inline void *scr_cyc_alloc_inline(size_t size, ScrCycType *type) { - if (size > SIZE_MAX - sizeof(ScrCycHdr)) scr_trap("scriptc: out of memory\n"); - uint32_t id = scr_cyc_type_id(type); - ScrCycHdr *h = (ScrCycHdr *)scr_mem_calloc(sizeof(ScrCycHdr) + size); - if (!h) scr_trap("scriptc: out of memory\n"); - h->id = (uint16_t)id; - scr_cyc_live++; - return h + 1; -} -static inline void scr_cyc_free_inline(void *obj) { - if (scr_weak_dispose_hook) scr_weak_dispose_hook(obj); - scr_cyc_live--; - if (scr_cyc_gen(scr_cyc_hdr(obj)) == SCR_CYC_OLD) scr_cyc_old_freed++; - scr_mem_free(scr_cyc_hdr(obj)); -} -_Static_assert(SCR_CYC_NURSERY == 0, "zeroed cycle headers start in the nursery"); -_Static_assert(sizeof(((ScrCycHdr *)0)->gen) == 1 && SCR_CYC_OLD == 2, - "LLVM backend's inline free compares an i8 generation against 2"); - -/* Dispose an object whose reference count already reached zero. The caller - * removes its cycle candidate first. Nested disposals keep bounded stack - * depth; deeper objects wait until the outer disposal drains the worklist. - * The callback owns the object and must release its children and free it. */ -void scr_rc_destroy(void *obj, void (*destroy)(void *)); - -/* 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. Retains need no hook: a candidate - * re-retained after it was buffered simply stays a candidate, and the pass - * that walks it finds it externally referenced. */ -void scr_cyc_on_release(void *obj); -void scr_cyc_on_dead(void *obj); - -/* Inline halves of the hooks for runtime release paths, mirroring the - * emitted ones (llvm/shapes.ts): a release of an already-buffered - * object, and the death of an object that was never buffered, need no call. - * Most releases of long-lived containers take the first early return. */ -static inline void scr_cyc_released(void *obj) { - if (!scr_cyc_is_buffered(scr_cyc_hdr(obj))) scr_cyc_on_release(obj); -} -static inline void scr_cyc_dead(void *obj) { - if (scr_cyc_is_buffered(scr_cyc_hdr(obj))) scr_cyc_on_dead(obj); -} - -/* 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); -void scr_cyc_context_cleanup(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); - -/* Publication (@scriptc/threads publish; scr_cycle.c): makes everything a - * root reaches immortal and immutable. Compiler-emitted per-type walkers - * mark their object (scr_pub_mark: false when already immortal) and push - * their references; containers, strings and dyn subgraphs have runtime - * walkers. scr_publish throws a TypeError, leaving the graph unchanged, - * when it reaches an unpublishable dyn value. */ -typedef struct ScrPub ScrPub; -typedef void (*ScrPubFn)(void *v, ScrPub *ctx); -/* What a marked object is, for the RC audit's live counts (scr_pub_mark). */ -enum { - SCR_PUB_KIND_LEAF = 0, /* not counted (bigints) */ - SCR_PUB_KIND_STRING = 1, - SCR_PUB_KIND_ARRAY = 2, - SCR_PUB_KIND_MAP = 3, - SCR_PUB_KIND_OBJECT = 4, - SCR_PUB_KIND_UNION = 5, - SCR_PUB_KIND_DYN = 6, -}; -void scr_publish(void *root, ScrPubFn fn); -void scr_pub_push(ScrPub *ctx, void *v, ScrPubFn fn); -bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind); -void scr_pub_refuse(ScrPub *ctx, const char *message); -void scr_pub_refuse_at(ScrPub *ctx, const char *message, const char *detail); -void scr_pub_str_fn(void *v, ScrPub *ctx); -void scr_pub_leaf_fn(void *v, ScrPub *ctx); -void scr_pub_dyn_fn(void *v, ScrPub *ctx); -void scr_pub_arr(void *v, ScrPub *ctx, ScrPubFn elem); -void scr_pub_map(void *v, ScrPub *ctx, ScrPubFn key, ScrPubFn val); -void scr_publish_refuse(const char *message); -/* Writes into published objects (emitted guards): Node's frozen-object - * TypeErrors. scr_throw_published_array answers false (and throws nothing) - * when Node's operation would not write the array. */ -struct ScrArr; -SCR_COLD void scr_throw_published_field(const char *prop, const char *owner); -bool scr_throw_published_array(int op, const struct ScrArr *a, double index, double n); -SCR_COLD void scr_throw_published_collection(bool set); - -/* ── 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. Builders and concat results may have spare capacity 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. - * - * On 64-bit little-endian targets the capacity word is split: the low half - * is cap (strings never exceed SCR_STR_MAX_CAP bytes; JavaScript strings are - * far shorter) and the high half caches the string's Map key hash (0 = not - * computed). Emitted literals and runtime statics still initialize one - * size-sized word to len, which leaves the hash half zero. Only uniquely - * owned heap strings are ever mutated, and every such path clears the hash - * (scr_str_hash_forget); immortal strings are never written, so they always - * hash on demand. - * - * Bit 31 of the capacity word (on every target) is the proven-ASCII fact: - * when set, every byte is below 0x80, so UTF-16 indices equal byte offsets - * and .length == len. A clear bit means "not known", never "non-ASCII": - * constructors set it only where it is cheap, and string reads that prove - * ASCII set it later. Capacity therefore stops at 2^31 - 1 bytes, which - * still holds every string V8 can represent (2^29 - 24 UTF-16 units, at - * most three UTF-8 bytes each). Generated code reads the bit for inline - * charCodeAt/length (llvm/strings.ts); emitted literals and runtime - * statics set it in their capacity word. scr_str_hash_forget clears it - * with the hash: every in-place mutation already forgets cached facts. - */ -#if SIZE_MAX > UINT32_MAX && defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ -#define SCR_STR_HASH_CACHE 1 -#else -#define SCR_STR_HASH_CACHE 0 -#endif -#define SCR_STR_MAX_CAP ((size_t)0x7fffffff) -typedef struct ScrStr { - size_t rc; - size_t len; -#if SCR_STR_HASH_CACHE - uint32_t cap : 31; - uint32_t ascii : 1; - uint32_t hash; -#else - size_t cap : 31; - size_t ascii : 1; -#endif - char data[]; -} ScrStr; - -/* The capacity word of a static string: cap == len, the proven-ASCII bit, - * plus the precomputed 32-bit key hash where the header caches one (0 - * leaves it to be computed). */ -#define SCR_STR_ASCII_BIT ((size_t)1 << 31) -#if SCR_STR_HASH_CACHE -#define SCR_STR_CAP_WORD(len, hash32, ascii) \ - ((size_t)(len) | ((ascii) ? SCR_STR_ASCII_BIT : 0) | \ - ((size_t)(uint32_t)(hash32) << 32)) -#else -#define SCR_STR_CAP_WORD(len, hash32, ascii) ((size_t)(len) | ((ascii) ? SCR_STR_ASCII_BIT : 0)) -#endif - -void scr_str_hash_prime(ScrStr *s); /* scr_map.c; before a string becomes immortal */ -/* Forget every cached content fact (key hash, proven ASCII) before a - * uniquely owned string's bytes change. */ -static inline void scr_str_hash_forget(ScrStr *s) { -#if SCR_STR_HASH_CACHE - s->hash = 0; -#endif - s->ascii = 0; -} - -/* True only when every byte is proven below 0x80 (see above). */ -static inline bool scr_str_known_ascii(const ScrStr *s) { return s->ascii; } - -/* Record a proven-ASCII fact. Immortal strings may live in read-only - * memory and are shared across threads, so they are never written. */ -static inline void scr_str_note_ascii(ScrStr *s) { - if (s->rc != SIZE_MAX) s->ascii = 1; -} - -/* memcpy for the short, variable-length copies of string construction - * (identifiers, keys, number text): fixed-size overlapping moves instead of - * a libc call. Longer spans keep the platform's bulk copy. */ -static inline void scr_copy_bytes(char *dst, const char *src, size_t n) { - if (n <= 16) { - if (n >= 8) { - uint64_t a, b; - memcpy(&a, src, 8); - memcpy(&b, src + n - 8, 8); - memcpy(dst, &a, 8); - memcpy(dst + n - 8, &b, 8); - } else if (n >= 4) { - uint32_t a, b; - memcpy(&a, src, 4); - memcpy(&b, src + n - 4, 4); - memcpy(dst, &a, 4); - memcpy(dst + n - 4, &b, 4); - } else if (n) { - dst[0] = src[0]; - dst[n / 2] = src[n / 2]; - dst[n - 1] = src[n - 1]; - } - return; - } - memcpy(dst, src, n); -} - -/* Whether a byte span is all ASCII: 32 bytes per step, stopping at the - * first chunk with a high bit. */ -static inline bool scr_bytes_all_ascii(const char *p, size_t n) { - const uint64_t high = UINT64_C(0x8080808080808080); - size_t i = 0; - for (; n - i >= 32; i += 32) { - uint64_t w[4]; - memcpy(w, p + i, sizeof w); - if ((w[0] | w[1] | w[2] | w[3]) & high) return false; - } - uint64_t acc = 0; - for (; n - i >= 8; i += 8) { - uint64_t w; - memcpy(&w, p + i, 8); - acc |= w; - } - for (; i < n; i++) acc |= (unsigned char)p[i]; - return (acc & high) == 0; -} - -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: 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) */ - -/* ── lone surrogates: canonical WTF-8 storage ───────────────────────── - * String storage is UTF-8 extended to lone UTF-16 surrogates: an unpaired - * surrogate is its generalized 3-byte encoding ED A0..BF 80..BF, while a - * high surrogate directly followed by a low one is always stored as the - * pair's 4-byte UTF-8 sequence. Canonical storage keeps byte equality, - * hashing and searching exact. Builders preserve it by fusing a trailing - * lone high surrogate with a leading lone low one (scr_wtf8_joins). A lone - * surrogate is exactly as long as U+FFFD, so byte sinks transcode to UTF-8 - * (scr_str_utf8) without changing any length or offset. */ - -/* Set once a lone surrogate may exist anywhere in the process: program - * startup stamps it when a literal holds one, and every runtime producer - * notes it (scr_wtf8_put, scr_wtf8_note). Until then no string can hold a - * lone surrogate, so builders and byte sinks skip all surrogate work. It is - * process-wide (strings cross threads) and never cleared. */ -extern bool scr_wtf8_lone; -static inline bool scr_wtf8_any(void) { return __atomic_load_n(&scr_wtf8_lone, __ATOMIC_RELAXED); } -void scr_wtf8_note(void); - -/* Whether p starts a lone surrogate (p[1] must be readable: string data - * is NUL-terminated). */ -static inline bool scr_wtf8_surrogate_at(const char *p) { - return (unsigned char)p[0] == 0xED && (unsigned char)p[1] >= 0xA0; -} - -/* Whether p starts a lone low surrogate (p[1] readable, as above). */ -static inline bool scr_wtf8_low_at(const char *p) { - return (unsigned char)p[0] == 0xED && (unsigned char)p[1] >= 0xB0; -} - -/* Whether appending b to a puts a lone high surrogate directly before a - * lone low surrogate. Tests the first byte of b before anything else, so - * ordinary text costs one compare. */ -static inline bool scr_wtf8_joins(const char *a, size_t alen, const char *b, size_t blen) { - return __builtin_expect(scr_wtf8_any() && (unsigned char)b[0] == 0xED && blen >= 3 && - (unsigned char)b[1] >= 0xB0 && alen >= 3 && - (unsigned char)a[alen - 3] == 0xED && - ((unsigned char)a[alen - 2] & 0xF0) == 0xA0, - 0); -} - -/* The first lone surrogate in a span, or NULL. memchr-driven. */ -const char *scr_wtf8_find_surrogate(const char *p, size_t n); -/* Fuse every adjacent lone high/low pair in place; returns the new length. */ -size_t scr_wtf8_fuse(char *p, size_t n); -/* Restore canonical storage after a builder joined a lone high surrogate - * to a lone low one: fuses in place and drops cached content facts. The - * _from form starts at a byte offset before which storage is canonical. */ -void scr_str_fuse_surrogates(ScrStr *s); -void scr_str_fuse_from(ScrStr *s, size_t from); -/* Replace every lone surrogate with U+FFFD in place (same length). */ -void scr_wtf8_replace_surrogates(char *p, size_t n); -static inline void scr_wtf8_to_utf8(char *p, size_t n) { - if (__builtin_expect(scr_wtf8_any(), 0)) scr_wtf8_replace_surrogates(p, n); -} -/* A string's bytes as well-formed UTF-8 for a byte sink: s->data itself, - * or (only when s holds a lone surrogate) a malloc'd NUL-terminated copy - * with each one replaced by U+FFFD, returned in *owned for the caller to - * release with scr_str_utf8_end. *owned is NULL on the fast path. */ -const char *scr_str_utf8_copy(const ScrStr *s, char **owned); -static inline const char *scr_str_utf8(const ScrStr *s, char **owned) { - *owned = NULL; - if (s->ascii || !scr_wtf8_any()) return s->data; - return scr_str_utf8_copy(s, owned); -} -static inline void scr_str_utf8_end(char *owned) { - if (owned) free(owned); -} -static inline bool scr_str_has_surrogate(const ScrStr *s) { - return scr_wtf8_any() && !s->ascii && scr_wtf8_find_surrogate(s->data, s->len) != NULL; -} -/* Encode one UTF-16 code unit or code point as WTF-8 (1-4 bytes). */ -size_t scr_wtf8_put(char *dst, uint32_t cp); - -/* Borrow both args, return +1. */ -ScrStr *scr_str_concat(ScrStr *a, ScrStr *b); -/* Borrow each part without mutation; return one independently owned result. */ -ScrStr *scr_str_concat_parts(ScrStr *const *parts, size_t count); -/* Concatenate up to 16 string or number operands (parts[i] == NULL selects - * nums[i], formatted like String(n)) onto an optional borrowed head, +1 - * result. The head may be appended in place under scr_str_concat's rule for - * its left operand; parts are never mutated. */ -ScrStr *scr_str_concat_mixed(ScrStr *head, ScrStr *const *parts, const double *nums, size_t count); -/* The same with the left operand / head MOVED in (`s = s + x` when nothing - * else can observe `s`): one call instead of a snapshot retain, the - * binding's release and the snapshot's release around a borrowing concat. */ -ScrStr *scr_str_concat_move(ScrStr *a, ScrStr *b); -ScrStr *scr_str_concat_mixed_move(ScrStr *head, ScrStr *const *parts, const double *nums, - size_t count); -/* a + String(x): scr_str_concat's contract with a number right operand. */ -ScrStr *scr_str_concat_f64(ScrStr *a, double x); - -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); -/* a.localeCompare(b) with no locales/options: Node's root-locale order for - * Latin-script text, code-point order outside it (scr_string.c). */ -double scr_str_locale_compare(ScrStr *a, ScrStr *b); - -/* ECMAScript string-list ordering: compare UTF-16 code units even though - * ScrStr stores WTF-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 *prototype_data; - struct ScrBox *static_data; - struct ScrBox *caps[]; -} ScrClassObj; - -static inline ScrClassObj *scr_classobj_retain(ScrClassObj *c) { - if (c && c->rc != SIZE_MAX) { - c->rc++; - } - return c; -} -void scr_classobj_release(ScrClassObj *c); -void scr_classobj_clear_properties(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); -struct ScrDyn *scr_dyn_class_prototype(struct ScrDyn *value, struct ScrDyn *base); -struct ScrDyn *scr_dyn_class_base_prototype(struct ScrDyn *constructor); -struct ScrDyn *scr_dyn_class_inherit(struct ScrDyn *constructor, struct ScrDyn *base); -void scr_dyn_class_super(struct ScrDyn *constructor, struct ScrDyn *receiver, struct ScrDyn *arguments); -struct ScrDyn *scr_dyn_assign_prototype(struct ScrDyn *target, struct ScrDyn *sources, struct ScrDyn *protected_keys); -struct ScrDyn *scr_dyn_bag_get(struct ScrDyn *bag, ScrStr *key, struct ScrDyn *receiver); -struct ScrDyn *scr_dyn_reflect_get(struct ScrDyn *target, struct ScrDyn *key, struct ScrDyn *receiver); -bool scr_dyn_reflect_set(struct ScrDyn *target, struct ScrDyn *key, struct ScrDyn *value, struct ScrDyn *receiver); -bool scr_dyn_reflect_define(struct ScrDyn *receiver, struct ScrDyn *key, struct ScrDyn *value); -bool scr_dyn_abstract_eq(const struct ScrDyn *left, const struct ScrDyn *right); -int scr_dyn_function_kind(const struct ScrDyn *value); -void scr_dyn_bag_set(struct ScrDyn *bag, ScrStr *key, struct ScrDyn *value, struct ScrDyn *receiver); -/* 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); - -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 */ - bool message_present; /* super() leaves the inherited message property visible */ - bool message_enumerable; /* assignment creates an enumerable own property */ - bool name_present; - bool name_enumerable; - ScrStr *stack_frames; /* captured native source frames */ - ScrStr *stack; /* lazily formatted, cached stack */ - double system_errno; /* present exactly when system_call is non-NULL */ - ScrStr *system_call; - ScrStr *system_path; - ScrStr *system_dest; -} 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_AGGREGATE = 8, /* AggregateError — ScrAggregateError, the wider layout */ - SCR_ERR_COUNT = 9, -}; - -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; - bool message_present; - bool message_enumerable; - bool name_present; - bool name_enumerable; - ScrStr *stack_frames; - ScrStr *stack; - double system_errno; - ScrStr *system_call; - ScrStr *system_path; - ScrStr *system_dest; - 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. */ -SCR_COLD void scr_throw_domex(const char *name, const char *message); -SCR_COLD void scr_throw_domex_str(const char *name, ScrStr *message); - -/* AggregateError: the ScrError prefix plus the `errors` list (a dyn - * array; NULL until set). Like DOMException's slots it is HIDDEN from - * the compiler's IR field list (user `extends AggregateError` is fenced) - * and read through scr_aggregate_errors. The dyn half (construction from - * an iterable's elements, the list itself) lives in scr_json.c; the - * teardown drops the list through the hook it installs. */ -typedef struct ScrAggregateError { - ScrError base; - struct ScrDyn *errors; -} ScrAggregateError; -ScrError *scr_aggregate_alloc(void); -void scr_aggregate_install_errors_drop(void (*fn)(void *obj)); -/* new AggregateError(errors, message?, options?) runs in spec order: - * scr_aggregate_new installs the message and cause (both borrowed, dyn - * undefined when absent; +1, NULL with an exception pending when the - * message conversion throws), then the caller iterates `errors` and - * hands the list to scr_aggregate_set_errors. */ -ScrError *scr_aggregate_new(const struct ScrDyn *message, const struct ScrDyn *options); -void scr_aggregate_set_errors(ScrError *e, struct ScrDyn *errors); /* both borrowed */ -struct ScrDyn *scr_aggregate_errors(ScrError *e); /* +1 */ - -/* 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. */ -SCR_COLD 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_define_cause(ScrError *e, struct ScrDyn *value); -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); -void scr_error_install_message_reader(ScrStr *(*reader)(ScrError *)); -void scr_error_install_name_reader(ScrStr *(*reader)(ScrError *)); -ScrStr *scr_error_default_name(const ScrError *e); -ScrStr *scr_error_stack(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 takes ownership of both strings and picks the - * builtin vtable whose class name matches `name` (Error otherwise). */ -SCR_COLD void scr_throw_error(int kind, ScrStr *message); -SCR_COLD void scr_throw_error_msg(int kind, const char *message, size_t len); -SCR_COLD void scr_throw_error_named(ScrStr *name, ScrStr *message); -/* A read of a declare-d const nothing defines: throws Node's catchable - * ReferenceError " is not defined". Borrows name; always throws. */ -SCR_COLD 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); -SCR_COLD 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. */ -SCR_COLD 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. - * - * Where JS produces a *lone surrogate* — charAt on half of an astral - * pair, or a slice boundary that splits a pair — the result holds that - * surrogate (canonical WTF-8, see above). - */ - -/* .length — number of UTF-16 code units (astral chars count as 2). */ -double scr_str_utf16_len(ScrStr *s); - -/* The receiver of an indexed scan whose UTF-16 code units the index cache - * holds (scr_string.c), published so emitted length and charCodeAt reads of - * that string are a compare and a load. Cleared whenever its entry drops - * the units (release, append, eviction); all fields zero otherwise. Emitted - * code reads it only where SCR_TL is empty. */ -typedef struct ScrStrScan { - const ScrStr *s; - const uint16_t *units; - size_t n; -} ScrStrScan; -extern SCR_TL ScrStrScan scr_str_scan; - -/* 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 keep the half character as a lone surrogate. 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); - -/* Scalar-only local slices retain their original owner without copying. - * Bounds and split-surrogate replacements are snapshotted at construction. */ -typedef struct ScrStringSlice { - size_t start, length; - uint32_t split; -} ScrStringSlice; -void scr_str_slice_range(ScrStr *s, double start, double end, bool substring, - ScrStringSlice *out); -double scr_str_slice_char_code_at(ScrStr *s, const ScrStringSlice *range, double index); - -/* 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); -ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form); -/* NFC over a code point buffer (scr_regex.c, beside normalize): a fresh - * malloc'd buffer the caller frees, its length in *out_len. Borrows input. */ -uint32_t *scr_nfc_code_points(const uint32_t *input, size_t length, size_t *out_len); - -/* UTS #46 domain conversion (scr_idna.c — the url unit's IDNA tables; it - * rides the regex switch for NFC). to_ascii answers "" on failure; - * to_unicode decodes each valid Punycode label and never fails. Both - * borrow the UTF-8 span and return +1. */ -ScrStr *scr_idna_to_ascii(const char *p, size_t n); -ScrStr *scr_idna_to_unicode(const char *p, size_t n); - -/* charAt(i): 1-code-unit string; out of range → empty string. Half of an - * astral pair → that lone surrogate. 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 - * a lone surrogate); otherwise splits on every occurrence, - * empty pieces kept. Borrows both; returns a +1 string[] (SCR_ELEM_STR). */ -struct ScrArr; -/* String.prototype.replace/replaceAll with a string search value and a - * string replacement: GetSubstitution without captures ($$, $&, $`, $'; - * every other '$' spelling stays literal). An empty search value matches - * at 0, or at every UTF-16 position under replaceAll. Borrows; +1. */ -ScrStr *scr_str_replace_literal(ScrStr *s, ScrStr *search, ScrStr *rep, bool all); -/* Every occurrence of the ASCII byte chars[k] replaced by the k-th - * NUL-separated text of `texts`, in one pass (at most 16 distinct - * characters; the compiler fuses chains of global one-character - * replacements into it). Borrows all three; +1. */ -ScrStr *scr_str_replace_chars(ScrStr *s, ScrStr *chars, ScrStr *texts); -/* The same over a nonempty canonical WTF-8 needle (literal regex - * patterns share it). Borrows; +1. */ -ScrStr *scr_str_replace_bytes(ScrStr *s, const char *pat, size_t plen, ScrStr *rep, bool all); -/* First occurrence of a nonempty needle in a byte span, or NULL. */ -const char *scr_str_find_bytes(const char *hay, size_t hay_len, const char *needle, - size_t needle_len); -struct ScrArr *scr_str_split(ScrStr *s, ScrStr *sep); -struct ScrArr *scr_str_split_limit(ScrStr *s, ScrStr *sep, double limit); -/* Internal dense split traversal. The cursor borrows input strings, returns - * one owned piece per step, and uses NULL only for exhaustion. Its layout - * is shared with generated stack storage, including wasm32. */ -typedef struct ScrSplitCursor { - size_t offset; - uint32_t remaining; - uint32_t pending_unit; /* the low surrogate still owed by a split pair */ - char unit[4]; /* the bytes of the current surrogate piece */ -} ScrSplitCursor; -void scr_str_split_cursor_init(ScrSplitCursor *cursor, uint32_t limit); -/* Optional scratch is a separately owned, NULL-initialized string slot. - * A piece that has not escaped can reuse it after the preceding yield is - * released. The caller releases the slot on every exit; NULL disables reuse. */ -ScrStr *scr_str_split_cursor_next(ScrStr *s, ScrStr *sep, ScrSplitCursor *cursor, - ScrStr **scratch); -/* Borrowed split bytes remain valid while the source snapshot is owned. - * Materialization returns +1 and uses the same optional scratch contract. */ -const char *scr_str_split_cursor_span(ScrStr *s, ScrStr *sep, ScrSplitCursor *cursor, - size_t *length); -ScrStr *scr_str_split_materialize(const char *bytes, size_t length, ScrStr **scratch); - -/* 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 keeps its - * high half as a lone surrogate. 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(): whether s holds no lone surrogate, and - * s with each lone surrogate replaced by U+FFFD (s itself, retained, when - * it has none). Borrow s; toWellFormed +1. */ -bool scr_str_is_well_formed(ScrStr *s); -ScrStr *scr_str_to_well_formed(ScrStr *s); -/* Node text utilities: borrowed input, +1 result or NULL+pending. The VT - * helper lives in the optional regex unit; USV conversion uses native strings. */ -ScrStr *scr_util_strip_vt(const struct ScrDyn *input); -ScrStr *scr_util_to_usv_string(const struct ScrDyn *input); -ScrStr *scr_util_to_string(const struct ScrDyn *input); /* ToString alone */ -void scr_dyn_install_to_primitive_symbol(struct ScrDyn *key); - -/* 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), a lone surrogate \uNNNN-escapes, the rest - * passes through. Total. 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); -/* parseInt over a borrowed UTF-8 byte span. */ -double scr_parse_int_bytes(const char *p, size_t n, 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); -/* StringToNumber over a borrowed UTF-8 byte span. */ -double scr_bytes_to_number(const char *p, size_t len); - -/* 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). A lone surrogate throws the - * spec's URIError ("URI malformed"). Borrows s; result +1. */ -ScrStr *scr_str_encode_uri_component(ScrStr *s); -/* querystring.escape: the component encoder, throwing Node's URIError - * [ERR_INVALID_URI] on a lone surrogate. Borrows s; result +1. */ -ScrStr *scr_qs_escape(ScrStr *s); -/* lib/internal/querystring.js encodeStr over UTF-16 code units: ASCII - * outside `no_escape` and every non-ASCII code unit percent-encode as - * UTF-8, and ANY surrogate pairs with the code unit after it (Node's own - * rule); only a surrogate in last position throws URIError - * [ERR_INVALID_URI] and returns NULL. Borrows s; +1. */ -ScrStr *scr_qs_encode_str(const ScrStr *s, const signed char no_escape[128]); - -/* 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); -ScrStr *scr_str_decode_uri(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_qs_escape beside the component encoder (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); -/* querystring.parse(qs) with the default separators and maxKeys, as a - * fresh checked-dynamic null-prototype object (url.parse's parsed query). - * Borrows qs; +1; never throws. */ -struct ScrDyn *scr_qs_parse_dyn(const ScrStr *qs); - -/* 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); -ScrStr *scr_util_style_text(const struct ScrDyn *format, const struct ScrDyn *text, const struct ScrDyn *options); -/* Node's terminal color decision (FORCE_COLOR, NO_COLOR, TERM, CI hosts) - * once the stream is a terminal (scr_util_style.c). */ -bool scr_util_terminal_colors(void); -/* util.inherits, util.deprecate, util.callbackify and util.debuglog - * (scr_util_callable.c). Arguments are borrowed; function results are +1 - * native dyn functions, or NULL with Node's TypeError pending. */ -void scr_util_inherits(struct ScrDyn *ctor, struct ScrDyn *super_ctor); -struct ScrDyn *scr_util_deprecate(struct ScrDyn *fn, struct ScrDyn *msg, struct ScrDyn *code, - struct ScrDyn *options); -struct ScrDyn *scr_util_callbackify(struct ScrDyn *original); -struct ScrDyn *scr_util_debuglog(struct ScrDyn *section, struct ScrDyn *callback); - -/* Node/libuv system error utilities. Arguments are checked without coercion; - * entries is a fresh array of [number, [name, message]] pairs (+1). */ -ScrStr *scr_util_system_error_name(const struct ScrDyn *error); -ScrStr *scr_util_system_error_message(const struct ScrDyn *error); -struct ScrDyn *scr_util_system_error_entries(void); - -/* Node 24's dotenv grammar, shared by util.parseEnv and process.loadEnvFile. - * Arguments are borrowed; parseEnv returns a fresh ordinary object (+1). - * Validation and file failures leave a coded exception pending. */ -struct ScrDyn *scr_util_parse_env(const struct ScrDyn *content); -/* Checked primitives, ordinary records/arrays and byte views. Opaque native - * objects and accessor descriptors retain explicit catchable refusals. */ -bool scr_util_is_deep_strict_equal(const struct ScrDyn *a, const struct ScrDyn *b, - const struct ScrDyn *skip_prototype); -void scr_process_load_env_file(const struct ScrDyn *path); - -/* 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; a lone surrogate throws URIError [ERR_INVALID_URI] (NULL). */ -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). Throws the same - * URIError on a lone surrogate. 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/pointers via memcpy and casts), except SCR_ELEM_BOOL, whose - * dense storage holds one byte (0 or 1) per element; `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 out of bounds (length ) - * 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_ops) as one - * constant descriptor per element type, stored once per array; 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; - -/* The RC entry points of an SCR_ELEM_REF array's element type: one constant - * per element type (emitted by the compiler, or static in the runtime), - * shared by every array of that type. retain/release are the type's `_v` - * adapters; trace is non-NULL exactly when the element type carries a cycle - * header. */ -typedef struct ScrArrOps { - void *(*retain)(void *); - void (*release)(void *); - ScrTraceFn trace; -} ScrArrOps; - -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 */ - /* One ScrElemKind. */ - uint8_t elem; - /* 1 exactly when ops->trace is non-NULL: the element type carries a cycle - * header, so the array is CYCLE-CAPABLE (an element can point back at the - * array that owns it) and allocates with the hidden collector header — - * its 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. Kept beside `elem` so the RC fast paths test it with one load. */ - uint8_t cyc; - uint16_t spare; - /* A lower bound on the hole-free prefix: filled <= len, filled <= cap, - * and every dense state below it is SCR_ARR_VALUE. Element reads below - * it need no state check and element writes below it leave the states - * alone. Any operation that may store another state (a hole, present - * undefined) or move states below it lowers it first (scr_arr_cut); - * 0 is always valid. */ - uint32_t filled; - /* SCR_ELEM_REF only; NULL for every other element kind. */ - const ScrArrOps *ops; - /* Dense values: `cap` 8-byte slots, or `cap` bytes for SCR_ELEM_BOOL - * (scr_arr_slot_width). Sparse entries and properties keep 8-byte slots - * for every kind; a bool slot is 0 or 1 wherever it lives. */ - uint64_t *data; - uint8_t *present; /* one state per dense slot; heap arrays share data allocation */ - /* Rarely used storage (sparse indices, noncanonical numeric properties, - * match-result metadata) lives out of line: NULL until first needed, so - * the common dense array stays small. */ - struct ScrArrExt *ext; -} ScrArr; - -typedef struct ScrArrExt { - 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; - struct ScrDyn *metadata; /* heterogeneous named properties of match results */ -} ScrArrExt; -/* The LLVM backend mirrors ScrArr as %ScrArr: its inline retain/release read - * `cyc` (field 4, i8) to decide mark-live / candidate buffering exactly like - * scr_arr_retain and scr_arr_release; its inline element access uses filled - * (field 6, i32), data (field 8) and present (field 9). */ -_Static_assert(offsetof(ScrArr, cyc) == 3 * sizeof(size_t) + 1, - "inline RC fast paths read ScrArr.cyc as %ScrArr field 4"); -_Static_assert(offsetof(ScrArr, filled) == 3 * sizeof(size_t) + 4, - "inline element access reads ScrArr.filled as %ScrArr field 6"); -_Static_assert(offsetof(ScrArr, data) == 3 * sizeof(size_t) + 8 + sizeof(void *), - "inline element reads use ScrArr.data as %ScrArr field 8"); - -/* Bytes per dense value slot. */ -static inline size_t scr_arr_slot_width(const ScrArr *a) { - return a->elem == SCR_ELEM_BOOL ? 1 : sizeof(uint64_t); -} - -/* The dense value at `index` (< cap) as an 8-byte slot, and its store. */ -static inline uint64_t scr_arr_dense_get(const ScrArr *a, size_t index) { - if (a->elem == SCR_ELEM_BOOL) return ((const uint8_t *)a->data)[index]; - return a->data[index]; -} - -static inline void scr_arr_dense_put(ScrArr *a, size_t index, uint64_t slot) { - if (a->elem == SCR_ELEM_BOOL) ((uint8_t *)a->data)[index] = (uint8_t)slot; - else a->data[index] = slot; -} - -/* Lower the hole-free prefix to at most `index`: call before storing a - * non-value state at `index`, or before moving states into [index, ...). */ -static inline void scr_arr_cut(ScrArr *a, size_t index) { - if (a->filled > index) a->filled = (uint32_t)index; -} - -/* The first index in [f, end) whose state is not a present value (or end). - * Compares eight states per step: bulk copies (slice, concat, spread) end - * with this scan over the whole copied range. */ -static inline size_t scr_arr_value_run(const uint8_t *present, size_t f, size_t end) { - const uint64_t values = 0x0101010101010101ull * SCR_ARR_VALUE; - while (end - f >= 8) { - uint64_t word; - memcpy(&word, present + f, sizeof word); - if (word != values) break; - f += 8; - } - while (f < end && present[f] == SCR_ARR_VALUE) f++; - return f; -} - -/* Extend the hole-free prefix over populated dense states. Starts at the - * current bound, so after an append it costs only the appended range. */ -static inline void scr_arr_refill(ScrArr *a) { - size_t end = a->len < a->cap ? a->len : a->cap; - size_t f = a->filled; - if (f < end) f = scr_arr_value_run(a->present, f, end); - a->filled = (uint32_t)f; -} - -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: `ops` must outlive the array (a constant; the - * compiler emits one per element type) and its retain/release must be - * non-NULL; ops->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_ops(const ScrArrOps *ops, size_t initial_cap); -/* An empty array expected to receive a few elements: its first slots live - * in the array's own block (one allocation; the block of a plain empty - * array plus five 8-byte slots). Otherwise exactly scr_arr_new(elem, 0) and - * scr_arr_new_ops(ops, 0). Returns +1. */ -ScrArr *scr_arr_new_small(ScrElemKind elem); -ScrArr *scr_arr_new_ops_small(const ScrArrOps *ops); - -static inline ScrArr *scr_arr_retain(ScrArr *a) { - if (a->rc != SIZE_MAX) { - a->rc++; - } - return a; -} - -/* Releases ref elements (SCR_ELEM_STR/ARR/BYTES/REF) recursively at rc == 0. - * NULL-tolerant (uninitialized locals). Cycle-capable arrays (`cyc`) feed - * the candidate-root buffer like every headered type. */ -void scr_arr_release(ScrArr *a); -void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target); -/* The array's out-of-line storage, allocated (zeroed) on first use. */ -ScrArrExt *scr_arr_ext(ScrArr *a); -/* Moves inline dense slots (kept in the header block of a small array) to - * their own allocation, so the storage pointers may be swapped with another - * array's. No-op for other arrays. */ -void scr_arr_outline(ScrArr *a); -/* Swaps the sparse slots and numeric properties of two arrays; each keeps - * its own match metadata (refresh of a cached typed view, llvm/dyn.ts). */ -void scr_arr_swap_indexed_ext(ScrArr *a, ScrArr *b); - -/* 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); -/* Mutating indexed operations borrow the receiver and value, preserve - * sparse presence, and return the receiver retained. */ -ScrArr *scr_arr_copy_within(ScrArr *a, double target, double start, double end); -ScrArr *scr_arr_fill_f64(ScrArr *a, double value, double start, double end); -ScrArr *scr_arr_fill_bool(ScrArr *a, bool value, double start, double end); -ScrArr *scr_arr_fill_ref(ScrArr *a, void *value, double start, double end); -ScrArr *scr_arr_fill_undefined(ScrArr *a, double start, double end); -/* 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 */ -void *scr_arr_borrow_ref(ScrArr *a, double i); /* same checks; borrowed */ -/* Borrow a non-null reference element, or NULL for a hole/undefined/missing - * property. Keep the array alive and retain before removing its element. - * This read does not invoke retain callbacks or mutate runtime state. */ -void *scr_arr_peek_ref(const ScrArr *a, double i); - -/* 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); -/* Compiler-built slot buffers do not alias array storage. Both operations - * move ownership of every slot and preserve the evaluated argument order. */ -double scr_arr_push_many(ScrArr *a, size_t count, const uint64_t *slots); -double scr_arr_unshift_many(ScrArr *a, size_t count, const uint64_t *slots); -/* 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); -/* Default primitive String ordering; stable, borrowed receiver, owned result. */ -ScrArr *scr_arr_sort_primitive(ScrArr *a, bool copy); -/* Stable comparator sort of the first `count` slots of a private, compacted - * snapshot (every slot a value; f64 or reference elements). `f` is a borrowed - * closure of `arity` (0..2) element parameters returning a number. Elements - * move without reference-count traffic; a pending exception from `f` stops - * the sort and leaves the snapshot unchanged. */ -struct ScrClosure; -/* A nonzero `order` says `f` orders number keys ascending (1) or descending - * (-1) without side effects: f64 elements are their own keys, and reference - * elements read theirs through the borrowed `key` closure (element -> - * number). Keys that are all numbers other than NaN and -0 then sort without - * calling `f`, with the same stable result. */ -void scr_arr_sort_values(ScrArr *a, double count, struct ScrClosure *f, uint32_t arity, - int32_t order, struct ScrClosure *key); -/* Comparator sort of a dense array in place: `a.sort(f)`, or with `owned` - * the private copy of `a.toSorted(f)`. Returns false without calling `f` - * when `a` has holes, storage-level undefined or a sparse tail, or is - * published; the caller then sorts a compacted snapshot. Otherwise sorts - * and returns true; a pending exception from `f` leaves a shared `a` - * unwritten (an owned copy then holds a permutation of its values). */ -/* The borrowed 8-byte slots of `a` when every index below its length holds - * a value in dense storage (length in *len); NULL for holes, storage-level - * undefined, sparse tails, empty arrays and boolean elements. */ -const uint64_t *scr_arr_dense_slots(const ScrArr *a, size_t *len); -bool scr_arr_sort_dense(ScrArr *a, struct ScrClosure *f, uint32_t arity, bool owned, - int32_t order, struct ScrClosure *key); -/* V8's TimSort over n slots with a caller comparison: less(ctx, x, y) - * answers whether x orders before y (comparefn(x, y) < 0), or sets *failed, - * with an exception pending, to stop the sort. Returns false after a - * failure; `values` is then unchanged, and it holds every slot exactly once, - * in its original order, whenever `less` runs. */ -typedef bool (*ScrSortLess)(void *ctx, uint64_t x, uint64_t y, bool *failed); -bool scr_sort_slots_with(uint64_t *values, size_t n, ScrSortLess less, void *ctx); - -/* 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); -/* The same splice in statement position: inserts `count` owned value slots - * (moved in) and releases the removed elements without building a result - * array. Borrows a. */ -void scr_arr_splice_drop(ScrArr *a, double start, double deleteCount, size_t count, - const uint64_t *slots); -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); - -typedef int32_t (*ScrRegexMatcherFn)(const uint8_t *s, int32_t len, int32_t start, - uint8_t **capture); - -typedef struct ScrRegex { - size_t rc; /* SIZE_MAX = immortal literal template */ - 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 */ - double last_index; - struct ScrRegex *literal; /* immutable bytecode owner for literal instances */ - void *native; /* one-byte-subject matcher built with bc (NULL: lre_exec only) */ - /* Build-time matcher of a literal template (NULL: none). Searches the - * ASCII bytes s[0..len) from start (one anchored attempt when sticky); - * on a match stores 2 * ncap pointers into s (NULL when unset) and - * returns 1, otherwise 0. A negative len returns ncap. */ - ScrRegexMatcherFn matcher; -} ScrRegex; - -/* Backtrack stack of build-time regex matchers: grow *stack (initially the - * matcher's inline buffer) to at least `need` words; traps on OOM. */ -void scr_regex_stack_grow(int32_t **stack, int32_t *cap, int32_t *inline_buf, int32_t need); -void scr_regex_stack_free(int32_t *stack); - -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); -ScrRegex *scr_regex_literal(ScrRegex *literal); /* fresh state, shared cached bytecode */ -double scr_regex_last_index(ScrRegex *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); /* exec state transitions */ -/* s.match(re): +1 string[] of whole matches under /g, otherwise - * [whole, ...captures] (nonparticipating captures hold ""), or NULL for - * no match. Non-global /y uses exec state transitions. Borrows both. */ -ScrArr *scr_regex_match(ScrStr *s, ScrRegex *re); -/* re.exec(s), string-first ABI: match slice with global/sticky lastIndex - * transitions. Borrows both; +1 or NULL. */ -ScrArr *scr_regex_exec(ScrStr *s, ScrRegex *re); -/* exec for a result read only through constant indices and null tests: - * bit k of `mask` (k < 53) selects the captures that are materialized; the - * rest, and the index/input properties, are left out. Same lastIndex - * transitions and NULL result as scr_regex_exec. Borrows both; +1 or NULL. */ -ScrArr *scr_regex_exec_groups(ScrStr *s, ScrRegex *re, double mask); -/* scr_regex_exec_groups whose captures in `lazy` (a subset of `mask`) are - * read only through scr_regex_group_eq/_number/_parse_int: they may stay - * spans of the subject instead of strings. */ -ScrArr *scr_regex_exec_spans(ScrStr *s, ScrRegex *re, double mask, double lazy); -/* s.match(re) read only through constant indices and null tests: a global - * regex answers scr_regex_match; otherwise scr_regex_exec_spans. */ -ScrArr *scr_regex_match_groups(ScrStr *s, ScrRegex *re, double mask, double lazy); -/* Reads of a lazy capture k of such a result (borrowed): `capture === lit`, - * Number(capture) and parseInt(capture, radix). */ -bool scr_regex_group_eq(ScrArr *m, double k, ScrStr *lit); -double scr_regex_group_number(ScrArr *m, double k); -double scr_regex_group_parse_int(ScrArr *m, double k, double radix); -/* 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 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). - * - * Primitive keys compare by value and reference keys by identity. Numbers - * use SameValueZero: NaN equals NaN and -0 is stored as +0. Union wrappers - * are representations, never identities; their arm descriptors select - * the primitive or reference comparison, including null and undefined. - * - * 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, - SCR_MAP_KEY_BIGINT, SCR_MAP_KEY_BOOL, SCR_MAP_KEY_UNION_VALUE, - SCR_MAP_KEY_NULL, SCR_MAP_KEY_UNDEFINED /* tag-only union arms */ -} 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 */ - uint64_t hash; /* nonzero cached hash; zero = tombstone (key/val released) */ -} ScrMapEntry; - -/* Checked views use the original storage. Slot converters borrow on reads - * and return owned references on writes, just like typed Map operations. */ -typedef struct { - const char *type; - struct ScrDyn *(*key_box)(uint64_t); - uint64_t (*key_unbox)(const struct ScrDyn *); - bool (*key_matches)(const struct ScrDyn *); - struct ScrDyn *(*val_box)(uint64_t); - uint64_t (*val_unbox)(const struct ScrDyn *); -} ScrMapDynOps; - -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; - /* Reference-represented keys, including bigint and union values. */ - 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) */ - /* Two bits per inserted entry hash (scr_map.c scr_map_filter_bits): a - * lookup whose bits are not all set is absent without touching the index - * or the entries. Deletions leave their bits; compaction recomputes it. */ - uint64_t filter; - size_t ecap; /* entries capacity */ - ScrMapEntry *entries; - size_t nbuckets; /* zero for small linear maps; otherwise power of two >= 2 * nentries */ - void *buckets; /* compact tagged indices, or size_t indices for large tables */ - size_t iter_depth; /* > 0: an iteration is active — no compaction */ - const ScrMapDynOps *dyn_ops; - const uint8_t *union_keys; /* immutable key-kind table indexed by union tag */ - /* Direct index for number keys (SCR_MAP_KEY_F64 only). While ndense is - * nonzero every live key is an integer in [0, ndense) and dense[k] holds - * its entry index + 1 (0 = absent); the map then keeps no bucket table. - * A key outside that shape moves the map to hashing; whenever the bucket - * table would grow, compact integer keys move it back. Iteration order is - * always the entries array's. */ - uint32_t *dense; - size_t ndense; - /* Every entry below this index is a tombstone. A walk that starts at or - * below it advances it to the first live entry (scr_map_iter_skip); - * compaction and clear reset it. A map that deletes from the front (a - * queue, an LRU cache) then finds its first live entry without walking - * the whole deleted prefix again. */ - size_t head; -} ScrMap; -/* The LLVM backend's inline retain/release read both trace slots through - * its %ScrMapRc prefix type (fields 5 and 8), mirroring scr_map_retain and - * scr_map_release's `key_trace || val_trace` header test. */ -_Static_assert(offsetof(ScrMap, val_trace) == sizeof(size_t) + 2 * sizeof(uint32_t) + 2 * sizeof(void *), - "inline RC fast paths read ScrMap.val_trace as %ScrMapRc field 5"); -_Static_assert(offsetof(ScrMap, key_trace) == offsetof(ScrMap, val_trace) + 3 * sizeof(void *), - "inline RC fast paths read ScrMap.key_trace as %ScrMapRc field 8"); -/* The LLVM backend's inline number-key lookup (sc_map_entry_f64) reads - * entries, dense and ndense through %ScrMapIx fields 13, 19 and 20, and - * entries as %ScrMapEntry { key, val, hash }; %ScrMapIx field 11 is the - * i64 filter. */ -_Static_assert(offsetof(ScrMap, filter) == - (offsetof(ScrMap, nlive) + sizeof(size_t) + _Alignof(uint64_t) - 1) / _Alignof(uint64_t) * _Alignof(uint64_t), - "%ScrMapIx field 11 is ScrMap.filter, directly after nlive"); -_Static_assert(offsetof(ScrMap, nlive) == sizeof(size_t) + 2 * sizeof(uint32_t) + 6 * sizeof(void *) + sizeof(size_t), - "%ScrMapIx field 10 is ScrMap.nlive"); -/* The inline iteration steps (sc_map_iter_entry) read the entry count as - * %ScrMapIx field 9 and test each entry's hash, the third field. */ -_Static_assert(offsetof(ScrMap, nentries) + sizeof(size_t) == offsetof(ScrMap, nlive), - "%ScrMapIx field 9 is ScrMap.nentries, directly before nlive"); -_Static_assert(offsetof(ScrMapEntry, hash) == 16, "inline iteration reads ScrMapEntry.hash at byte 16"); -_Static_assert(offsetof(ScrMap, entries) == offsetof(ScrMap, filter) + sizeof(uint64_t) + sizeof(size_t), - "inline number-key lookups read ScrMap.entries as %ScrMapIx field 13"); -_Static_assert(offsetof(ScrMap, dense) == offsetof(ScrMap, entries) + 6 * sizeof(void *), - "inline number-key lookups read ScrMap.dense as %ScrMapIx field 19"); -_Static_assert(offsetof(ScrMap, ndense) == offsetof(ScrMap, dense) + sizeof(void *), - "inline number-key lookups read ScrMap.ndense as %ScrMapIx field 20"); -_Static_assert(sizeof(ScrMapEntry) == 24 && offsetof(ScrMapEntry, val) == 8, - "inline number-key lookups read ScrMapEntry.val at byte 8"); -/* Entry for a number key, or NULL when absent. The pointer is valid until - * the next mutation; generated code reads the value slot through it. */ -const ScrMapEntry *scr_map_entry_f64(const ScrMap *m, double key); -/* Entry for a string key, or NULL when absent, on the same terms. Typed - * reference reads retain the value with its static type's retain instead - * of the collection's adapter. */ -const ScrMapEntry *scr_map_entry_str(const ScrMap *m, const ScrStr *key); - -/* Called once on a fresh UNION_VALUE collection, before its first insert. */ -void scr_map_union_keys(ScrMap *map, const uint8_t *kinds); -/* String-key lookups borrow an exact byte span. Reference results return - * +1; scalar outputs are written only on a hit, like the string getters. */ -bool scr_map_get_span_f64(const ScrMap *m, const char *bytes, size_t length, double *out); -bool scr_map_get_span_bool(const ScrMap *m, const char *bytes, size_t length, bool *out); -void *scr_map_get_span_ref(const ScrMap *m, const char *bytes, size_t length); - -/* Fresh shallow copies with compact storage and independent mutation. - * keys_only selects a Set; checked views materialize their boxed values. */ -ScrMap *scr_map_clone(const ScrMap *source, bool keys_only); -ScrMap *scr_map_clone_dyn(const ScrMap *source, bool keys_only); -void scr_map_values_into(ScrMap *set, const ScrMap *source, bool checked); - -void scr_map_dyn_attach(ScrMap *map, const ScrMapDynOps *ops); -struct ScrDyn *scr_map_dyn_key(const ScrMap *map, double index); -struct ScrDyn *scr_map_dyn_value(const ScrMap *map, double index); -struct ScrDyn *scr_map_dyn_get(ScrMap *map, const struct ScrDyn *key); -bool scr_map_dyn_has(ScrMap *map, const struct ScrDyn *key, bool remove); -void scr_map_dyn_set(ScrMap *map, const struct ScrDyn *key, const struct ScrDyn *value, bool set); - -/* 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_bool(const ScrMap *m, bool 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_bool(ScrMap *m, bool 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_bool_f64(ScrMap *m, bool key, double v); -void scr_map_set_bool_bool(ScrMap *m, bool key, bool v); -void scr_map_set_bool_ref(ScrMap *m, bool 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_bool_f64(const ScrMap *m, bool key, double *out); -bool scr_map_get_bool_bool(const ScrMap *m, bool key, bool *out); -void *scr_map_get_bool_ref(const ScrMap *m, bool 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); -/* The first live index at or after i, or iter_count when none is left. - * iter_start is the first live index of the map (for-of starts there). */ -double scr_map_iter_skip(ScrMap *m, double i); -double scr_map_iter_start(ScrMap *m); -double scr_map_iter_key_f64(const ScrMap *m, double i); -bool scr_map_iter_key_bool(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_bool(const ScrMap *s); -ScrArr *scr_set_to_arr_str(const ScrMap *s); -ScrArr *scr_set_to_arr_ref(const ScrMap *s, const ScrArrOps *ops); -ScrArr *scr_set_to_arr_dyn(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, as for arrays (scr_arr_new_ops). */ -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); -/* Publication makes objects immortal and never frees them, so the RC audit - * stops counting them (scr_message.c). */ -void scr_str_live_forget(void); -void scr_dyn_live_forget(void); -void scr_arr_live_forget(void); -void scr_map_live_forget(void); -void scr_union_live_forget(void); -#endif - -/* For the same reason LeakSanitizer must not report a published object - * whose last reference is dropped (scr_message.c, scr_publish.c). Ignored - * chunks are leak-scan roots, so everything an immortal object reaches is - * covered and the verdict on the rest of the heap is unchanged. Weak: ASan - * runtimes without a leak checker leave it NULL; other builds compile the - * hook away. */ -#if defined(__has_feature) -#if __has_feature(address_sanitizer) -#define SCR_LSAN_IMMORTAL 1 -void __lsan_ignore_object(const void *p) __attribute__((weak)); -#endif -#endif -static inline void scr_lsan_immortal(const void *obj) { -#ifdef SCR_LSAN_IMMORTAL - if (__lsan_ignore_object) __lsan_ignore_object(obj); -#else - (void)obj; -#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; - -/* A box can lie on a cycle only when its trace visits its payload: a - * closure, or a class instance that carries a cycle header. Every other box - * is a leaf of the object graph, so its releases buffer no candidate. The - * emitted inline release (llvm/shapes.ts, the box row) mirrors this test, - * reading `kind` and `obj_trace` at the asserted offsets. */ -static inline bool scr_box_traced(const ScrBox *b) { - return b->kind == SCR_BOX_FUNC || b->obj_trace != NULL; -} -_Static_assert(SCR_BOX_FUNC == 4, "the emitted box release compares kind against 4"); -#if UINTPTR_MAX == UINT64_MAX -_Static_assert(offsetof(ScrBox, kind) == 8 && offsetof(ScrBox, obj_trace) == 32, - "the emitted box release reads kind at 8 and obj_trace at 32"); -#else -_Static_assert(offsetof(ScrBox, kind) == 4 && offsetof(ScrBox, obj_trace) == 16, - "the emitted box release reads kind at 4 and obj_trace at 16"); -#endif - -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++; - } - 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; - uint32_t function_kind; /* low bits: 0 ordinary, 1 generator, 2 async, 3 async generator; bit 2: ordinary own prototype; bit 3: checked callable adapter in caps[0] */ - /* The root's `length` as boxed values report it; meaningful only when - * identity is set (the root's own boxes carry their arity). */ - uint32_t identity_length; - /* The JS function this closure stands for. NULL means the closure is - * its own identity. A signature adapter (the same function viewed - * through a different native ABI) points at the root of the function - * it adapts, so chains of adapters collapse to one root and `===`, - * SameValue, searches and own properties all observe one function. - * Borrowed: the adapter keeps its original alive through its captures. */ - struct ScrClosure *identity; - /* The same function with every reference argument borrowed (the caller - * keeps each one alive across the call and releases nothing extra), or - * NULL. Same signature as `fn`; only callers that hold their arguments - * for the whole call may enter through it. */ - void *fn_borrowed; - 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++; - } - return c; -} - -void scr_closure_release(ScrClosure *c); /* releases the boxes; NULL-tolerant */ - -/* The identity root of a function value (NULL-tolerant). */ -static inline ScrClosure *scr_closure_identity(ScrClosure *c) { - return c != NULL && c->identity != NULL ? c->identity : c; -} - -/* JS function identity: two closures are the same function when their - * identity roots match. */ -bool scr_closure_identity_equal(ScrClosure *a, ScrClosure *b); - -/* Mark `adapter` as a signature view of `original` (both borrowed); - * `length` is the original's arity when it is a root. */ -void scr_closure_adopt_identity(ScrClosure *adapter, ScrClosure *original, uint32_t length); - -/* Mark `adapter` as a typed view of a dynamic function value (both - * borrowed). Other callables keep the adapter as its own identity. */ -void scr_dyn_adopt_identity(ScrClosure *adapter, const struct ScrDyn *value); - -/* ── 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; -/* The speed posture's inline union release (shapes.ts) reads arm_trace at - * this byte offset to skip buffering untraced boxes, like scr_union_release. */ -_Static_assert(offsetof(ScrUnion, arm_trace) == 2 * sizeof(size_t) + 2 * sizeof(void *), - "inline RC fast paths read ScrUnion.arm_trace at 32 (64-bit) / 16 (32-bit)"); - -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++; - } - 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 */ -/* Internal owner hook; observes listener presence without running script. */ -void scr_emitter_observe(ScrEmitter *em, - void (*observe)(void *, const ScrStr *, bool), void *context); -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. - * The fixed shims cover the common short tuples. Longer adapters use the - * opaque cursor interface below without depending on the va_list ABI. */ -#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); -/* Invoke cb->fn(cb, cursor). The cursor borrows a private va_list copy for - * this call; each next operation reads one borrowed pointer argument. */ -void scr_ee_inv_args(ScrClosure *cb, va_list ap); -void *scr_ee_arg_next(void *cursor); -/* 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_flex(ScrEmitter *em, ScrDyn *error); -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[] */ -/* events.errorMonitor's event key (immortal). Every name parameter above - * accepts it; the registry matches it by identity, so no string name, - * whatever its bytes, reaches the errorMonitor listeners. */ -ScrStr *scr_emitter_error_monitor(void); -/* 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 SCR_TL 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) */ -/* events.addAbortListener (scr_events_add_abort_listener): passed to the - * signal-taking stream and readline entry points by programs that link - * the event-target unit. */ -typedef ScrDyn *(*ScrAbortAddFn)(ScrDyn *signal, ScrDyn *listener); - -typedef struct ScrStream { - size_t rc; - const ScrVt *vt; - ScrEeReg *reg; /* ScrEmitter prefix */ - const char *cls; /* ScrEmitter prefix (display name) */ - ScrStreamState *st; -} ScrStream; - -extern SCR_TL 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 "not supported yet" 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); -ScrStream *scr_stream_on_dyn(ScrStream *s, ScrStr *name, ScrDyn *cb, bool once, bool prepend); -ScrDyn *scr_stream_read_dyn(ScrStream *s, ScrDyn *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, arrayBuffer - * a +1 ArrayBuffer dyn over them. 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); -ScrPromise *scr_sc_array_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); -/* stream.addAbortSignal(signal, s) and the constructors' signal option - * (from_option: a falsy signal is ignored). Borrows; answers s +1, or - * NULL with ERR_INVALID_ARG_TYPE pending. */ -ScrStream *scr_stream_add_abort_signal(ScrStream *s, ScrDyn *signal, bool from_option, - ScrAbortAddFn add); -/* finished(s, options[, cb]) — Node's eos with its listeners and state - * predicates: readable/writable are -1 (the default), 0 or 1; on_error is - * options.error !== false; signal is NULL or borrowed (ERR_INVALID_ARG_TYPE - * pending and NULL answered for a non-signal). The callback forms answer - * the +1 cleanup closure (cb moves; the typed form takes its invoke - * adapter); the promise form rejects instead of throwing. */ -ScrClosure *scr_stream_finished_opts(ScrStream *s, ScrClosure *cb, ScrStreamErrInv inv, - double readable, double writable, bool on_error, - ScrDyn *signal, ScrAbortAddFn add); -ScrClosure *scr_stream_finished_opts_dyn(ScrStream *s, ScrDyn *cb, double readable, - double writable, bool on_error, ScrDyn *signal, - ScrAbortAddFn add); -ScrPromise *scr_sp_finished_opts(ScrStream *s, double readable, double writable, bool on_error, - bool cleanup, ScrDyn *signal, ScrAbortAddFn add); -/* stream/promises pipeline(...streams, { signal, end }): end false leaves - * the destination open; a non-signal value rejects with - * ERR_INVALID_ARG_TYPE. Streams borrowed. */ -ScrPromise *scr_sp_pipeline_opts(double n, ScrStream **streams, bool end, ScrDyn *signal, - ScrAbortAddFn add); -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, - /* Noncatchable context shutdown. Owns no payload; generated unwinding - * releases locals while bypassing user catch and finally handlers. */ - SCR_EXC_TERMINATE, -} 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 ScrStackFrame { - struct ScrStackFrame *previous; - const char *text; -} ScrStackFrame; - -void scr_stack_enter(ScrStackFrame *frame, const char *text); -void scr_stack_leave(ScrStackFrame *frame); -ScrStr *scr_stack_capture(void); /* +1, NULL when no source frames exist */ -double scr_stack_limit_get(void); -void scr_stack_limit_set(double limit); - -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; - ScrStackFrame *stack; /* execution state; never moves with an exception */ -} ScrExcCell; - -/* Runtime-internal (fiber machinery in scr_async.c) — never emitted. */ -ScrExcCell *scr_exc_swap_cell(ScrExcCell *cell); /* NULL = main's cell */ -/* The active cell, read inline by generated exception polls (thread-local - * builds use NULL for the main cell until scr_init installs it). */ -extern SCR_TL ScrExcCell *scr_exc_active; -ScrExcCell *scr_exc_current_cell(void); /* the ACTIVE cell */ -/* Runtime code that moved a payload into or out of the active cell (or a - * cell that may be active) without the throw/take/clear entry points. */ -void scr_exc_cell_changed(void); - -bool scr_exc_pending(void); - -#if !((defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) || defined(SCR_WORKERS)) -/* Runtime units test the active cell's kind inline, as emitted exception - * polls do (scr_exc_active is never NULL outside thread-instanced libraries - * and worker executables, which also poll cross-thread termination in - * scr_exc_pending). The out-of-line definition in scr_exception.c stays - * for those lanes and for emitted code's slow paths. */ -static inline bool scr_exc_pending_inline(void) { return scr_exc_active->kind != SCR_EXC_NONE; } -#define scr_exc_pending() scr_exc_pending_inline() -#endif -_Static_assert(offsetof(ScrExcCell, kind) == 0 && sizeof(ScrExcKind) == 4 && SCR_EXC_NONE == 0, - "the LLVM emitter's inline pending check loads an i32 kind at offset 0"); - -SCR_COLD void scr_throw_f64(double v); -SCR_COLD void scr_throw_bool(bool v); -SCR_COLD 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. */ -SCR_COLD 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. */ -SCR_COLD void scr_throw_ref_classified(void *v, void *(*retain)(void *), void (*release)(void *), - ScrTraceFn trace, bool object); -SCR_COLD 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. */ -SCR_COLD 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 - * participates in cycle collection because a closure may capture the - * binding and its payload may retain that closure. */ -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 */ -void scr_caught_trace_v(void *c, ScrTraceVisit visit, void *ctx); -/* 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 " 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 SCR_TL 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); - -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) */ - -bool scr_stdio_write(int fd, const void *data, size_t len); -bool scr_stdio_write_str(int fd, const ScrStr *s); -int scr_stdio_write_raw(int fd, const void *data, size_t len); -/* `wide` marks string bytes that may hold lone surrogates, which become - * U+FFFD when they reach the stream itself. */ -extern SCR_TL bool (*scr_stdio_write_hook)(int fd, const void *data, size_t len, bool wide); -/* 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.: +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 */ -/* NODE_DEBUG as the process started (NULL when unset), for util.debuglog. */ -const char *scr_node_debug_initial(void); -/* 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); -/* Nonzero hands the address-space teardown after scr_process_exit to a - * helper process (Linux, `--detached-exit`). scr_lib.c defines a weak 0; - * the program object of such an executable defines it as 1. */ -extern const unsigned char scr_detached_exit; -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); -/* perf_hooks user timing (scr_perf.c): mark, measure, the getEntries - * family and the clears take the call's dyn argument vector (borrowed) - * and apply Node's validation and errors. mark and measure answer 1 and - * the getters the entry count; the answered entries stay readable through - * the scr_perf_entry_* accessors (index into the answer) until the next - * timeline call. which: 0 getEntries, 1 getEntriesByName, 2 - * getEntriesByType. Accessor results are +1. */ -double scr_perf_mark(ScrDyn *args); -double scr_perf_measure(ScrDyn *args); -double scr_perf_entries(ScrDyn *args, double which); -void scr_perf_clear_marks(ScrDyn *args); -void scr_perf_clear_measures(ScrDyn *args); -ScrStr *scr_perf_entry_name(double i); -ScrStr *scr_perf_entry_type(double i); -double scr_perf_entry_start(double i); -double scr_perf_entry_duration(double i); -ScrDyn *scr_perf_entry_detail(double i); -/* The entry objects themselves: entry_keep stores the program's object - * for the last mark or measure (borrowed, retained), entry_ref answers - * entry i's stored object (+1), so the getters return the same objects - * mark() and measure() did. */ -void scr_perf_entry_keep(ScrDyn *ref); -ScrDyn *scr_perf_entry_ref(double i); -uint64_t scr_hrtime_ns(void); -ScrDyn *scr_process_hrtime_value(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); -/* process.memoryUsage(): take samples rss and the runtime heap once; - * field(k) reads that sample (k: 0 rss, 1 heapTotal, 2 heapUsed, - * 3 external, 4 arrayBuffers). heapTotal is the bytes the runtime heap - * holds from the system, heapUsed the bytes in live allocations, and - * external/arrayBuffers are 0 (byte storage lives in the same heap). - * memoryUsage.rss() is the resident set alone. An unreadable resident - * set throws Node's uv_resident_set_memory error. */ -void scr_process_memory_usage_take(void); -double scr_process_memory_usage_field(double k); -double scr_process_rss(void); -/* The runtime heap's footprint (scr_alloc.c): bytes held from the system - * and bytes in live allocations, for the calling thread's allocator plus - * the system allocator where it reports them. */ -void scr_heap_stats(double *total, double *used); -/* process.ppid: the parent's pid at the time of the read. */ -double scr_process_ppid(void); -/* process.title: libuv's process title — argv[0] until set; a set - * truncates to the original argument area (POSIX) and becomes the name - * ps shows, or the console title (Windows). "node" without arguments - * (library sessions). +1 / borrowed. */ -ScrStr *scr_process_title(void); -void scr_process_set_title(const ScrStr *title); -/* process.argv0: the original argv[0] the binary was started with. +1. */ -ScrStr *scr_process_argv0(void); -/* process.release: the stable { name, lts, sourceUrl, headersUrl[, - * libUrl] } object of the compatibility target's release. +1. */ -ScrDyn *scr_process_release(void); -/* geteuid(2) / getegid(2) / getgroups(2) (with the effective gid added - * when missing, like Node). Windows has no such members: the calls throw - * the TypeError Node's missing member does. */ -double scr_process_geteuid(void); -double scr_process_getegid(void); -ScrArr *scr_process_getgroups(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); - -/* Sequentially consistent integer operations and shared Int32 wait queues. */ -typedef struct ScrBytes ScrBytes; -ScrStr *scr_atomics_wait(ScrBytes *, double, double, double); -double scr_atomics_notify(ScrBytes *, double, double); -double scr_atomics_op(ScrBytes *, double, double, double, double); - -/* 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_fdatasync(double fd); -void scr_fs_ftruncate(double fd, double length); -void scr_fs_utimes(ScrStr *path, double atime, double mtime, bool nofollow); -void scr_fs_invalid_path(const ScrDyn *value, const ScrStr *path); -void scr_fs_invalid_named_path(const ScrDyn *value, const ScrStr *path, const char *name); -void scr_fs_futimes(double fd, double atime, double mtime); -void scr_fs_link(ScrStr *source, ScrStr *dest); -void scr_fs_symlink(ScrStr *target, ScrStr *destination, int kind); -int scr_fs_symlink_infer(ScrStr *absolute); -ScrBytes *scr_fs_readlink_bytes(ScrStr *path); -void scr_fs_link_checked(const ScrDyn *existing, const ScrDyn *destination); -void scr_fs_symlink_checked(const ScrDyn *target, const ScrDyn *destination, const ScrDyn *type); -void scr_fs_symlink_promise_checked(const ScrDyn *target, const ScrDyn *destination, const ScrDyn *type); -ScrDyn *scr_fs_readlink_checked(const ScrDyn *path, const ScrDyn *options, bool promise); -ScrDyn *scr_fs_readlink_dyn(const ScrDyn *path, const ScrDyn *options); -ScrDyn *scr_fs_statfs(ScrStr *path, bool bigint); -ScrDyn *scr_fs_statfs_checked(const ScrDyn *path, const ScrDyn *options); -ScrPromise *scr_fsp_statfs(const ScrDyn *path, const ScrDyn *options); -ScrStr *scr_fs_readlink_str(const ScrDyn *path, const ScrDyn *options); -ScrBytes *scr_fs_readlink_buffer(const ScrDyn *path, const ScrDyn *options); -ScrPromise *scr_fsp_link(const ScrDyn *existing, const ScrDyn *destination); -ScrPromise *scr_fsp_symlink(const ScrDyn *target, const ScrDyn *destination, const ScrDyn *type); -ScrPromise *scr_fsp_readlink_dyn(const ScrDyn *path, const ScrDyn *options); -ScrPromise *scr_fsp_readlink_str(const ScrDyn *path, const ScrDyn *options); -ScrPromise *scr_fsp_readlink_buffer(const ScrDyn *path, const ScrDyn *options); -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_utimes(const ScrDyn *path, const ScrDyn *atime, const ScrDyn *mtime); -ScrPromise *scr_fsp_lutimes(const ScrDyn *path, const ScrDyn *atime, const ScrDyn *mtime); -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_utimes_promise(ScrFileHandle *h, const ScrDyn *atime, const ScrDyn *mtime); -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::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: [\n]"), spawn - * failure throws "spawnSync ENOENT", a timeout SIGTERMs the child - * and throws "spawnSync 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: \n" with the - * unconditional newline, "spawn " 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 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); -/* Runtime options use the same spawn core; validation and explicit - * unsupported-option refusals throw before starting a child. Borrows. */ -ScrChild *scr_spawn_dynamic(ScrStr *cmd, ScrArr *args, const ScrDyn *options); -void scr_child_on_spawn(ScrChild *child, ScrClosure *cb); /* moves zero-argument callback */ -/* 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); -void scr_child_ref(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); -void scr_child_on_dyn(ScrChild *child, ScrStr *event, ScrDyn *listener); /* borrowed */ -/* Optional native-stream bridge. The table avoids a stream dependency - * in programs that only use the child-process surface. */ -typedef struct ScrChildStreamOps { - ScrStream *(*readable)(void); - ScrStream *(*writable)(ScrClosure *, ScrStreamChunkInv, ScrClosure *, ScrStreamPlainInv); - void *(*retain)(void *); - void (*release)(void *); - ScrTraceFn trace; - bool (*push)(ScrStream *, ScrBytes *); - bool (*end)(ScrStream *); - void (*write_done)(ScrStream *, ScrError *); - void (*final_done)(ScrStream *, ScrError *); -} ScrChildStreamOps; -extern const ScrChildStreamOps scr_stream_child_ops; -ScrDyn *scr_stream_dyn_view(ScrStream *stream); /* borrowed; +1 live method/property view */ -void scr_child_dyn_streams_install(const ScrChildStreamOps *ops, - ScrDyn *(*readable_box)(void *), ScrDyn *(*writable_box)(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); -ScrDyn *scr_crypto_native(void); -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); -/* The option and any-value forms of createHash, copy, update, digest and - * crypto.hash, with Node 24.22's validation and errors. */ -ScrCryptoHash *scr_crypto_hash_new_opts(ScrStr *alg, ScrDyn *options); -ScrCryptoHash *scr_crypto_hash_copy_opts(ScrCryptoHash *h, ScrDyn *options); -ScrCryptoHash *scr_crypto_hmac_new_dyn(ScrStr *alg, ScrDyn *key, ScrDyn *options); -ScrCryptoHash *scr_crypto_hash_update_enc(ScrCryptoHash *h, ScrStr *data, ScrStr *enc); -ScrCryptoHash *scr_crypto_hash_update_dyn(ScrCryptoHash *h, ScrDyn *data, ScrDyn *enc); -ScrDyn *scr_crypto_hash_digest_dyn(ScrCryptoHash *h, ScrDyn *enc); -ScrDyn *scr_crypto_hash_oneshot(ScrStr *alg, ScrDyn *input, ScrDyn *options); -bool scr_crypto_timing_safe_equal(ScrBytes *left, ScrBytes *right); -bool scr_crypto_timing_safe_equal_dyn(ScrDyn *left, ScrDyn *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); -double scr_crypto_random_int_max(double max); -ScrDyn *scr_crypto_random_fill_sync_dyn(ScrDyn *buf, ScrDyn *offset, ScrDyn *size); -double scr_crypto_random_fill_args(ScrBytes *view, bool typed_array, double offset, double size, - int form); -void scr_crypto_random_int_refill(void); -bool scr_crypto_random_int_check(double min, double max, bool min_given, double *range); -bool scr_crypto_random_int_try(double min, double range, double *out); -ScrStr *scr_crypto_random_uuid_opts(ScrDyn *options); -ScrStr *scr_crypto_random_uuid_v7(ScrDyn *options); -ScrBytes *scr_crypto_pbkdf2(ScrBytes *password, ScrBytes *salt, - double iterations, double keylen, ScrStr *digest); -ScrBytes *scr_crypto_pbkdf2_job(ScrBytes *password, ScrBytes *salt, double iterations, - double keylen, ScrStr *digest, const char **failure); -ScrBytes *scr_crypto_hkdf_bytes(ScrStr *digest, ScrBytes *ikm, ScrBytes *salt, - ScrBytes *info, double keylen); -ScrDyn *scr_crypto_hkdf(ScrStr *digest, ScrBytes *ikm, ScrBytes *salt, - ScrBytes *info, double keylen); -ScrBytes *scr_crypto_scrypt(ScrBytes *password, ScrBytes *salt, double keylen, ScrDyn *options); -ScrBytes *scr_crypto_scrypt_derive(ScrBytes *password, ScrBytes *salt, double keylen, - double n, double r, double p, double maxmem); -typedef void (*ScrCryptoBytesFn)(ScrClosure *cb, ScrError *error /* borrowed */, ScrBytes *value /* moves */); -void scr_crypto_defer_bytes(ScrBytes *value /* moves */, ScrError *error /* moves */, ScrClosure *cb /* moves */, - ScrCryptoBytesFn fn); -void scr_crypto_defer_dyn(ScrDyn *value /* moves */, ScrError *error /* moves */, ScrClosure *cb /* moves */, - ScrCryptoBytesFn fn); -/* randomFill and randomInt with callbacks (scr_crypto_async.c). `form` is - * randomFill's argument count before the callback. */ -void scr_crypto_random_fill_async(ScrBytes *buf, double offset, double size, double form, - ScrClosure *cb, ScrCryptoBytesFn fn); -void scr_crypto_random_fill_async_dyn(ScrDyn *buf, ScrDyn *offset, ScrDyn *size, double form, - ScrClosure *cb, ScrCryptoBytesFn fn); -typedef void (*ScrCryptoIntFn)(ScrClosure *cb, double value); -void scr_crypto_random_int_async(double min, double max, double min_given, ScrClosure *cb, - ScrCryptoIntFn 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); -void scr_crypto_hkdf_async(ScrStr *digest, ScrBytes *ikm, ScrBytes *salt, - ScrBytes *info, double keylen, ScrClosure *cb, ScrCryptoBytesFn fn); -void scr_crypto_scrypt_async(ScrBytes *password, ScrBytes *salt, double keylen, - ScrDyn *options, ScrClosure *cb, ScrCryptoBytesFn fn); - -/* The digest primitives beyond MD5/SHA-1/SHA-2 (scr_hash.c). Each final - * reads a copy of its context. */ -typedef struct ScrKeccakCtx { - uint64_t s[25]; - unsigned char buf[200]; - size_t rate; /* bytes absorbed per permutation */ - size_t len; /* buffered bytes */ - unsigned char pad; /* domain bits: 0x06 SHA-3, 0x01 Keccak, 0x1f SHAKE, 0x04 KECCAK-KMAC */ -} ScrKeccakCtx; -void scr_keccak_init(ScrKeccakCtx *ctx, size_t rate, unsigned char pad); -void scr_keccak_update(ScrKeccakCtx *ctx, const unsigned char *data, size_t len); -void scr_keccak_final(const ScrKeccakCtx *ctx, unsigned char *out, size_t len); -typedef struct ScrRipemd160Ctx { - uint32_t h[5]; - uint64_t bytes; - unsigned char buf[64]; - size_t len; -} ScrRipemd160Ctx; -void scr_ripemd160_init(ScrRipemd160Ctx *ctx); -void scr_ripemd160_update(ScrRipemd160Ctx *ctx, const unsigned char *data, size_t len); -void scr_ripemd160_final(const ScrRipemd160Ctx *ctx, unsigned char out[20]); -typedef struct ScrBlake2bCtx { - uint64_t h[8]; - uint64_t t[2]; - unsigned char buf[128]; - size_t len; -} ScrBlake2bCtx; -void scr_blake2b_init(ScrBlake2bCtx *ctx); -void scr_blake2b_update(ScrBlake2bCtx *ctx, const unsigned char *data, size_t len); -void scr_blake2b_final(const ScrBlake2bCtx *ctx, unsigned char out[64]); -typedef struct ScrBlake2sCtx { - uint32_t h[8]; - uint32_t t[2]; - unsigned char buf[64]; - size_t len; -} ScrBlake2sCtx; -void scr_blake2s_init(ScrBlake2sCtx *ctx); -void scr_blake2s_update(ScrBlake2sCtx *ctx, const unsigned char *data, size_t len); -void scr_blake2s_final(const ScrBlake2sCtx *ctx, unsigned char out[32]); -typedef struct ScrSm3Ctx { - uint32_t h[8]; - uint64_t bytes; - unsigned char buf[64]; - size_t len; -} ScrSm3Ctx; -void scr_sm3_init(ScrSm3Ctx *ctx); -void scr_sm3_update(ScrSm3Ctx *ctx, const unsigned char *data, size_t len); -void scr_sm3_final(const ScrSm3Ctx *ctx, unsigned char out[32]); - -size_t scr_crypto_digest_raw(const char *alg, const unsigned char *data, size_t len, - unsigned char out[64]); -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[64]); -/* 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 exactly - * (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); -/* path.parse / path.format for both families: parse answers a fresh - * string[] [root, dir, base, ext, name]; format takes the pathObject - * fields with undefined spelled "" (Node reads each through `||`). */ -ScrArr *scr_path_parse(ScrStr *path); -ScrArr *scr_path_win32_parse(ScrStr *path); -ScrStr *scr_path_format(ScrStr *root, ScrStr *dir, ScrStr *base, ScrStr *name, ScrStr *ext); -ScrStr *scr_path_win32_format(ScrStr *root, ScrStr *dir, ScrStr *base, ScrStr *name, - ScrStr *ext); -/* path.matchesGlob (scr_glob.c): Node 24's minimatch with matchGlobPattern's - * options, case-insensitive on macOS and Windows hosts like Node. Borrows - * both; a brace sequence over 2^32 - 1 elements throws Node's RangeError, a - * pattern over 65536 units its TypeError, and a segment regex invalid under - * the u flag its SyntaxError. scr_glob_match is the same with the family - * and case mode explicit (the oracle tests use it). */ -bool scr_path_matches_glob(ScrStr *path, ScrStr *pattern); -bool scr_path_win32_matches_glob(ScrStr *path, ScrStr *pattern); -bool scr_glob_match(ScrStr *path, ScrStr *pattern, bool windows, bool nocase); - -/* 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); -/* Engine callers supply the target OS's flags, rather than the frontend's - * portable constant mask. Argument validation happens before this seam. */ -double scr_fs_open_native(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); -ScrPromise *scr_file_handle_sync_promise(ScrFileHandle *h); -ScrPromise *scr_file_handle_datasync_promise(ScrFileHandle *h); -ScrPromise *scr_file_handle_truncate_promise(ScrFileHandle *h, double length); -ScrPromise *scr_file_handle_chmod_promise(ScrFileHandle *h, double mode); -double scr_file_handle_readv(ScrFileHandle *h, ScrArr *buffers, double position); -double scr_file_handle_writev(ScrFileHandle *h, ScrArr *buffers, double position); -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); -double scr_fs_readv_sync(double fd, ScrArr *buffers, double position); -double scr_fs_writev_sync(double fd, ScrArr *buffers, double position); -/* Unlike scalar readSync's zero-length fast path, a nonempty vector of - * empty views still tests the descriptor and positioned-read capability. */ -double scr_fs_read_zero(double fd, double position); -void scr_fs_close(double fd); - -/* ── WHATWG URL (scr_url.c) ────────────────────────────────────────── - * A mutable, refcounted URL value with selected component setters. 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 */ -bool scr_url_can_parse(ScrStr *input); -bool scr_url_can_parse_base(ScrStr *input, ScrStr *base); -ScrDyn *scr_url_parse(ScrStr *input); /* +1 native URL or null; invalid URLs do not throw */ -ScrDyn *scr_url_parse_base(ScrStr *input, ScrStr *base); -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() */ -void scr_url_set(ScrUrl *u, ScrStr *field, ScrStr *value); /* borrowed; href may throw */ -bool scr_url_set_component(ScrUrl *u, ScrStr *field, ScrStr *value); /* borrowed; false for ignored writes */ -void scr_url_set_checked(ScrUrl *u, ScrStr *field, const ScrDyn *value); /* borrowed; conversion may throw */ -/* node:url's legacy surface (scr_url_legacy.c — linked with the regex, - * querystring and dyn-async units). url.parse() answers a checked-dynamic - * Url instance; format() accepts a Url-shaped object, a string, or a - * native URL (with its options bag); resolve() takes two strings. `warn` - * emits DEP0169 once per process (false at node_modules call sites). - * domainToASCII/Unicode take the argument and whether it was omitted - * (ERR_MISSING_ARGS). All borrow; +1, or NULL with Node's error pending. */ -ScrDyn *scr_url_legacy_parse(const ScrDyn *url, const ScrDyn *parse_query, const ScrDyn *slashes, - bool warn); -ScrStr *scr_url_legacy_format(const ScrDyn *object, const ScrDyn *options, bool warn); -ScrStr *scr_url_legacy_resolve(const ScrDyn *from, const ScrDyn *to, bool warn); -ScrStr *scr_url_domain_to_ascii(const ScrDyn *domain, bool missing); -ScrStr *scr_url_domain_to_unicode(const ScrDyn *domain, bool missing); -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 exactly: '+' 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); -uint64_t scr_bigint_hash(const ScrBigInt *value); /* normalized value, no allocation */ -bool scr_bigint_eq_string(ScrBigInt *a, ScrStr *b); /* StringToBigInt failure is false */ -double scr_bigint_cmp_string(ScrBigInt *a, ScrStr *b); /* 2 = invalid StringToBigInt */ -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); -/* Small-value bigint trees the compiler evaluates in 64-bit registers: - * mag64 stores the low 64 bits of the magnitude and its length in 64-bit - * words, and returns the sign (-1, 0, 1); from_mag64 builds +-mag (+1). - * wrap_eval is the exact fallback over a compiler-built postfix program - * (leaves, and numbers for BigInt(number) operands); +1 or NULL with an - * exception pending. */ -int scr_bigint_mag64(const ScrBigInt *value, uint64_t *mag, uint32_t *words); -/* Whether a StringToBigInt text with this many significant digits in radix - * 2, 8, 10 or 16 can fit the maximum BigInt size (no size arithmetic that - * can wrap; the parser's guard before it allocates). */ -bool scr_bigint_digits_fit(size_t digits, unsigned radix); -ScrBigInt *scr_bigint_from_mag64(uint64_t mag, bool negative); -ScrBigInt *scr_bigint_wrap_eval(const char *program, ScrBigInt *const *leaves, const double *numbers); -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 */ -/* node:os host facts (scr_os.c), libuv's answers: uname machine/version, - * the host name, "LE"/"BE", free memory in bytes, uptime in seconds and - * the 1/5/15-minute load averages (+1 fresh results; never throw). */ -ScrStr *scr_os_machine(void); -ScrStr *scr_os_version(void); -ScrStr *scr_os_hostname(void); -ScrStr *scr_os_endianness(void); -double scr_os_freemem(void); -double scr_os_uptime(void); -ScrArr *scr_os_loadavg(void); -/* os.getPriority(pid): validateInt32, then the scheduling priority; - * failures throw Node's RangeError or SystemError (ERR_SYSTEM_ERROR). */ -double scr_os_get_priority(double pid); -/* os.cpus(): take answers the CPU count of a fresh snapshot; model and - * field (0 speed, 1 user, 2 nice, 3 sys, 4 idle, 5 irq) read it. */ -double scr_os_cpus_take(void); -ScrStr *scr_os_cpus_model(double i); -double scr_os_cpus_field(double i, double field); -/* os.constants: the per-thread null-prototype constants object (+1). */ -ScrDyn *scr_os_constants(void); -double scr_os_available_parallelism(void); /* uv_available_parallelism: CPUs this process may use, >= 1 */ -/* 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 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/, 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 throw the - * "not supported yet" Error instead of returning a wrong result. typeof - * answers "object", truthiness is true, String() renders - * "[object Object]" (Object.prototype.toString — Node's answer for - * these classes), JSON serialization and util.inspect throw the - * "not supported yet" Error. */ - 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 - * 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 throws - * the "not supported yet" Error. 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, - /* 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 = 12, /* tag 11 is retired; preserve the native ABI */ - /* 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 */ - SCR_DYN_SYMBOL, /* native symbol identity, 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, /* ScrEvent — new Event() and AbortSignal's abort events */ - 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, 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_MAP, /* native Map, shared backing */ - SCR_DYNH_DATE, /* JavaScript Date with stable native identity */ - SCR_DYNH_ITERATOR, /* live native array/string/byte iterator */ - SCR_DYNH_CONSOLE, /* stored global console value */ - SCR_DYNH_SEARCH_PARAMS, /* live WHATWG URLSearchParams */ - SCR_DYNH_FETCH_REQUEST, - SCR_DYNH_WORKER, - SCR_DYNH_MESSAGE_PORT, - SCR_DYNH_SHARED_ARRAY_BUFFER, - SCR_DYNH_CONSOLE_INSTANCE, /* node:console Console instance */ - SCR_DYNH_EVENT_TARGET, /* ScrEventTarget — new EventTarget() */ - SCR_DYNH_CUSTOM_EVENT, /* ScrEvent with detail — new CustomEvent() */ - SCR_DYNH_BLOB, /* ScrBlob — new Blob() and Blob#slice() */ - SCR_DYNH_FILE, /* ScrBlob with a name — new File() */ - 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 *); - bool traced; - struct ScrDynTypedCast *next; -} ScrDynTypedCast; - -/* 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 own shared immutable 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 : 1; - /* Snapshot written by trace before visiting children. Collected teardown - * must not inspect the value: an earlier cycle member may already be freed. - * Ordinary mutations need not maintain this collector-only decision. */ - bool value_traced : 1; - /* Ordinary object insertion sets all three attributes true. */ - bool writable; - bool enumerable; - bool configurable; -} ScrDynEntry; - -_Static_assert(sizeof(ScrDynEntry) == (sizeof(void *) == 8 ? 48 : 24), - "LLVM checked-object readers use the target entry stride"); -_Static_assert(offsetof(ScrDynEntry, value) == 2 * sizeof(void *), - "LLVM checked-object readers use the third pointer-sized field"); - -/* Element access for a live typed reference to a native ScrArr, emitted per - * array type by the compiler. The runtime decides presence and length from - * the native array itself; these hooks only convert one element. - * get: +1 dyn view of the element at a dense index whose state is - * SCR_ARR_VALUE, built exactly as the whole-array snapshot builds it. - * set: stores `value` at index (index <= length) when it fits the element - * type and returns true; returns false with no side effects when it - * does not, so the caller can take the snapshot path and report the - * mismatch with its usual diagnostic. */ -typedef struct ScrDynTypedArrayOps { - ScrDyn *(*get)(void *arr, double index); - bool (*set)(void *arr, double index, const ScrDyn *value); -} ScrDynTypedArrayOps; - -/* Field access for a live typed reference to a declared-field record, - * emitted per record shape by the compiler. `names` lists the fields in - * the order the record's snapshot defines them; the compiler emits these - * hooks only for shapes whose names are all ordinary (no array-index - * spellings, no `__proto__`), so that order is also JS own-key order. - * has: whether field `index` is an own property (an absent optional - * field is not); - * get: +1 dyn view of field `index`, built exactly as the snapshot - * builds it. Callers check `has` first. */ -typedef struct ScrDynTypedRecordOps { - size_t count; - ScrStr *const *names; /* immortal field-name strings */ - bool (*has)(void *rec, size_t index); - ScrDyn *(*get)(void *rec, size_t index); - /* Every field is required: `has` always answers true and no key of the - * record can be deleted. */ - bool fixed; - /* The field names as an immortal string[], built on first use by - * for-in over a record with every field present (atomic slot). */ - ScrArr *keys; -} ScrDynTypedRecordOps; - -/* Prototype members of a compiled class seen as `unknown`, emitted per - * class by the compiler: the string names of methods, getters and setters - * declared on the class and its bases, and `constructor`. The checked - * view of an instance holds only its own fields; `in` answers inherited - * members from this table. - * exact: the table of the instance's own class when it may be a - * subclass of the capsule's static class; NULL otherwise. */ -typedef struct ScrDynTypedClassOps { - const struct ScrDynTypedClassOps *(*exact)(void *instance); - size_t count; - ScrStr *const *names; /* immortal member-name strings */ - ScrStr *class_name; /* immortal constructor name; NULL for an anonymous class */ -} ScrDynTypedClassOps; - -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) */ - /* Callbacks keep the optional symbol unit out of symbol-free binaries. */ - struct { ScrSym *value; void (*release)(ScrSym *); ScrStr *(*render)(ScrSym *); ScrStr *(*description)(ScrSym *); } symbol; - struct { size_t len; size_t cap; ScrDyn **items; struct ScrDyn *properties; bool sealed; bool frozen; unsigned char *presence; } arr; /* owned; NULL presence means dense */ - 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); - struct ScrDynObjectIndex *index; /* optional wide-object lookup; owns no values */ - /* On a runtime-built class prototype (e.g. url.Url.prototype): the - * constructor name (static text) util.inspect prints for objects - * inheriting from it (scr_dyn_class_name). NULL otherwise. Only - * prototypes that live until their thread exits set it, and they - * clear it before release, so freelist reuse never sees it. */ - const char *class_name; - } 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 ownership edge is traced by the cycle collector. */ - 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; - ScrDyn *(*materialize)(void *); - void (*commit)(void *, const ScrDyn *); - /* Live native arrays: per-element access that bypasses the whole-array - * snapshot (scr_dyn_typed_ref_bind_array). Declared-field records - * keep their ScrDynTypedRecordOps here instead, flagged by `record` - * (scr_dyn_typed_ref_bind_record), and class instances their - * ScrDynTypedClassOps, flagged by `klass` - * (scr_dyn_typed_ref_bind_class). NULL for other referents. */ - const ScrDynTypedArrayOps *array; - /* 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; - /* One word keeps this, the largest payload, at its previous size. */ - uint32_t type_key_len : 28; - uint32_t traced : 1; /* native referent has a cycle header */ - uint32_t observed : 1; /* native release reports this identity's disposal */ - uint32_t record : 1; /* `array` holds ScrDynTypedRecordOps */ - uint32_t klass : 1; /* `array` holds ScrDynTypedClassOps */ - } 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, - * adapter promise otherwise). */ - ScrPromise *promise; - - 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; - /* Symbol properties have their own descriptor table. Its private string - * keys encode native identities; symbol_keys retains the actual keys in - * insertion order, never exposing those encodings to JavaScript. */ - struct ScrDyn *symbol_properties; - struct ScrDyn *symbol_keys; -}; -/* Compiled class dispatch reads a capsule's instance and key in place. */ -_Static_assert(offsetof(ScrDyn, v.typed_ref.ptr) == 16, "typed_ref.ptr offset"); -_Static_assert(offsetof(ScrDyn, v.typed_ref.type_key) == 16 + 3 * sizeof(void *), - "typed_ref.type_key offset"); - -static inline ScrDyn *scr_dyn_retain(ScrDyn *d) { - if (d->rc != SIZE_MAX) { - d->rc++; - } - return d; -} - -void scr_dyn_release(ScrDyn *d); /* releases the tree recursively; NULL-tolerant */ - -/* The runtime class an object inherits from (util.inspect's constructor - * name, found on its prototype chain), or NULL. */ -static inline const char *scr_dyn_class_name(const ScrDyn *d) { - for (const ScrDyn *p = d->prototype; p; p = p->prototype) - if (p->kind == SCR_DYN_OBJ && p->v.obj.class_name) return p->v.obj.class_name; - return NULL; -} - -/* The element hooks of a live array capsule, or NULL (including for record - * and class capsules, which keep their hooks in the same slot). */ -static inline const ScrDynTypedArrayOps *scr_dyn_typed_ref_array_ops(const ScrDyn *d) { - return d->v.typed_ref.record || d->v.typed_ref.klass ? NULL : d->v.typed_ref.array; -} - -/* 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); - -/* Compiler-emitted description of a statically known JSON.parse target - * (`JSON.parse(text) as T`). Constant data in the program image; the LLVM - * backend mirrors this layout ({size, size, ptr, ptr, ptr, ptr, ptr} and - * {ptr, size, size, ptr}). Records list their declared fields (at most - * 64), each with its byte offset in the record struct; bool fields are one - * byte, f64 fields doubles, every other kind a +1 pointer. */ -enum { - SCR_JSCHEMA_F64 = 0, - SCR_JSCHEMA_BOOL = 1, - SCR_JSCHEMA_STR = 2, - SCR_JSCHEMA_REC = 3, - SCR_JSCHEMA_ARR = 4, -}; -typedef struct ScrJsonSchema ScrJsonSchema; -typedef struct { - const char *name; - size_t name_len; - size_t offset; - const ScrJsonSchema *type; -} ScrJsonSchemaField; -struct ScrJsonSchema { - size_t kind; - size_t nfields; - const ScrJsonSchemaField *fields; /* records */ - const ScrJsonSchema *elem; /* arrays */ - void *(*rec_new)(void); /* records: zeroed instance, rc 1 */ - ScrArr *(*arr_new)(size_t); /* arrays: empty array of the element kind */ - void (*release)(void *); /* records and arrays */ -}; - -/* One-pass parse straight into the native layout. Returns the +1 record - * or array, or NULL WITHOUT throwing whenever the checked-dynamic route - * (scr_json_parse + dynCheck) might fail or differ; the caller then runs - * that route, which reports the exact error. */ -void *scr_json_parse_schema(const ScrStr *text, const ScrJsonSchema *schema); - -/* 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. An array replacer selects the object - * keys to serialize. 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); -ScrDyn *scr_json_stringify_value(const ScrDyn *value, const ScrDyn *replacer, const ScrDyn *space); - -/* 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); -/* for-in keys as a native string[] (+1); NULL with an exception pending. */ -ScrArr *scr_dyn_for_in_key_strs(const ScrDyn *v); -bool scr_dyn_for_in_stable(const ScrDyn *v); -/* Object.keys as a native string[] (+1); NULL with an exception pending. */ -ScrArr *scr_dyn_obj_key_strs(const ScrDyn *v); -/* Object.entries as owned (key, value) pairs: compiled code moves each - * pair into a native [string, any] row, then frees the snapshot with - * scr_dyn_entry_pairs_free. NULL with an exception pending. */ -typedef struct { - ScrStr *key; - ScrDyn *value; -} ScrDynEntryPair; -typedef struct { - size_t count; - ScrDynEntryPair pairs[]; -} ScrDynEntryPairs; -ScrDynEntryPairs *scr_dyn_obj_entry_pairs(const ScrDyn *v); -void scr_dyn_entry_pairs_free(ScrDynEntryPairs *pairs); -/* Object.values as a native array of owned checked values in own-key order - * (+1), for results compiled code only iterates, indexes or reads. NULL with - * an exception pending. */ -ScrArr *scr_dyn_obj_value_list(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_own_keys(const ScrDyn *value); -ScrDyn *scr_dyn_get_own_property_names(const ScrDyn *value); -ScrDyn *scr_dyn_get_own_property_symbols(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); -ScrDyn *scr_dyn_object_rest(const ScrDyn *source, const ScrDyn *excluded); -ScrDyn *scr_dyn_copy_property_descriptors(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); -ScrDyn *scr_dyn_native_map(ScrMap *value); /* borrowed backing, +1 box */ -bool scr_dyn_native_map_is(const ScrDyn *value); -void scr_regex_reset_last_index(ScrRegex *re, double index); -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_search_params(ScrSearchParams *value); -bool scr_dyn_native_search_params_is(const ScrDyn *value); -bool scr_dyn_native_date_is(const ScrDyn *value); -ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments); -double scr_dyn_native_date_value(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_symbol(ScrSym *value); /* borrows and retains value */ -ScrSym *scr_sym_well_known(ScrStr *name); -ScrDyn *scr_dyn_symbol_get(const ScrDyn *value, const ScrStr *key); -ScrDyn *scr_dyn_symbol_key_get(const ScrDyn *value, const ScrDyn *key, bool optional); -void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored); -void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key); -bool scr_dyn_has_key_computed(const ScrDyn *value, const ScrDyn *key); -bool scr_dyn_has_own_computed(const ScrDyn *value, const ScrDyn *key); -bool scr_dyn_property_is_enumerable_computed(const ScrDyn *value, const ScrDyn *key); -void scr_dyn_key_delete_computed(ScrDyn *value, const ScrDyn *key, bool strict); -ScrStr *scr_dyn_string_constructor(const ScrDyn *value); -ScrDyn *scr_dyn_property_key_value(const ScrDyn *value); -ScrDyn *scr_dyn_to_numeric(const ScrDyn *value); -ScrDyn *scr_dyn_increment(const ScrDyn *value, bool increment); -/* Internal constructor consumes a retained symbol with its native operations. */ -ScrDyn *scr_dyn_symbol_ref(ScrSym *value, void (*release)(ScrSym *), ScrStr *(*render)(ScrSym *), ScrStr *(*description)(ScrSym *)); -ScrDyn *scr_dyn_new_str(ScrStr *s); -ScrDyn *scr_dyn_new_arr(void); -ScrDyn *scr_dyn_array_constructor(void); -bool scr_dyn_arr_has_index(const ScrDyn *arr, size_t index); -void scr_dyn_arr_push_hole(ScrDyn *arr); -ScrDyn *scr_dyn_new_obj(void); -ScrDyn *scr_dyn_iterator(const ScrDyn *value, const ScrStr *spell); -ScrDyn *scr_dyn_iterator_optional(const ScrDyn *value); -void scr_dyn_install_iterator_symbol(ScrDyn *key); -void scr_dyn_install_async_iterator_symbol(ScrDyn *key); -ScrDyn *scr_dyn_array_values_function(void); -ScrDyn *scr_dyn_array_from_iterator(const ScrDyn *value); -ScrDyn *scr_dyn_iterator_result(ScrDyn *value); -/* Internal consumption of a captured builtin next method. Check can_step - * once before entering the loop. step returns an owned ordinary value; - * read step_done immediately afterward, before running any user code. */ -bool scr_dyn_iterator_can_step(const ScrDyn *iterator, const ScrDyn *next); -ScrDyn *scr_dyn_iterator_step(const ScrDyn *iterator); -bool scr_dyn_iterator_step_done(const ScrDyn *iterator); -/* 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); -bool scr_dyn_is_callable(const ScrDyn *value); -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_bytes_constructor(const ScrStr *name); -bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee); -bool scr_dyn_data_view_is(const ScrDyn *value); -ScrDyn *scr_dyn_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what); -ScrDyn *scr_bytes_construct(const ScrDyn *callee, const ScrDyn *args, const ScrStr *what); -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 *)); -ScrDyn *scr_dyn_new_typed_ref_observed( - 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 */ -/* What string conversion renders for a capsule (+1): its materialized - * view, or, for an EventTarget or Event subclass without its own - * toString, the "[object EventTarget]" / "[object Event]" string of its - * inherited @@toStringTag. */ -ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d); -void scr_dyn_typed_ref_commit(ScrDyn *d); -/* Attach per-element access to a live array capsule (idempotent). Indexed - * reads, `length`, in-range writes, `in`, own-property probes, and array - * iteration then touch one element instead of rebuilding the snapshot. */ -void scr_dyn_typed_ref_bind_array(ScrDyn *d, const ScrDynTypedArrayOps *ops); -/* Attach per-field access to a live record capsule (idempotent). While the - * capsule has no cached checked view (nothing has added properties or - * changed its prototype), keyed reads of declared fields, `in`/hasOwn, - * for-in, Object.keys/values/entries, typeof and Array.isArray answer from - * the native record, creating only the field values they return. */ -void scr_dyn_typed_ref_bind_record(ScrDyn *d, const ScrDynTypedRecordOps *ops); -/* Attach the prototype member names of a class instance capsule - * (idempotent), so `in` sees its methods and accessors. */ -void scr_dyn_typed_ref_bind_class(ScrDyn *d, const ScrDynTypedClassOps *ops); -/* The constructor name of a class instance capsule's own class, for - * util.inspect; NULL when the capsule is not a bound class instance, its - * class is anonymous, or its prototype was replaced. */ -const ScrStr *scr_dyn_typed_ref_class_name(const ScrDyn *d); -/* Fast keyed read on a live array capsule: +1 value for `length` and for - * present or explicitly-undefined elements; NULL when the answer needs the - * snapshot (holes, out-of-range, non-index names, or no bound ops). */ -ScrDyn *scr_dyn_typed_ref_key_get(const ScrDyn *d, const ScrStr *key); -/* +1 view of an own element of a bound live array capsule; NULL for a hole - * or an index at or past its length. */ -ScrDyn *scr_dyn_typed_ref_element(const ScrDyn *d, size_t index); -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 *), bool traced); -/* 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); -bool scr_dyn_typed_array_is(const ScrDyn *d, int elem); -bool scr_dyn_util_type_is(const ScrDyn *value, const ScrStr *probe); -ScrDyn *scr_dyn_util_type_value(const ScrDyn *value, const ScrStr *probe); -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 (worker - * executables keep the hook in scr_thread_hot). */ -#ifndef SCR_WORKERS -extern SCR_TL void (*scr_weak_dispose_hook)(void *); -#endif -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); -ScrDyn *scr_dyn_array_prototype(void); /* +1, installs native method adapters */ -ScrDyn *scr_dyn_array_prototype_base(void); /* +1, shared native array identity */ -/* 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); -/* The `in` operator: scr_dyn_has_key plus members inherited from builtin - * prototypes and compiled class prototypes. */ -bool scr_dyn_in(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 " 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); -ScrDyn *scr_dyn_object_prototype(void); -ScrDyn *scr_dyn_function_apply(void); -ScrDyn *scr_dyn_reflect_apply(ScrDyn *target, ScrDyn *receiver, ScrDyn *arguments_list); -ScrDyn *scr_dyn_apply_array_like(const ScrDyn *target, const ScrDyn *receiver, const ScrDyn *arguments, const char *what); -bool scr_dyn_same_value(const ScrDyn *left, const ScrDyn *right); -ScrDyn *scr_dyn_builtin_method(const ScrStr *prototype, const ScrStr *method); -ScrStr *scr_dyn_to_string_argument(const ScrDyn *d, const ScrDyn *argument, const ScrStr *what); -ScrStr *scr_num_to_string_radix(double value, const ScrDyn *radix); -/* 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); -ScrStr *scr_regex_replace_callback(ScrStr *subject, ScrRegex *regex, ScrDyn *callback, bool all); -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); -ScrBigInt *scr_dyn_bigint_constructor(const ScrDyn *d); -ScrBigInt *scr_dyn_bigint_coerce(const ScrDyn *d); -ScrDyn *scr_dyn_add(const ScrDyn *left, const ScrDyn *right); /* borrowed; +1 or NULL/pending */ -ScrDyn *scr_dyn_arithmetic(const ScrDyn *left, const ScrDyn *right, const ScrStr *operation); -bool scr_dyn_compare(const ScrDyn *left, const ScrDyn *right, const ScrStr *operation); -ScrDyn *scr_dyn_bitwise(const ScrDyn *left, const ScrDyn *right, const ScrStr *operation); - -/* `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); -void scr_error_register_dyn(const ScrVt *vt, ScrDyn *(*box)(void *)); -ScrDyn *scr_error_dyn_fields(const ScrError *e); -void scr_error_commit_dyn(ScrError *e, const ScrDyn *view); -/* 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 "not supported yet" Error; - * a name the kind's prototype lacks throws Node's " 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_prepare_method(ScrDyn *recv, const char *method); -ScrDyn *scr_dyn_invoke_prepared(ScrDyn *recv, ScrDyn *callee, const char *method, ScrDyn *const *args, size_t argc, const char *what); -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 "not supported yet" 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); -ScrDyn *scr_dyn_define_prototype_props(ScrDyn *target, ScrDyn *descs, ScrDyn *protected_keys); -/* 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_key(const ScrDyn *value, const ScrStr *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 - * " 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; -ScrSearchParams *scr_dyn_native_search_params_check(const ScrDyn *value, const ScrDynPath *path); -ScrSearchParams *scr_sp_new_checked(const ScrDyn *value); -ScrMap *scr_dyn_native_set_check(const ScrDyn *value, const ScrDynPath *path); -ScrMap *scr_dyn_native_map_check(const ScrDyn *value, const ScrDynPath *path); -bool scr_dyn_native_collection_is(const ScrDyn *value, int map, const char *type); -ScrMap *scr_dyn_native_collection_check(const ScrDyn *value, int map, const char *type, const ScrDynPath *path); -ScrStr *scr_url_checked_to_path(const ScrDyn *value); -ScrStr *scr_url_checked_to_path_options(const ScrDyn *value, const ScrDyn *options); -ScrBytes *scr_url_checked_to_path_buffer(const ScrDyn *value, const ScrDyn *options); -ScrUrl *scr_url_checked_from_path(const ScrDyn *value, const ScrDyn *options); -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 at , - * got " (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 "not supported yet" - * Error; names the class's surface never had throw Node's - * " 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 "not supported yet" Error 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 "not supported yet" Error — Node would take an - * expando, but a 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 throws the - * "not supported yet" Error). 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); - /* A live native iterator step; index advances only when a value is emitted. */ - ScrDyn *(*iter_step)(void *h, double *index, unsigned selection); - /* util.inspect renders ` ` for the dyn object this answers - * (+1); NULL = the class has no inspection, and util.inspect throws the - * "not supported yet" Error. */ - ScrDyn *(*inspect_view)(void *h); - /* structuredClone of the handle (+1 new box); NULL = DataCloneError. */ - ScrDyn *(*clone)(void *h); - /* JSON.stringify writes `{}` (the class keeps its state in internal - * slots behind prototype getters) instead of the unsupported fence. */ - bool json_empty_object; - /* Past the depth limit util.inspect prints `[cls]` (the class's - * inspector returns `this`) instead of the bare class name. */ - bool inspect_bracketed_past_depth; -} ScrDynHandleOps; -ScrDyn *scr_dyn_native_handle_iterator(ScrDyn *source, unsigned selection); - -/* ── EventTarget / Event / CustomEvent (scr_event_target.c) ─────────── - * Node's internal/event_target over checked-dynamic values. A target's - * listeners live in a ScrEventTargetCore: per event type, a linked list - * in registration order whose nodes keep their `next` link after removal, - * so a dispatch that cached a removed node continues where Node's does. - * Plain `new EventTarget()` values are SCR_DYNH_EVENT_TARGET handles; - * AbortSignal embeds a core of its own (MaxListeners 0, the `onabort` - * event-handler attribute). Events are ScrEvent handles (SCR_DYNH_EVENT, - * SCR_DYNH_CUSTOM_EVENT for CustomEvent). Listener exceptions, and - * rejections of a listener's returned promise or of a handleEvent call, - * are reported as uncaught exceptions from a nextTick, like Node. */ -typedef struct ScrEtRoot ScrEtRoot; -typedef struct ScrEtHandler ScrEtHandler; -typedef struct ScrEtHook ScrEtHook; -typedef struct ScrEventTargetCore { - ScrEtRoot *roots; /* per event type, in first-add order */ - ScrEtHandler *handlers; /* event-handler attributes (`onabort`) */ - ScrEtHook *hooks; /* options.signal removal hooks aimed here (weak) */ - /* A compiled subclass instance's own capsule (+1), kept from its first - * method call so every later `this` and event target is the same value. - * The instance holds the core, so this is a cycle the collector reclaims - * through the scr_event_target_state_* helpers. NULL otherwise. */ - ScrDyn *self; - double max_listeners; /* 0 = no leak warning (AbortSignal) */ - bool warned; - const char *cls; /* the warning's target name ("EventTarget") */ - bool tracked; /* in the exit teardown registry */ - struct ScrEventTargetCore *tracked_prev, *tracked_next; -} ScrEventTargetCore; - -typedef struct ScrEvent { - size_t rc; - ScrStr *type; - ScrDyn *target; /* +1, NULL until dispatched */ - ScrDyn *detail; /* CustomEvent's detail (+1); NULL for Event */ - double time_stamp; - bool custom, bubbles, cancelable, composed, trusted; - bool default_prevented, propagation_stopped, stop, dispatching, in_passive; - bool tracked; - struct ScrEvent *tracked_prev, *tracked_next; -} ScrEvent; - -void scr_et_core_init(ScrEventTargetCore *core, double max_listeners, const char *cls); -/* Drops every listener and handler (teardown and target destruction). */ -void scr_et_core_drop(ScrEventTargetCore *core); -/* The EventTarget methods over a core: `self` is the target's own dyn - * value (borrowed — `this` for listeners and the events' target), args - * borrowed. +1 result, or NULL with the exception pending. */ -ScrDyn *scr_et_add_listener(ScrEventTargetCore *core, ScrDyn *const *args, size_t argc); -ScrDyn *scr_et_remove_listener(ScrEventTargetCore *core, ScrDyn *const *args, size_t argc); -ScrDyn *scr_et_dispatch_event(ScrEventTargetCore *core, ScrDyn *self, ScrDyn *const *args, size_t argc); -/* Dispatches an event handle (borrowed) without the argument checks. */ -void scr_et_dispatch(ScrEventTargetCore *core, ScrDyn *self, ScrDyn *event); -/* `on` attributes (Node's defineEventHandler): get answers +1 (null - * when unset); set registers the wrapper listener on first assignment. */ -ScrDyn *scr_et_handler_get(ScrEventTargetCore *core, const char *type); -void scr_et_handler_set(ScrEventTargetCore *core, const char *type, const ScrDyn *value); -/* A trusted (isTrusted) Event handle of the given type, +1. */ -ScrDyn *scr_event_new_trusted(const char *type); -/* new EventTarget() / new Event(type, options) / new CustomEvent(type, - * options): `args` is the argument vector as one dyn array (borrowed). - * +1 handle, or NULL with the exception pending. */ -ScrDyn *scr_event_target_new(void); -ScrDyn *scr_event_new(ScrDyn *args); -ScrDyn *scr_custom_event_new(ScrDyn *args); -/* `v instanceof Event` (kind 0), `CustomEvent` (1), `EventTarget` (2). */ -bool scr_event_is(const ScrDyn *v, double kind); -/* The AbortSignal unit's view of its handles for options.signal: the - * signal's core and aborted flag, or NULL for a non-signal value. - * Installed by the AbortSignal unit; NULL when no signal can exist. */ -extern SCR_TL ScrEventTargetCore *(*scr_et_signal_core_fn)(const ScrDyn *d, bool *aborted); -/* The compiled `listener.handleEvent` read (dyn → dyn; the runtime keeps a - * reference), installed by %main when a program class declares - * handleEvent: dispatch reads a capsule listener's handleEvent through it, - * which reaches the class's compiled methods. Without it a capsule's own - * view answers. */ -void scr_event_handle_event_reader(ScrDyn *reader); -/* events.addAbortListener(signal, listener): +1 disposable, or NULL. */ -ScrDyn *scr_events_add_abort_listener(ScrDyn *signal, ScrDyn *listener); - -/* Compiled subclasses (`class Bus extends EventTarget`, `class Ping - * extends Event`): an instance lays out like a compiler-emitted hierarchy - * class (rc, vt, then this prefix, then the subclass fields — the - * ScrEmitter arrangement). super() creates the engine state behind the - * prefix pointer; `cls` is the concrete class's name, stamped by the - * emitted allocation (the leak warning and util.inspect print it). The - * instances' checked-dynamic capsules carry the RC adapters below, which - * is how the engine recognizes them: a target's capsule is the `this` of - * its listeners and the events' target, and an event's capsule is what - * its listeners receive. */ -typedef struct ScrEventTargetObj { - size_t rc; - const ScrVt *vt; - ScrEventTargetCore *core; /* NULL until super() */ - const char *cls; -} ScrEventTargetObj; - -typedef struct ScrEventObj { - size_t rc; - const ScrVt *vt; - ScrEvent *ev; /* NULL until super() */ - const char *cls; -} ScrEventObj; - -void *scr_event_target_obj_retain_v(void *o); -void scr_event_target_obj_release_v(void *o); /* dispatches through vt */ -void *scr_event_obj_retain_v(void *o); -void scr_event_obj_release_v(void *o); -void *scr_event_target_obj_retain(void *o); -void scr_event_target_obj_release(void *o); -void *scr_event_obj_retain(void *o); -void scr_event_obj_release(void *o); -/* super() / super(type, options): `args` is the argument vector as one - * dyn array (borrowed); a bad type or options value leaves Node's - * exception pending. */ -void scr_event_target_obj_init(void *obj); -void scr_event_obj_init(void *obj, ScrDyn *args); -void scr_event_obj_init_type(void *obj, ScrStr *type); /* super(type), type borrowed */ -/* The emitted teardown's and collector's prefix helpers (NULL-tolerant): - * trace visits the listener values, `on` handlers and the cached - * capsule of a target, and the target and detail of an event; gcfree - * frees the state without releasing those traced edges. */ -void scr_event_target_state_drop(ScrEventTargetCore *core); -void scr_event_target_state_trace(ScrEventTargetCore *core, ScrTraceVisit visit, void *ctx); -void scr_event_target_state_gcfree(ScrEventTargetCore *core); -void scr_event_state_drop(ScrEvent *ev); -void scr_event_state_trace(ScrEvent *ev, ScrTraceVisit visit, void *ctx); -void scr_event_state_gcfree(ScrEvent *ev); -/* The inherited members on typed receivers: `method`/`key` name an - * EventTarget or Event member, `args` is the argument vector as one dyn - * array, `self` the target's own capsule (borrowed). +1 results, or NULL - * with the exception pending. */ -ScrDyn *scr_event_target_obj_call(void *obj, ScrDyn *self, ScrStr *method, ScrDyn *args); -/* dispatchEvent(event): the bool result, false with the exception pending. */ -bool scr_event_target_obj_dispatch(void *obj, ScrDyn *self, ScrDyn *event); -ScrDyn *scr_event_obj_call(void *obj, ScrStr *method, ScrDyn *args); -ScrDyn *scr_event_obj_get(void *obj, ScrStr *key); -void scr_event_obj_set(void *obj, ScrStr *key, ScrDyn *value); - -/* The same members through a capsule (property reads and writes, method - * calls, util.inspect, string conversion), installed by the EventTarget - * unit with its first value and NULL before. get answers +1 or NULL (not - * an inherited member); set and invoke answer whether they handled the - * member. */ -typedef struct ScrEventRefHooks { - ScrDyn *(*get)(const ScrDyn *d, const ScrStr *key); - bool (*set)(ScrDyn *d, const ScrStr *key, const ScrDyn *value); - bool (*invoke)(ScrDyn *d, const char *method, ScrDyn *const *args, size_t argc, - const char *what, ScrDyn **out); - ScrDyn *(*inspect_view)(const ScrDyn *d, const char **cls); - /* Object.prototype.toString's answer with the inherited @@toStringTag - * ("[object EventTarget]", "[object Event]"), or NULL. */ - const char *(*string_tag)(const ScrDyn *d); -} ScrEventRefHooks; -extern SCR_TL const ScrEventRefHooks *scr_event_ref_hooks; - -/* ── Blob / File (scr_blob.c) ────────────────────────────────────────── - * Node 24's lib/internal/blob.js and file.js: immutable bytes copied from - * the sources at construction, shared by slices and structured clones. - * The constructors take the argument vector as one dyn array (borrowed) - * and validate it like Node; +1 handle, or NULL with the exception - * pending. slice takes its (start, end, contentType) vector the same way. - * The readers answer +1 promises (string, u8 bytes, and an ArrayBuffer - * dyn value) that settle on Node's microtask turn for the Blob's part - * count. scr_blob_is answers `v instanceof Blob` (kind 0) or `File` (1). */ -ScrDyn *scr_blob_new(ScrDyn *args); -ScrDyn *scr_file_new(ScrDyn *args); -ScrDyn *scr_blob_slice(ScrDyn *self, ScrDyn *args); -ScrPromise *scr_blob_text(ScrDyn *self); -ScrPromise *scr_blob_bytes(ScrDyn *self); -ScrPromise *scr_blob_array_buffer(ScrDyn *self); -bool scr_blob_is(const ScrDyn *value, double kind); - -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_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)); -ScrDyn *scr_console_native(void); -/* node:console Console instances: a native handle over the stdout/stderr - * targets — process stdio (fd 1/2) or a writable stream bound by - * scr_console_bind (scr_stream.c, which also installs the stream writer so - * console-only programs never link the stream unit) — and the - * ignoreErrors flag. new answers a +1 handle with both targets set to - * the given fds (0 = a stream is bound next); bind retains the stream. - * write prints `text\n` to the selected target: a stream receives ONE - * write, wrapped in Node's ignoreErrors handling (a noop 'error' listener - * around the call, a completion handler that swallows the write's own - * error, synchronous throws discarded); the global console handle writes - * the process streams like console.log/error. */ -typedef struct ScrConsoleInst { - size_t rc; - double fd[2]; /* 1/2 process stdio targets; 0 when stream[i] is bound */ - ScrStream *stream[2]; /* owned */ - ScrClosure *on_written[2]; /* owned: the per-stream completion handlers */ - bool ignore_errors; - void (*stream_write)(struct ScrConsoleInst *, int which, ScrStr *line); - void (*stream_drop)(struct ScrConsoleInst *); /* releases the bound streams */ -} ScrConsoleInst; -ScrDyn *scr_console_new(double out_fd, double err_fd, bool ignore_errors); -void scr_console_bind(ScrDyn *console, double which, ScrStream *stream); -void scr_console_inst_write(ScrDyn *console, ScrStr *text, bool to_stderr); -ScrDyn *scr_process_stdio(double fd); -bool scr_dyn_process_stdio_is(const ScrDyn *value); -double scr_dyn_process_stdio_fd(const ScrDyn *value, const ScrDynPath *path); - -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 throw "not supported yet" for it. +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 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 SCR_TL 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); -/* Promise.all / allSettled / any over a spread dyn array (borrowed); - * `reader` (borrowed) reads a program class instance's `then`. +1. */ -ScrPromise *scr_dyn_promise_all(const ScrDyn *entries, const ScrDyn *reader); -ScrPromise *scr_dyn_promise_all_settled(const ScrDyn *entries, const ScrDyn *reader); -ScrPromise *scr_dyn_promise_any(const ScrDyn *entries, const ScrDyn *reader); - -/* Native predicates for typed reference capsules and boxed errors. */ -bool scr_dyn_ref_typeof_is(const ScrDyn *d, const char *name); -bool scr_dyn_ref_is_array(const ScrDyn *d); -bool scr_dyn_ref_is_error(const ScrDyn *d); -bool scr_dyn_is_nullish(const ScrDyn *d); - -/* ── 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 without extra code. */ -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 */ -/* A class's typed-ref construction recipe, emitted once per class as a - * constant (kind: 0 plain, 1 observed, 2 traced capsule). `view` names the - * live view bound to the new capsule, as eager boxing binds it: 0 none, - * 1 array ops, 2 record ops, 3 class ops (`ops`). */ -typedef struct { - void *(*retain)(void *); - void (*release)(void *); - const char *type_key; - size_t type_key_len; - ScrDyn *(*materialize)(void *); - void (*commit)(void *, const ScrDyn *); - int kind; - int view; - const void *ops; -} ScrDynTypedRefRecipe; -/* A class instance as the window's receiver, boxed with its recipe only when - * read. ptr is BORROWED: the caller holds the instance across the window. */ -void scr_dyn_this_push_typed(void *ptr, const ScrDynTypedRefRecipe *recipe); -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); -ScrDyn *scr_gen_receiver(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 throws the "not - * supported yet" Error). */ -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. */ -SCR_COLD void scr_throw_arg_type(const ScrStr *argname, const ScrStr *expected, const ScrDyn *got); -SCR_COLD 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. */ -SCR_COLD void scr_throw_prop_type(const ScrStr *name, const ScrStr *expected, const ScrDyn *got); -SCR_COLD 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. */ -SCR_COLD 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); -ScrStr *scr_str_inspect_quoted(const ScrStr *s, bool truncate); -/* A ladder's post-validation refuse: throws the compiler-rendered SC2020 - * statement-fence text verbatim (Node's validation errors run first). */ -SCR_COLD 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; -/* Emitted JSON writers append constant text inline (llvm/walkers.ts - * appendBytes) through this prefix. */ -_Static_assert(offsetof(ScrJsonBuf, data) == 0 && offsetof(ScrJsonBuf, len) == sizeof(char *) && - offsetof(ScrJsonBuf, cap) == sizeof(char *) + sizeof(size_t), - "LLVM backend expects ScrJsonBuf to start with { data, len, cap }"); - -void scr_jb_init(ScrJsonBuf *b); -/* Append a borrowed byte span verbatim, including embedded NUL bytes. */ -void scr_jb_write(ScrJsonBuf *b, const char *s, size_t n); -/* scr_jb_write for text that holds no lone surrogate (fixed text, JSON - * tokens), which cannot complete a surrogate pair at the seam. */ -void scr_jb_write_utf8(ScrJsonBuf *b, const char *s, size_t n); -/* 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); -/* Append String(number), including NaN/Infinity, without a temporary string. */ -void scr_jb_put_number(ScrJsonBuf *b, double v); -/* Quoted + escaped JSON string: \" \\ \n \r \t \b \f, other control chars - * as \u00XX, lone surrogates as \udXXX, everything else (UTF-8 included) - * verbatim — exactly the JS JSON.stringify escape set. */ -void scr_jb_put_json_str(ScrJsonBuf *b, const ScrStr *s); - -/* 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 */ -/* Reformat compiler-produced compact JSON with an already resolved gap. - * Borrows both inputs; gap_len is a byte length, including embedded NUL. - * Returns a new owned string, or retains compact when the gap is empty. */ -ScrStr *scr_json_indent(const ScrStr *compact, const char *gap, size_t gap_len); - -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); -/* Emitted by the RC-audit lane's ProgramLifetime constructors, after the - * allocation note: such instances are never freed (the exit audit skips its - * leak report, and LeakSanitizer scans them as roots). */ -void scr_obj_lifetime_note(const void *obj); -#ifdef SCR_RC_AUDIT -long scr_obj_live_count(void); -long scr_obj_lifetime_count(void); -#else -/* Only the RC audit lane counts live objects. Other runtimes keep the - * out-of-line no-op symbols for direct-emission objects that still call - * them (scr_object.c), while runtime-internal call sites compile away. */ -#define scr_obj_alloc_note() ((void)0) -#define scr_obj_free_note() ((void)0) -#endif -#ifdef SCR_RC_AUDIT -/* A released lifetime region freed `n` program-lifetime instances. */ -void scr_obj_lifetime_forget(long n); -#endif - -/* ── counted lifetime regions (scr_region.c) ─────────────────────────── - * Programs that construct a LifetimeRegion (@scriptc/experimental) allocate - * every ProgramLifetime instance in the thread's CURRENT region instead of - * the small-object allocator. A region owns SCR_RGN_CHUNK-aligned chunks; - * each class packs its instances into segments of a chunk, so a region's - * objects are enumerable without a header. Instead of a per-object count, - * the class's retain and release adjust the chunk's first word (`count`); - * with the first reference each object starts with, it gives the number of - * references to the chunk's objects. - * - * release() condemns a region. A condemned region is freed only when a walk - * of its objects, and of the ordinary objects they own, explains every - * counted reference: trial deletion restricted to the region. References - * the walk cannot see (locals, globals, objects outside the walked graph, - * shapes it does not know) stay unexplained and keep the region. Freeing - * releases every reference field of every object, which tears the owned - * ordinary objects down through their normal paths, runs the cycle - * collector for the owned cycles, and then requires every count to be 0. - * Region 0 of each thread (the default) is never freed: programs that do - * not use regions keep ProgramLifetime's behavior. - * - * Only references to objects of regions that can be freed are counted: - * until the process creates its first region, scr_region_counting is 0 and - * retains and releases touch nothing, and chunks of the default regions - * have no owner, so references to their objects are never counted. Every - * reference to an object of a non-default region is created after that - * region exists, so its counts are exact. - * - * The layouts below are an ABI with the emitted code (llvm/regions.ts): - * the inline allocation fast path bumps the class's ScrRgnCursor, and the - * retain/release bodies test scr_region_counting, then the chunk's owner, - * then adjust its count. */ -#define SCR_RGN_CHUNK_SHIFT 21 -#define SCR_RGN_CHUNK ((size_t)1 << SCR_RGN_CHUNK_SHIFT) - -typedef struct ScrRgnWalk ScrRgnWalk; - -/* One per ProgramLifetime class, emitted as a mutable global: `slot` is - * assigned process-wide on the first allocation (-1 until then). */ -typedef struct ScrRgnClass { - size_t size; /* instance size (the stride rounds it to 8) */ - void (*visit)(void *obj, ScrRgnWalk *w); /* reports each reference field */ - void (*release_fields)(void *obj); /* releases each reference field */ - const char *name; - int32_t slot; -} ScrRgnClass; - -/* The calling thread's allocation cursor for one class in its current - * region: emitted per class (thread-local in worker executables). NULL - * fields send the next allocation to scr_region_alloc. */ -typedef struct ScrRgnCursor { - char *bump, *limit; -} ScrRgnCursor; - -/* The emitted type table (terminated by a NULL release): the release entry - * point a container stores for a type (shapes.ts vAdapters), and the walker - * of its objects. A NULL walker marks a ProgramLifetime class. */ -typedef struct ScrRgnType { - void (*release)(void *); - void (*walk)(void *obj, ScrRgnWalk *w); -} ScrRgnType; - -typedef struct ScrRgnChunk { - int64_t count; /* word 0: references to this chunk's objects, less one per object */ - void *owner; /* word 1: the owning thread; NULL in default regions */ - struct ScrRegion *region; - struct ScrRgnChunk *next; - char *bump; /* the next free segment byte */ - int64_t found; /* references the last walk saw into this chunk */ -} ScrRgnChunk; -_Static_assert(offsetof(ScrRgnChunk, count) == 0 && offsetof(ScrRgnChunk, owner) == sizeof(int64_t), - "llvm/regions.ts addresses the chunk count at 0 and the owner at 8"); - -static inline ScrRgnChunk *scr_rgn_chunk_of(const void *obj) { - return (ScrRgnChunk *)((uintptr_t)obj & ~(uintptr_t)(SCR_RGN_CHUNK - 1)); -} - -/* 1 once any thread has created a region (never reset); emitted retains - * and releases count nothing while it is 0. */ -extern _Atomic uint8_t scr_region_counting; - -/* The allocation slow path: a zeroed instance of `cls` in the current - * region. Refills `cursor`. */ -void *scr_region_alloc(ScrRgnCursor *cursor, ScrRgnClass *cls); -/* new LifetimeRegion(): a fresh region id (ids are never reused). `types` - * is the program's constant type table. Retries condemned regions. */ -double scr_region_create(const ScrRgnType *types); -/* run(fn): makes region `id` current until the matching leave. An id this - * thread does not own (another thread's, or a freed region's) selects the - * thread's default region. Calls nest. */ -void scr_region_enter(double id); -void scr_region_leave(void); -/* release(): condemns region `id` and tries to free it. */ -void scr_region_release(double id); - -/* Walk callbacks for emitted visits and walkers. scr_region_ref reports a - * reference to a ProgramLifetime instance; scr_region_slot reports a - * reference of the type whose container release entry point is `release` - * (unknown entry points are not walked, which keeps their references - * unexplained). Both tolerate NULL. */ -void scr_region_ref(ScrRgnWalk *w, void *obj); -void scr_region_slot(ScrRgnWalk *w, void *obj, void (*release)(void *)); - -/* ── 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); -void scr_promise_share_identity(ScrPromise *dst, ScrPromise *src); -bool scr_promise_identity_equal(ScrPromise *a, ScrPromise *b); -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); -/* `await f(...)` of a fiber-backed async f: spawn and await in one call, - * returning the awaited value (a rejection re-throws into the caller). */ -void scr_async_spawn_await_void(void (*entry)(ScrFiber *, void *), void *argpack); -double scr_async_spawn_await_f64(void (*entry)(ScrFiber *, void *), void *argpack); -bool scr_async_spawn_await_bool(void (*entry)(ScrFiber *, void *), void *argpack); -ScrStr *scr_async_spawn_await_str(void (*entry)(ScrFiber *, void *), void *argpack); -void *scr_async_spawn_await_ref(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); -/* Fiberless async call (native targets): the compiler proves an async body - * can never park its execution context and runs it directly on the - * caller's stack between enter and leave. The frame saves what a fresh - * fiber would isolate — the Error.stack frame chain and the - * AsyncLocalStorage context — and leave restores both, returning a fresh - * pending promise (+1) the caller settles from the body's outcome. */ -typedef struct ScrAsyncInline { - void *stack; - void *als; -} ScrAsyncInline; -void scr_async_inline_enter(ScrAsyncInline *frame); -ScrPromise *scr_async_inline_leave(ScrAsyncInline *frame); -/* Leaves the frame without creating the call's promise: `await f()` of a - * fiberless f consumes the result directly (the emitted await-call entry - * still takes the await's microtask turn). */ -void scr_async_inline_exit(ScrAsyncInline *frame); - -/* 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); -ScrDyn *scr_await_dyn_value_settled(ScrDyn *value); -void scr_promise_resolve_dyn(ScrPromise *destination, ScrDyn *value); -/* `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 SCR_TL ScrAlsCtx **scr_als_active; -#ifdef SCR_WORKERS -ScrAlsCtx **scr_als_slot(void); -#define SCR_ALS_SLOT() scr_als_slot() -#else -#define SCR_ALS_SLOT() scr_als_active -#endif -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 SCR_TL 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 SCR_TL 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). - * scr_process_emit_warning takes the ARGUMENT VECTOR as one dyn array, - * applies Node's full grammar/TypeErrors at the call and emits on the - * next tick, as Node does; scr_emit_warning is the C-side entry and - * reports synchronously. */ -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); -/* scr_emit_warning once a 'warning' listener exists (scr_lib.c; set by the - * listener registration): units that cannot link the dyn-async unit route - * their warnings through it, and print the default report otherwise. */ -extern SCR_TL void (*scr_warning_emit_hook)(const char *name, const char *code, ScrStr *message); -/* process.noDeprecation (which 0) and process.throwDeprecation (which 1): - * the stored value (+1; undefined until assigned), the assignment - * (borrowed), and the truthiness the warning paths test. */ -ScrDyn *scr_process_deprecation_flag(double which); -void scr_process_set_deprecation_flag(double which, ScrDyn *value); -bool scr_process_deprecation_flag_on(int which); -/* Node's emitWarning tail on the next tick (util.deprecate): a - * DeprecationWarning is dropped under noDeprecation and thrown from its - * own tick under throwDeprecation; otherwise the report and the 'warning' - * listeners run. scr_warning_new builds the string form's warning value - * (borrows; +1). */ -void scr_warning_emit_deferred(ScrDyn *warning); -ScrDyn *scr_warning_new(ScrStr *message, const char *type, const ScrStr *code); -/* 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); - -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_join: subscribe the result to an entry with a reaction job that - * runs one microtask turn after the entry settles (or after the join, for - * an entry already settled) — 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; the entry counts as HANDLED from the join. race_add is the same - * callback run inside the entry's own settlement (or now, if settled), - * for destinations that view the entry at another inner type. The - * payload accessors return retained/by-value fulfillment payloads for - * the emitted adapters (losing entries keep their settlements). */ -void scr_promise_race_join(ScrPromise *race, ScrPromise *in, - void (*adapt)(ScrPromise *dst, ScrPromise *src)); -void scr_promise_race_add(ScrPromise *race, ScrPromise *in, - void (*adapt)(ScrPromise *dst, ScrPromise *src)); - -/* ── Promise.all (compiler-emitted combinator plumbing) ─────────────── - * BORROWS the Promise[] entries array and the pre-capacity (empty, - * cap >= ps->len) values array — NULL values/store for Promise - * entries, whose result fulfills void — retains what it keeps, and - * returns the result promise +1. Node-exact: each entry settles the - * result through a reaction job one microtask turn after it settles, - * values land at their INPUT index regardless of settlement order, the - * first rejection's job wins (every entry counts as handled), 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)); - -/* ── Promise.allSettled / Promise.any ───────────────────────────────── - * The same countdown and per-entry reaction jobs as Promise.all, with - * the mode deciding what an entry's job does. allSettled stores EVERY - * settlement through `store` (the emitted or dyn settled-record builder) - * and fulfills with the array once every entry has settled. any settles - * the result from the first fulfillment (same inner type — a copy), - * stores each rejection reason into `errors` at its input index, and - * once every entry has rejected calls `fail(result, errors)`, which - * rejects the result (the AggregateError). The empty array fulfills / - * fails immediately. Both BORROW their arrays and return +1. - * - * The builder underneath (ScrCombine) takes entries one at a time, so a - * caller that runs other work between entries — the dyn path's - * thenable jobs — keeps Node's queue order: settled entries batch into - * one job until scr_promise_combine_flush closes the batch. */ -typedef void (*ScrAllStore)(ScrArr *a, double i, ScrPromise *src); -typedef void (*ScrAnyFail)(ScrPromise *result, ScrArr *errors); -enum { SCR_COMBINE_ALL = 0, SCR_COMBINE_ALL_SETTLED = 1, SCR_COMBINE_ANY = 2 }; -typedef struct ScrCombine { - struct ScrAllState *st; - ScrPromise *result; - size_t next; /* input index of the next entry */ - size_t batched; /* settled entries counted in the open batch */ - ScrPromise *decided; /* the open batch's first deciding entry (+1) */ - bool open; /* the open batch holds at least one entry */ -} ScrCombine; -void scr_promise_combine_begin(ScrCombine *c, int mode, size_t n, ScrArr *values, - ScrAllStore store, ScrAnyFail fail); -void scr_promise_combine_add(ScrCombine *c, ScrPromise *in); /* borrowed */ -void scr_promise_combine_flush(ScrCombine *c); -ScrPromise *scr_promise_combine_end(ScrCombine *c); /* +1 */ -ScrPromise *scr_promise_all_settled(ScrArr *ps, ScrArr *values, ScrAllStore store); -ScrPromise *scr_promise_any(ScrArr *ps, ScrArr *errors, ScrAllStore store, ScrAnyFail fail); -/* The typed Promise.any entry (scr_async_dyn.c): reasons collect as dyn - * values and exhaustion rejects with an AggregateError. */ -ScrPromise *scr_promise_any_typed(ScrArr *ps); -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_is_rejected(const ScrPromise *p); -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); - -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 and node:readline/promises (scr_readline.c, linked - * under the events gate) ───────────────────────────────────────────── - * The non-terminal Interface (the file header has the model). Interfaces - * are f64 handles. A stream input or output goes through ScrRlIo, which - * scr_stream.c provides (scr_stream_rl_open), so stdin-only programs - * never link the stream unit; the stream side feeds input through - * scr_rl_input_*. Signal-taking entry points get addAbortListener as - * `add` (scr_events_add_abort_listener), passed only by programs that - * link the event-target unit. Listener event numbers: 0 'line', - * 1 'close', 2 'pause', 3 'resume', 4 'error'. */ -typedef struct ScrRlIo { - void (*pause)(void *s); - void (*resume)(void *s); - void (*detach)(void *s, double id); /* removes the interface's input listeners */ - void (*write)(void *s, ScrStr *text); /* borrows text */ - void (*release)(void *s); -} ScrRlIo; -double scr_rl_open(const ScrRlIo *io, void *in /*+1 or NULL: stdin*/, void *out /*+1 or NULL*/, - bool out_stdout, double crlf_delay, ScrStr *prompt, ScrDyn *signal, - ScrAbortAddFn add); -void scr_rl_input_bytes(double id, ScrBytes *chunk); -void scr_rl_input_str(double id, ScrStr *chunk); -void scr_rl_input_end(double id); -void scr_rl_input_error(double id, ScrError *err); -/* question: throws ERR_USE_AFTER_CLOSE on a closed interface (may-throw); - * fn(cb, +1 answer). The promise form never throws. */ -void scr_rl_question(double id, ScrStr *query, ScrClosure *cb /*moves*/, - void (*fn)(ScrClosure *, ScrStr *)); -ScrPromise *scr_rl_question_promise(double id, ScrStr *query, ScrDyn *signal, ScrAbortAddFn add); -void scr_rl_close(double id); -void scr_rl_pause(double id); /* may throw ERR_USE_AFTER_CLOSE */ -void scr_rl_resume(double id); /* may throw ERR_USE_AFTER_CLOSE */ -void scr_rl_prompt(double id); /* may throw ERR_USE_AFTER_CLOSE */ -void scr_rl_set_prompt(double id, ScrStr *prompt); -ScrStr *scr_rl_get_prompt(double id); /* +1 */ -void scr_rl_write(double id, ScrStr *data); /* may throw ERR_USE_AFTER_CLOSE */ -void scr_rl_on(double id, double ev, ScrClosure *cb /*moves*/, void *fn, bool once); -void scr_rl_iter_open(double id); -ScrPromise *scr_rl_iter_next(double id); /* +1 promise: the line, or undefined */ -void scr_rl_iter_return(double id); -/* Listener adapters: line/answer (fn(cb, +1 text)) and error listeners. */ -void scr_rl_answer_thunk0(ScrClosure *cb, ScrStr *answer); -void scr_rl_answer_thunk_str(ScrClosure *cb, ScrStr *answer); -void scr_rl_error_thunk(ScrClosure *cb, ScrError *err); -void scr_rl_error_thunk0(ScrClosure *cb, ScrError *err); -void scr_rl_answer_thunk_dyn(ScrClosure *cb, ScrStr *answer); -void scr_rl_error_thunk_dyn(ScrClosure *cb, ScrError *err); -/* createInterface with a stream input and/or output (scr_stream.c): in is - * the input stream or NULL for process.stdin; out the output stream or - * NULL (out_stdout picks process.stdout). Both borrowed, like prompt - * (NULL: the default "> "). */ -double scr_stream_rl_open(ScrStream *in, ScrStream *out, bool out_stdout, double crlf_delay, - ScrStr *prompt, ScrDyn *signal, ScrAbortAddFn add); -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 SCR_TL void (*scr_process_exit_hook)(double code); -extern SCR_TL 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); -void scr_loop_commonjs_entry(void); -bool scr_report_unhandled_rejections(void); /* true = exit 1 */ -void scr_discard_unhandled_rejections(void); -long scr_abandoned_fiber_count(void); - - -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 *)); -/* A synchronous generator whose body the compiler emitted as an LLVM - * switched coroutine: no fiber stack; resumes call into the coroutine frame - * on the resumer's stack. The body registers its frame with - * scr_coro_started and ends in scr_gen_coro_finish (the trampoline - * epilogue of the fiber form). Native targets only. */ -ScrGen *scr_gen_new_stackless(void (*entry)(ScrFiber *, void *), void *argpack, - void (*drop_args)(void *)); -void scr_coro_started(void *handle); -/* The OUT stores of a coroutine body's yield (its suspension follows). */ -void scr_coro_yield_f64(ScrFiber *self, double v); -void scr_coro_yield_bool(ScrFiber *self, bool v); -void scr_coro_yield_ref(ScrFiber *self, void *v, void (*release)(void *)); /* moves */ -void scr_gen_coro_finish(ScrFiber *self); -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); -/* A valueless next() that answers done (the compiler's own drives). */ -bool scr_gen_step(ScrGen *g); -/* IteratorClose: a valueless return() whose result is dropped. */ -void scr_gen_close(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. */ -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 */ -ScrDyn *scr_gen_take_in_dyn(void); /* dyn channel: +1; NONE → undefined */ -/* yield* captures next/return/throw before ordinary yield unwinding. Ref - * converters borrow their typed payload and return an owned dyn value. */ -ScrDyn *scr_gen_delegate_resume(ScrDyn *(*next_ref)(void *), - ScrDyn *(*return_ref)(void *), - ScrDyn *(*caught_ref)(const ScrCaught *)); - -/* yield* over a typed generator delegate. start runs the first valueless - * next(); flag 1 marks a typed next channel, which a suspended delegate - * cannot take undefined into (the SC1071 fence), and flag 2 a completion - * read as a return type that cannot hold undefined. forward runs right - * after the delegating body's yield resumes and passes the consumer's - * next(v), return(v), or throw(e) to the delegate; next is its form for a - * resume with nothing pending. `modes` packs the next, return, and - * completion channel transfers (two bits each) and the typed-completion - * flag (64; 4 for next); the converters box a borrowed ref payload into - * an owned dyn. All answer whether the delegate is done. */ -bool scr_gen_delegate_start(ScrGen *d, unsigned flags); -bool scr_gen_delegate_next(ScrGen *d, unsigned flags, ScrDyn *(*to_dyn)(void *)); -bool scr_gen_delegate_forward(ScrGen *d, unsigned modes, ScrDyn *(*next_dyn)(void *), - ScrDyn *(*ret_dyn)(void *), ScrDyn *(*done_dyn)(void *)); - -/* 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 - -void scr_fetch_install(void); -ScrPromise *scr_fetch_input(ScrDyn *input, ScrDyn *init); -ScrDyn *scr_fetch_function(void); -void scr_global_fetch_install(ScrDyn *(*get)(void)); -ScrDyn *scr_fetch_request_new(ScrDyn *input, ScrDyn *init); -ScrDyn *scr_fetch_headers_new(ScrDyn *init); -ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init); /* +1 promise */ -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 */ -ScrPromise *scr_fetch_response_text(ScrDyn *response); /* +1 promise */ -ScrPromise *scr_fetch_response_array_buffer(ScrDyn *response); /* +1 promise */ -ScrPromise *scr_fetch_response_bytes(ScrDyn *response); /* +1 promise */ -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 */ - -/* ── 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); - -/* Clinger's exact fast path for a VALIDATED decimal span ([+-]? digits - * with optional fraction and exponent; no whitespace, no Infinity): sets - * *out and returns true when the value is m × 10^e with m <= 2^53 and a - * small |e| (one correctly rounded multiply/divide, bit-identical to - * strtod); false means the caller must take its strtod path. */ -bool scr_decimal_fast(const char *p, size_t n, double *out); -/* The same fast path with whole-span validation folded into its scan: -1 - * when p[0..n) is not such a decimal literal, 1 with *out set when exact, - * 0 when valid but the caller must use strtod. */ -int scr_decimal_scan(const char *p, size_t n, double *out); - -/* Number.prototype.toFixed's digit string for finite |x| < 1e21 and - * 0 <= f <= 100: the spec's n (closest to |x| × 10^f on the EXACT binary - * value, ties up) with the point placed f digits from the right and "-" - * for x < 0. Writes NUL-terminated text into buf (>= 128 bytes); returns - * the length. */ -size_t scr_f64_to_fixed(double x, int f, char *buf); - -/* 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, other surrogates stay lone. 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. - * ToUint32 and the bitwise operations live in scr_number.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 typed ELEMENT reads trap like arrays; invalid writes are - * ignored as in JavaScript. 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 ScrSharedBytes ScrSharedBytes; - -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 is_data_view; /* DataView is distinct from a numeric typed array. */ - bool external; /* Root aliases foreign memory; releasing it never frees data. */ - ScrSharedBytes *shared; /* Neutral backing, borrowed by views from their local owner. */ -} ScrBytes; - -/* Internal scoped locking is only for leaf memory operations. Never hold this - * guard across a script callback, blocking I/O, or a fiber suspension. */ -void scr_shared_enter(void); -void scr_shared_leave(void); -bool scr_shared_guard(const ScrBytes *, const ScrBytes *); -void scr_shared_guard_release(bool *); -#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) -#define SCR_SHARED_GUARD(a, b) bool scr_shared_held __attribute__((cleanup(scr_shared_guard_release))) = scr_shared_guard(a, b) -#else -#define SCR_SHARED_GUARD(a, b) ((void)0) -#endif -ScrSharedBytes *scr_shared_retain(ScrSharedBytes *); -void scr_shared_release(ScrSharedBytes *); -void scr_bytes_make_shared(ScrBytes *); -ScrBytes *scr_shared_wrap(ScrSharedBytes *); -void scr_bytes_read(const ScrBytes *, size_t, void *, size_t); -void scr_bytes_write(ScrBytes *, size_t, const void *, size_t); -ScrStr *scr_bytes_string(const ScrBytes *, size_t, size_t); -ScrBytes *scr_bytes_snapshot(const ScrBytes *); -void scr_bytes_snapshot_release(ScrBytes **); -ScrBytes *scr_bytes_local_copy(ScrBytes *); /* +1; shared inputs are copied */ -void scr_bytes_require_unshared(const ScrBytes *); -/* Snapshot before passing byte pointers to I/O or callback-capable consumers. - * The copy is local, so neither a lock nor shared raw memory escapes. */ -#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) -#define SCR_BYTES_SNAPSHOT(name) \ - ScrBytes *scr_snapshot_##name __attribute__((cleanup(scr_bytes_snapshot_release))) = scr_bytes_snapshot(name); \ - if (scr_snapshot_##name) name = scr_snapshot_##name -#else -#define SCR_BYTES_SNAPSHOT(name) ((void)0) -#endif -ScrDyn *scr_shared_array_buffer_new(ScrDyn *); -ScrDyn *scr_worker_data(void); -ScrDyn *scr_worker_parent_port(void); -ScrDyn *scr_worker_new(double root, ScrStr *filename, ScrDyn *options); -bool scr_shared_array_buffer_is(const ScrDyn *); -bool scr_buffer_storage_is(const ScrDyn *); -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); - -/* `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_constructor(void); -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 */ -ScrBytes *scr_bytes_take_data(uint8_t *data, size_t len); /* +1, consumes malloc storage */ - -/* Same-element-kind copy, preserving the raw representation. Cross-kind - * construction uses scr_bytes_convert. 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. Invalid typed reads trap; invalid writes are ignored. - * Checked reads preserve JavaScript's undefined result. Valid 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); -double scr_bytes_get_or_nan(const ScrBytes *b, double i); /* NaN when the index is invalid */ -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 */ -/* In-place numeric sort of a typed array: order 0 is the default - * TypedArray order (NaN last, -0 before +0); 1 or -1 is a recognized - * numeric comparator's, refused (false, nothing written) when a float - * element is NaN or -0. Runs no script code. */ -bool scr_bytes_sort_numeric(ScrBytes *b, int32_t order); -/* Comparator sort of a typed array with the comparison schedule Node - * uses; `f` is a borrowed closure of `arity` (0..2) number parameters. - * A comparator exception stays pending and nothing is written back. */ -void scr_bytes_sort_compare(ScrBytes *b, struct ScrClosure *f, uint32_t arity); -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 stays lone; 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); -double scr_text_decoder_encoding(const ScrDyn *label); -ScrStr *scr_text_decoder_name(double encoding); -ScrDyn *scr_text_encode_into(const ScrDyn *source, const ScrDyn *destination); -bool scr_buffer_is_ascii(const ScrDyn *input); -bool scr_buffer_is_utf8(const ScrDyn *input); -ScrBytes *scr_buffer_transcode(const ScrDyn *source, const ScrDyn *from, const ScrDyn *to); -ScrBytes *scr_bytes_buffer_source(const ScrDyn *value); -ScrStr *scr_text_decode_buffer_source(const ScrDyn *value); -ScrStr *scr_text_decode_options(const ScrBytes *b, bool fatal, bool ignore_bom); -ScrStr *scr_text_decode_stream(ScrDyn *state, const ScrBytes *b, double encoding, bool fatal, bool ignore_bom, bool stream); -ScrStr *scr_text_decode_legacy_options(const ScrBytes *b, double encoding, bool fatal, bool ignore_bom); - -/* 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); -ScrBytes *scr_bytes_from_str_checked(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); -double scr_buffer_byte_length_dyn(ScrDyn *value, 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 without an error; 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); -double scr_fs_timestamp_value(const ScrDyn *t, const char *name); -void scr_fs_utimes_checked(const ScrDyn *path, const ScrDyn *atime, const ScrDyn *mtime); -void scr_fs_lutimes_checked(const ScrDyn *path, const ScrDyn *atime, const ScrDyn *mtime); -void scr_fs_futimes_checked(const ScrDyn *fd, const ScrDyn *atime, const ScrDyn *mtime); -/* 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); /* , 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); -void scr_insp_begin(double recurse); -void scr_insp_entry(ScrStr *s, bool is_num); -/* Circular references, Node-exact ( / [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 " " - * 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); -/* A nested util.inspect inside a custom rendering (PerformanceEntry's): - * begin sets a fresh inspect context aside the caller's; end restores it - * and answers `prefix inner` with inner's newlines re-indented at the - * caller's level (+1; borrows both). */ -void scr_insp_nested_begin(void); -ScrStr *scr_insp_nested_end(ScrStr *prefix, ScrStr *inner); - -/* 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 nullish/empty encodings, a decoded string otherwise — or NULL - * with the exception pending. */ -ScrDyn *scr_fs_read_file_sync_dyn(ScrStr *path, const ScrDyn *enc); -ScrStr *scr_fs_read_file_encoded(ScrStr *path, ScrStr *encoding); -ScrStr *scr_fs_read_fd_encoded(double fd, ScrStr *encoding); -ScrDyn *scr_fs_callback_value(ScrStr *member); -ScrDyn *scr_fs_callback_call(ScrStr *member, const ScrDyn *args); -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); - -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); - -/* ── 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 throws the "not - * supported yet" Error). */ -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 throw the "not - * supported yet" Error; - * 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); -/* Several protocol-layer slices in order as one write: a single vectored - * syscall when the bytes can go out immediately, else the same buffering - * as consecutive scr_net_sock_write_native calls. */ -typedef struct { - const char *data; - size_t len; -} ScrNetSlice; -void scr_net_sock_writev_native(ScrNetSocket *s, const ScrNetSlice *slices, 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 */ -ScrNetSocket *scr_net_connect_checked(double port, ScrStr *host /*borrowed, nullable*/, ScrClosure *cb /*moves, nullable*/); /* +1 */ -bool scr_net_host_chk(const ScrStr *host /*borrowed, nullable*/); -/* 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. */ - -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 tlsClientError - * default, which tears down without an 'error' event). `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 without an 'error' - * event, 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 without an 'error' event (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 and *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*/); -void scr_http_res_on_end_callback(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 an implicit scheme switch. 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, ) — 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); -/* No-error assertions share Node's unwanted-exception/rejection message. - * The frontend reads and coerces actual?.message, preserving getter throws. */ -void scr_assert_unwanted_error(ScrStr *actual_message, bool rejection, ScrStr *msg, bool has_msg); -bool scr_assert_no_error_predicate(ScrDyn *predicate, ScrDyn *actual); -bool scr_assert_regex_dyn_test(ScrRegex *re, ScrDyn *actual); -/* 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): RegExp.test semantics, - * including global/sticky lastIndex transitions. */ -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 ──────────────────────────────────────────────────────────── */ - -enum { - SCR_ARG_F64, /* a number argument: inspect rendering (-0 prints "-0") */ - SCR_ARG_STR, /* a string, verbatim */ - SCR_ARG_BOOL, /* true / false */ - SCR_ARG_NUM, /* a number inside a string concatenation: String() rendering - * (-0 prints "0") — scr_console_*_parts only */ - SCR_ARG_KIND = 0xff, - SCR_ARG_GLUE = 0x100, /* flag: this part continues the previous argument - * (no separating space) — scr_console_*_parts only */ -}; - -typedef struct { - int tag; /* SCR_ARG_KIND bits, optionally | SCR_ARG_GLUE */ - 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); -/* The same lines where the compiler passed a string-concatenation argument - * (template literal, `"a" + n`) as its parts — glued with SCR_ARG_GLUE and - * numbers as SCR_ARG_NUM — so the joined string is never materialized. - * Separate entry points: programs using these tags cannot link against a - * runtime that does not understand them. */ -void scr_console_log_parts(size_t n, const ScrLogArg *args); -void scr_console_error_parts(size_t n, const ScrLogArg *args); -/* A Console instance's line on fd 1 or 2, without the global console's - * group indentation. */ -void scr_console_write_fd(int fd, size_t n, const ScrLogArg *args); -/* The global console's line rewrite while a group is open (scr_console.c; - * installed by scr_console_group): the indented heap copy of a rendered - * line, its length in *used. */ -extern SCR_TL char *(*scr_console_indenter)(const char *line, size_t *used); - -/* The global console's remaining members (scr_console_native.c). Arguments - * are borrowed; string results are +1. */ -ScrStr *scr_console_label(ScrDyn *label); -void scr_console_count(ScrStr *label); -ScrStr *scr_console_count_reset(ScrDyn *label); -ScrStr *scr_console_time(ScrStr *label); -ScrStr *scr_console_time_end(ScrStr *label); -ScrStr *scr_console_time_log(ScrStr *label); -void scr_console_group(double delta); -void scr_console_clear(void); -void scr_console_table(ScrDyn *data, ScrDyn *properties); -ScrStr *scr_console_dir_dyn(ScrDyn *value, double depth); -/* console.table's cell rendering (scr_inspect.c): Node's _inspect — depth - * 0 (-1 for objects with more than two keys), maxArrayLength 3, breakLength - * Infinity. +1. */ -ScrStr *scr_insp_table_cell(ScrDyn *value); -/* Node's getStringWidth over UTF-8 text (the non-ICU tables). */ -size_t scr_insp_display_width(const char *data, size_t len); -/* process.emitWarning(message) with the default Warning type, emitted on - * the next tick like Node's; an empty message emits nothing - * (scr_async_dyn.c). Borrows. */ -void scr_process_emit_warning_text(ScrStr *message); - -bool scr_fetch_web_is(const ScrDyn *value, const ScrStr *name); -bool scr_fetch_stream_is(const ScrDyn *value); +#include "scr_core.h" +#include "scr_rc.h" +#include "scr_string.h" +#include "scr_error.h" +#include "scr_string_methods.h" +#include "scr_qs.h" +#include "scr_array.h" +#include "scr_regex.h" +#include "scr_map.h" +#include "scr_closure.h" +#include "scr_ffi.h" +#include "scr_union.h" +#include "scr_events_emitter.h" +#include "scr_stream.h" +#include "scr_exception.h" +#include "scr_lib.h" +#include "scr_dc.h" +#include "scr_child.h" +#include "scr_crypto.h" +#include "scr_path.h" +#include "scr_file_handle.h" +#include "scr_url.h" +#include "scr_symbol.h" +#include "scr_bigint.h" +#include "scr_os.h" +#include "scr_dyn.h" +#include "scr_json.h" +#include "scr_event_target.h" +#include "scr_blob.h" +#include "scr_object.h" +#include "scr_region.h" +#include "scr_async.h" +#include "scr_events.h" +#include "scr_readline.h" +#include "scr_generator.h" +#include "scr_number.h" +#include "scr_bytes.h" +#include "scr_inspect.h" +#include "scr_zlib.h" +#include "scr_net.h" +#include "scr_tls.h" +#include "scr_http.h" +#include "scr_http2.h" +#include "scr_dgram.h" +#include "scr_watch.h" +#include "scr_test.h" +#include "scr_assert.h" +#include "scr_console.h" #endif /* SCR_RUNTIME_H */ diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h new file mode 100644 index 000000000..2dfe64ba2 --- /dev/null +++ b/packages/runtime/src/scr_stream.h @@ -0,0 +1,340 @@ +/* scriptc runtime — node:stream (scr_stream.c). Included by scr_runtime.h, + * which says where new declarations go. */ +#ifndef SCR_STREAM_H +#define SCR_STREAM_H + +#include "scr_events_emitter.h" + +/* ── 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) */ +/* events.addAbortListener (scr_events_add_abort_listener): passed to the + * signal-taking stream and readline entry points by programs that link + * the event-target unit. */ +typedef ScrDyn *(*ScrAbortAddFn)(ScrDyn *signal, ScrDyn *listener); + +typedef struct ScrStream { + size_t rc; + const ScrVt *vt; + ScrEeReg *reg; /* ScrEmitter prefix */ + const char *cls; /* ScrEmitter prefix (display name) */ + ScrStreamState *st; +} ScrStream; + +extern SCR_TL 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 "not supported yet" 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); +ScrStream *scr_stream_on_dyn(ScrStream *s, ScrStr *name, ScrDyn *cb, bool once, bool prepend); +ScrDyn *scr_stream_read_dyn(ScrStream *s, ScrDyn *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, arrayBuffer + * a +1 ArrayBuffer dyn over them. 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); +ScrPromise *scr_sc_array_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); +/* stream.addAbortSignal(signal, s) and the constructors' signal option + * (from_option: a falsy signal is ignored). Borrows; answers s +1, or + * NULL with ERR_INVALID_ARG_TYPE pending. */ +ScrStream *scr_stream_add_abort_signal(ScrStream *s, ScrDyn *signal, bool from_option, + ScrAbortAddFn add); +/* finished(s, options[, cb]) — Node's eos with its listeners and state + * predicates: readable/writable are -1 (the default), 0 or 1; on_error is + * options.error !== false; signal is NULL or borrowed (ERR_INVALID_ARG_TYPE + * pending and NULL answered for a non-signal). The callback forms answer + * the +1 cleanup closure (cb moves; the typed form takes its invoke + * adapter); the promise form rejects instead of throwing. */ +ScrClosure *scr_stream_finished_opts(ScrStream *s, ScrClosure *cb, ScrStreamErrInv inv, + double readable, double writable, bool on_error, + ScrDyn *signal, ScrAbortAddFn add); +ScrClosure *scr_stream_finished_opts_dyn(ScrStream *s, ScrDyn *cb, double readable, + double writable, bool on_error, ScrDyn *signal, + ScrAbortAddFn add); +ScrPromise *scr_sp_finished_opts(ScrStream *s, double readable, double writable, bool on_error, + bool cleanup, ScrDyn *signal, ScrAbortAddFn add); +/* stream/promises pipeline(...streams, { signal, end }): end false leaves + * the destination open; a non-signal value rejects with + * ERR_INVALID_ARG_TYPE. Streams borrowed. */ +ScrPromise *scr_sp_pipeline_opts(double n, ScrStream **streams, bool end, ScrDyn *signal, + ScrAbortAddFn add); +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); + +#endif /* SCR_STREAM_H */ diff --git a/packages/runtime/src/scr_string.h b/packages/runtime/src/scr_string.h new file mode 100644 index 000000000..9212effaf --- /dev/null +++ b/packages/runtime/src/scr_string.h @@ -0,0 +1,264 @@ +/* scriptc runtime — strings: layout, lone surrogates (WTF-8), concatenation + * and comparison (scr_string.c). Included by scr_runtime.h, which says where + * new declarations go. */ +#ifndef SCR_STRING_H +#define SCR_STRING_H + +#include "scr_rc.h" + +/* ── 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. Builders and concat results may have spare capacity 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. + * + * On 64-bit little-endian targets the capacity word is split: the low half + * is cap (strings never exceed SCR_STR_MAX_CAP bytes; JavaScript strings are + * far shorter) and the high half caches the string's Map key hash (0 = not + * computed). Emitted literals and runtime statics still initialize one + * size-sized word to len, which leaves the hash half zero. Only uniquely + * owned heap strings are ever mutated, and every such path clears the hash + * (scr_str_hash_forget); immortal strings are never written, so they always + * hash on demand. + * + * Bit 31 of the capacity word (on every target) is the proven-ASCII fact: + * when set, every byte is below 0x80, so UTF-16 indices equal byte offsets + * and .length == len. A clear bit means "not known", never "non-ASCII": + * constructors set it only where it is cheap, and string reads that prove + * ASCII set it later. Capacity therefore stops at 2^31 - 1 bytes, which + * still holds every string V8 can represent (2^29 - 24 UTF-16 units, at + * most three UTF-8 bytes each). Generated code reads the bit for inline + * charCodeAt/length (llvm/strings.ts); emitted literals and runtime + * statics set it in their capacity word. scr_str_hash_forget clears it + * with the hash: every in-place mutation already forgets cached facts. + */ +#if SIZE_MAX > UINT32_MAX && defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +#define SCR_STR_HASH_CACHE 1 +#else +#define SCR_STR_HASH_CACHE 0 +#endif +#define SCR_STR_MAX_CAP ((size_t)0x7fffffff) +typedef struct ScrStr { + size_t rc; + size_t len; +#if SCR_STR_HASH_CACHE + uint32_t cap : 31; + uint32_t ascii : 1; + uint32_t hash; +#else + size_t cap : 31; + size_t ascii : 1; +#endif + char data[]; +} ScrStr; + +/* The capacity word of a static string: cap == len, the proven-ASCII bit, + * plus the precomputed 32-bit key hash where the header caches one (0 + * leaves it to be computed). */ +#define SCR_STR_ASCII_BIT ((size_t)1 << 31) +#if SCR_STR_HASH_CACHE +#define SCR_STR_CAP_WORD(len, hash32, ascii) \ + ((size_t)(len) | ((ascii) ? SCR_STR_ASCII_BIT : 0) | \ + ((size_t)(uint32_t)(hash32) << 32)) +#else +#define SCR_STR_CAP_WORD(len, hash32, ascii) ((size_t)(len) | ((ascii) ? SCR_STR_ASCII_BIT : 0)) +#endif + +void scr_str_hash_prime(ScrStr *s); /* scr_map.c; before a string becomes immortal */ +/* Forget every cached content fact (key hash, proven ASCII) before a + * uniquely owned string's bytes change. */ +static inline void scr_str_hash_forget(ScrStr *s) { +#if SCR_STR_HASH_CACHE + s->hash = 0; +#endif + s->ascii = 0; +} + +/* True only when every byte is proven below 0x80 (see above). */ +static inline bool scr_str_known_ascii(const ScrStr *s) { return s->ascii; } + +/* Record a proven-ASCII fact. Immortal strings may live in read-only + * memory and are shared across threads, so they are never written. */ +static inline void scr_str_note_ascii(ScrStr *s) { + if (s->rc != SIZE_MAX) s->ascii = 1; +} + +/* memcpy for the short, variable-length copies of string construction + * (identifiers, keys, number text): fixed-size overlapping moves instead of + * a libc call. Longer spans keep the platform's bulk copy. */ +static inline void scr_copy_bytes(char *dst, const char *src, size_t n) { + if (n <= 16) { + if (n >= 8) { + uint64_t a, b; + memcpy(&a, src, 8); + memcpy(&b, src + n - 8, 8); + memcpy(dst, &a, 8); + memcpy(dst + n - 8, &b, 8); + } else if (n >= 4) { + uint32_t a, b; + memcpy(&a, src, 4); + memcpy(&b, src + n - 4, 4); + memcpy(dst, &a, 4); + memcpy(dst + n - 4, &b, 4); + } else if (n) { + dst[0] = src[0]; + dst[n / 2] = src[n / 2]; + dst[n - 1] = src[n - 1]; + } + return; + } + memcpy(dst, src, n); +} + +/* Whether a byte span is all ASCII: 32 bytes per step, stopping at the + * first chunk with a high bit. */ +static inline bool scr_bytes_all_ascii(const char *p, size_t n) { + const uint64_t high = UINT64_C(0x8080808080808080); + size_t i = 0; + for (; n - i >= 32; i += 32) { + uint64_t w[4]; + memcpy(w, p + i, sizeof w); + if ((w[0] | w[1] | w[2] | w[3]) & high) return false; + } + uint64_t acc = 0; + for (; n - i >= 8; i += 8) { + uint64_t w; + memcpy(&w, p + i, 8); + acc |= w; + } + for (; i < n; i++) acc |= (unsigned char)p[i]; + return (acc & high) == 0; +} + +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: 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) */ + +/* ── lone surrogates: canonical WTF-8 storage ───────────────────────── + * String storage is UTF-8 extended to lone UTF-16 surrogates: an unpaired + * surrogate is its generalized 3-byte encoding ED A0..BF 80..BF, while a + * high surrogate directly followed by a low one is always stored as the + * pair's 4-byte UTF-8 sequence. Canonical storage keeps byte equality, + * hashing and searching exact. Builders preserve it by fusing a trailing + * lone high surrogate with a leading lone low one (scr_wtf8_joins). A lone + * surrogate is exactly as long as U+FFFD, so byte sinks transcode to UTF-8 + * (scr_str_utf8) without changing any length or offset. */ + +/* Set once a lone surrogate may exist anywhere in the process: program + * startup stamps it when a literal holds one, and every runtime producer + * notes it (scr_wtf8_put, scr_wtf8_note). Until then no string can hold a + * lone surrogate, so builders and byte sinks skip all surrogate work. It is + * process-wide (strings cross threads) and never cleared. */ +extern bool scr_wtf8_lone; +static inline bool scr_wtf8_any(void) { return __atomic_load_n(&scr_wtf8_lone, __ATOMIC_RELAXED); } +void scr_wtf8_note(void); + +/* Whether p starts a lone surrogate (p[1] must be readable: string data + * is NUL-terminated). */ +static inline bool scr_wtf8_surrogate_at(const char *p) { + return (unsigned char)p[0] == 0xED && (unsigned char)p[1] >= 0xA0; +} + +/* Whether p starts a lone low surrogate (p[1] readable, as above). */ +static inline bool scr_wtf8_low_at(const char *p) { + return (unsigned char)p[0] == 0xED && (unsigned char)p[1] >= 0xB0; +} + +/* Whether appending b to a puts a lone high surrogate directly before a + * lone low surrogate. Tests the first byte of b before anything else, so + * ordinary text costs one compare. */ +static inline bool scr_wtf8_joins(const char *a, size_t alen, const char *b, size_t blen) { + return __builtin_expect(scr_wtf8_any() && (unsigned char)b[0] == 0xED && blen >= 3 && + (unsigned char)b[1] >= 0xB0 && alen >= 3 && + (unsigned char)a[alen - 3] == 0xED && + ((unsigned char)a[alen - 2] & 0xF0) == 0xA0, + 0); +} + +/* The first lone surrogate in a span, or NULL. memchr-driven. */ +const char *scr_wtf8_find_surrogate(const char *p, size_t n); +/* Fuse every adjacent lone high/low pair in place; returns the new length. */ +size_t scr_wtf8_fuse(char *p, size_t n); +/* Restore canonical storage after a builder joined a lone high surrogate + * to a lone low one: fuses in place and drops cached content facts. The + * _from form starts at a byte offset before which storage is canonical. */ +void scr_str_fuse_surrogates(ScrStr *s); +void scr_str_fuse_from(ScrStr *s, size_t from); +/* Replace every lone surrogate with U+FFFD in place (same length). */ +void scr_wtf8_replace_surrogates(char *p, size_t n); +static inline void scr_wtf8_to_utf8(char *p, size_t n) { + if (__builtin_expect(scr_wtf8_any(), 0)) scr_wtf8_replace_surrogates(p, n); +} +/* A string's bytes as well-formed UTF-8 for a byte sink: s->data itself, + * or (only when s holds a lone surrogate) a malloc'd NUL-terminated copy + * with each one replaced by U+FFFD, returned in *owned for the caller to + * release with scr_str_utf8_end. *owned is NULL on the fast path. */ +const char *scr_str_utf8_copy(const ScrStr *s, char **owned); +static inline const char *scr_str_utf8(const ScrStr *s, char **owned) { + *owned = NULL; + if (s->ascii || !scr_wtf8_any()) return s->data; + return scr_str_utf8_copy(s, owned); +} +static inline void scr_str_utf8_end(char *owned) { + if (owned) free(owned); +} +static inline bool scr_str_has_surrogate(const ScrStr *s) { + return scr_wtf8_any() && !s->ascii && scr_wtf8_find_surrogate(s->data, s->len) != NULL; +} +/* Encode one UTF-16 code unit or code point as WTF-8 (1-4 bytes). */ +size_t scr_wtf8_put(char *dst, uint32_t cp); + +/* Borrow both args, return +1. */ +ScrStr *scr_str_concat(ScrStr *a, ScrStr *b); +/* Borrow each part without mutation; return one independently owned result. */ +ScrStr *scr_str_concat_parts(ScrStr *const *parts, size_t count); +/* Concatenate up to 16 string or number operands (parts[i] == NULL selects + * nums[i], formatted like String(n)) onto an optional borrowed head, +1 + * result. The head may be appended in place under scr_str_concat's rule for + * its left operand; parts are never mutated. */ +ScrStr *scr_str_concat_mixed(ScrStr *head, ScrStr *const *parts, const double *nums, size_t count); +/* The same with the left operand / head MOVED in (`s = s + x` when nothing + * else can observe `s`): one call instead of a snapshot retain, the + * binding's release and the snapshot's release around a borrowing concat. */ +ScrStr *scr_str_concat_move(ScrStr *a, ScrStr *b); +ScrStr *scr_str_concat_mixed_move(ScrStr *head, ScrStr *const *parts, const double *nums, + size_t count); +/* a + String(x): scr_str_concat's contract with a number right operand. */ +ScrStr *scr_str_concat_f64(ScrStr *a, double x); + +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); +/* a.localeCompare(b) with no locales/options: Node's root-locale order for + * Latin-script text, code-point order outside it (scr_string.c). */ +double scr_str_locale_compare(ScrStr *a, ScrStr *b); + +/* ECMAScript string-list ordering: compare UTF-16 code units even though + * ScrStr stores WTF-8. Returns <0, 0, >0. */ +int scr_str_cmp_u16(ScrStr *a, ScrStr *b); + +#endif /* SCR_STRING_H */ diff --git a/packages/runtime/src/scr_string_methods.h b/packages/runtime/src/scr_string_methods.h new file mode 100644 index 000000000..035ad13b5 --- /dev/null +++ b/packages/runtime/src/scr_string_methods.h @@ -0,0 +1,255 @@ +/* scriptc runtime — the string methods, URI coding and number parsing + * (scr_string.c, scr_idna.c). Included by scr_runtime.h, which says where new + * declarations go. */ +#ifndef SCR_STRING_METHODS_H +#define SCR_STRING_METHODS_H + +#include "scr_error.h" + +/* ── 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. + * + * Where JS produces a *lone surrogate* — charAt on half of an astral + * pair, or a slice boundary that splits a pair — the result holds that + * surrogate (canonical WTF-8, see above). + */ + +/* .length — number of UTF-16 code units (astral chars count as 2). */ +double scr_str_utf16_len(ScrStr *s); + +/* The receiver of an indexed scan whose UTF-16 code units the index cache + * holds (scr_string.c), published so emitted length and charCodeAt reads of + * that string are a compare and a load. Cleared whenever its entry drops + * the units (release, append, eviction); all fields zero otherwise. Emitted + * code reads it only where SCR_TL is empty. */ +typedef struct ScrStrScan { + const ScrStr *s; + const uint16_t *units; + size_t n; +} ScrStrScan; +extern SCR_TL ScrStrScan scr_str_scan; + +/* 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 keep the half character as a lone surrogate. 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); + +/* Scalar-only local slices retain their original owner without copying. + * Bounds and split-surrogate replacements are snapshotted at construction. */ +typedef struct ScrStringSlice { + size_t start, length; + uint32_t split; +} ScrStringSlice; +void scr_str_slice_range(ScrStr *s, double start, double end, bool substring, + ScrStringSlice *out); +double scr_str_slice_char_code_at(ScrStr *s, const ScrStringSlice *range, double index); + +/* 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); +ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form); +/* NFC over a code point buffer (scr_regex.c, beside normalize): a fresh + * malloc'd buffer the caller frees, its length in *out_len. Borrows input. */ +uint32_t *scr_nfc_code_points(const uint32_t *input, size_t length, size_t *out_len); + +/* UTS #46 domain conversion (scr_idna.c — the url unit's IDNA tables; it + * rides the regex switch for NFC). to_ascii answers "" on failure; + * to_unicode decodes each valid Punycode label and never fails. Both + * borrow the UTF-8 span and return +1. */ +ScrStr *scr_idna_to_ascii(const char *p, size_t n); +ScrStr *scr_idna_to_unicode(const char *p, size_t n); + +/* charAt(i): 1-code-unit string; out of range → empty string. Half of an + * astral pair → that lone surrogate. 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 + * a lone surrogate); otherwise splits on every occurrence, + * empty pieces kept. Borrows both; returns a +1 string[] (SCR_ELEM_STR). */ +struct ScrArr; +/* String.prototype.replace/replaceAll with a string search value and a + * string replacement: GetSubstitution without captures ($$, $&, $`, $'; + * every other '$' spelling stays literal). An empty search value matches + * at 0, or at every UTF-16 position under replaceAll. Borrows; +1. */ +ScrStr *scr_str_replace_literal(ScrStr *s, ScrStr *search, ScrStr *rep, bool all); +/* Every occurrence of the ASCII byte chars[k] replaced by the k-th + * NUL-separated text of `texts`, in one pass (at most 16 distinct + * characters; the compiler fuses chains of global one-character + * replacements into it). Borrows all three; +1. */ +ScrStr *scr_str_replace_chars(ScrStr *s, ScrStr *chars, ScrStr *texts); +/* The same over a nonempty canonical WTF-8 needle (literal regex + * patterns share it). Borrows; +1. */ +ScrStr *scr_str_replace_bytes(ScrStr *s, const char *pat, size_t plen, ScrStr *rep, bool all); +/* First occurrence of a nonempty needle in a byte span, or NULL. */ +const char *scr_str_find_bytes(const char *hay, size_t hay_len, const char *needle, + size_t needle_len); +struct ScrArr *scr_str_split(ScrStr *s, ScrStr *sep); +struct ScrArr *scr_str_split_limit(ScrStr *s, ScrStr *sep, double limit); +/* Internal dense split traversal. The cursor borrows input strings, returns + * one owned piece per step, and uses NULL only for exhaustion. Its layout + * is shared with generated stack storage, including wasm32. */ +typedef struct ScrSplitCursor { + size_t offset; + uint32_t remaining; + uint32_t pending_unit; /* the low surrogate still owed by a split pair */ + char unit[4]; /* the bytes of the current surrogate piece */ +} ScrSplitCursor; +void scr_str_split_cursor_init(ScrSplitCursor *cursor, uint32_t limit); +/* Optional scratch is a separately owned, NULL-initialized string slot. + * A piece that has not escaped can reuse it after the preceding yield is + * released. The caller releases the slot on every exit; NULL disables reuse. */ +ScrStr *scr_str_split_cursor_next(ScrStr *s, ScrStr *sep, ScrSplitCursor *cursor, + ScrStr **scratch); +/* Borrowed split bytes remain valid while the source snapshot is owned. + * Materialization returns +1 and uses the same optional scratch contract. */ +const char *scr_str_split_cursor_span(ScrStr *s, ScrStr *sep, ScrSplitCursor *cursor, + size_t *length); +ScrStr *scr_str_split_materialize(const char *bytes, size_t length, ScrStr **scratch); + +/* 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 keeps its + * high half as a lone surrogate. 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(): whether s holds no lone surrogate, and + * s with each lone surrogate replaced by U+FFFD (s itself, retained, when + * it has none). Borrow s; toWellFormed +1. */ +bool scr_str_is_well_formed(ScrStr *s); +ScrStr *scr_str_to_well_formed(ScrStr *s); +/* Node text utilities: borrowed input, +1 result or NULL+pending. The VT + * helper lives in the optional regex unit; USV conversion uses native strings. */ +ScrStr *scr_util_strip_vt(const struct ScrDyn *input); +ScrStr *scr_util_to_usv_string(const struct ScrDyn *input); +ScrStr *scr_util_to_string(const struct ScrDyn *input); /* ToString alone */ +void scr_dyn_install_to_primitive_symbol(struct ScrDyn *key); + +/* 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), a lone surrogate \uNNNN-escapes, the rest + * passes through. Total. 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); +/* parseInt over a borrowed UTF-8 byte span. */ +double scr_parse_int_bytes(const char *p, size_t n, 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); +/* StringToNumber over a borrowed UTF-8 byte span. */ +double scr_bytes_to_number(const char *p, size_t len); + +/* 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). A lone surrogate throws the + * spec's URIError ("URI malformed"). Borrows s; result +1. */ +ScrStr *scr_str_encode_uri_component(ScrStr *s); +/* querystring.escape: the component encoder, throwing Node's URIError + * [ERR_INVALID_URI] on a lone surrogate. Borrows s; result +1. */ +ScrStr *scr_qs_escape(ScrStr *s); +/* lib/internal/querystring.js encodeStr over UTF-16 code units: ASCII + * outside `no_escape` and every non-ASCII code unit percent-encode as + * UTF-8, and ANY surrogate pairs with the code unit after it (Node's own + * rule); only a surrogate in last position throws URIError + * [ERR_INVALID_URI] and returns NULL. Borrows s; +1. */ +ScrStr *scr_qs_encode_str(const ScrStr *s, const signed char no_escape[128]); + +/* 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); +ScrStr *scr_str_decode_uri(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); + +#endif /* SCR_STRING_METHODS_H */ diff --git a/packages/runtime/src/scr_symbol.h b/packages/runtime/src/scr_symbol.h new file mode 100644 index 000000000..6897f4ceb --- /dev/null +++ b/packages/runtime/src/scr_symbol.h @@ -0,0 +1,31 @@ +/* scriptc runtime — ES Symbol values (scr_symbol.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_SYMBOL_H +#define SCR_SYMBOL_H + +#include "scr_string.h" + +/* ── 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) */ + +#endif /* SCR_SYMBOL_H */ diff --git a/packages/runtime/src/scr_test.h b/packages/runtime/src/scr_test.h new file mode 100644 index 000000000..667b96bb8 --- /dev/null +++ b/packages/runtime/src/scr_test.h @@ -0,0 +1,45 @@ +/* scriptc runtime — node:test (scr_test.c). Included by scr_runtime.h, which + * says where new declarations go. */ +#ifndef SCR_TEST_H +#define SCR_TEST_H + +#include "scr_bytes.h" + +/* ── 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 + +#endif /* SCR_TEST_H */ diff --git a/packages/runtime/src/scr_tls.h b/packages/runtime/src/scr_tls.h new file mode 100644 index 000000000..1882b4d82 --- /dev/null +++ b/packages/runtime/src/scr_tls.h @@ -0,0 +1,136 @@ +/* scriptc runtime — node:tls and node:https (scr_tls.c, scr_tls_ca.c). + * Included by scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_TLS_H +#define SCR_TLS_H + +#include "scr_net.h" + +/* ── 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 without an 'error' event (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 and *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); + +#endif /* SCR_TLS_H */ diff --git a/packages/runtime/src/scr_union.h b/packages/runtime/src/scr_union.h new file mode 100644 index 000000000..147bda131 --- /dev/null +++ b/packages/runtime/src/scr_union.h @@ -0,0 +1,91 @@ +/* scriptc runtime — unions and the void*-signature RC adapters (scr_union.c). + * Included by scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_UNION_H +#define SCR_UNION_H + +#include "scr_rc.h" + +/* ── 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; +/* The speed posture's inline union release (shapes.ts) reads arm_trace at + * this byte offset to skip buffering untraced boxes, like scr_union_release. */ +_Static_assert(offsetof(ScrUnion, arm_trace) == 2 * sizeof(size_t) + 2 * sizeof(void *), + "inline RC fast paths read ScrUnion.arm_trace at 32 (64-bit) / 16 (32-bit)"); + +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++; + } + 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); + +#endif /* SCR_UNION_H */ diff --git a/packages/runtime/src/scr_url.h b/packages/runtime/src/scr_url.h new file mode 100644 index 000000000..134893054 --- /dev/null +++ b/packages/runtime/src/scr_url.h @@ -0,0 +1,112 @@ +/* scriptc runtime — WHATWG URL and URLSearchParams (scr_url.c). Included by + * scr_runtime.h, which says where new declarations go. */ +#ifndef SCR_URL_H +#define SCR_URL_H + +#include "scr_exception.h" + +/* ── WHATWG URL (scr_url.c) ────────────────────────────────────────── + * A mutable, refcounted URL value with selected component setters. 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 */ +bool scr_url_can_parse(ScrStr *input); +bool scr_url_can_parse_base(ScrStr *input, ScrStr *base); +ScrDyn *scr_url_parse(ScrStr *input); /* +1 native URL or null; invalid URLs do not throw */ +ScrDyn *scr_url_parse_base(ScrStr *input, ScrStr *base); +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() */ +void scr_url_set(ScrUrl *u, ScrStr *field, ScrStr *value); /* borrowed; href may throw */ +bool scr_url_set_component(ScrUrl *u, ScrStr *field, ScrStr *value); /* borrowed; false for ignored writes */ +void scr_url_set_checked(ScrUrl *u, ScrStr *field, const ScrDyn *value); /* borrowed; conversion may throw */ +/* node:url's legacy surface (scr_url_legacy.c — linked with the regex, + * querystring and dyn-async units). url.parse() answers a checked-dynamic + * Url instance; format() accepts a Url-shaped object, a string, or a + * native URL (with its options bag); resolve() takes two strings. `warn` + * emits DEP0169 once per process (false at node_modules call sites). + * domainToASCII/Unicode take the argument and whether it was omitted + * (ERR_MISSING_ARGS). All borrow; +1, or NULL with Node's error pending. */ +ScrDyn *scr_url_legacy_parse(const ScrDyn *url, const ScrDyn *parse_query, const ScrDyn *slashes, + bool warn); +ScrStr *scr_url_legacy_format(const ScrDyn *object, const ScrDyn *options, bool warn); +ScrStr *scr_url_legacy_resolve(const ScrDyn *from, const ScrDyn *to, bool warn); +ScrStr *scr_url_domain_to_ascii(const ScrDyn *domain, bool missing); +ScrStr *scr_url_domain_to_unicode(const ScrDyn *domain, bool missing); +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 exactly: '+' 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 */ + +#endif /* SCR_URL_H */ diff --git a/packages/runtime/src/scr_watch.h b/packages/runtime/src/scr_watch.h new file mode 100644 index 000000000..c038dbe55 --- /dev/null +++ b/packages/runtime/src/scr_watch.h @@ -0,0 +1,38 @@ +/* scriptc runtime — fs.watch (scr_watch.c). Included by scr_runtime.h, which + * says where new declarations go. */ +#ifndef SCR_WATCH_H +#define SCR_WATCH_H + +#include "scr_bytes.h" + +/* ── 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)); + +#endif /* SCR_WATCH_H */ diff --git a/packages/runtime/src/scr_zlib.h b/packages/runtime/src/scr_zlib.h new file mode 100644 index 000000000..bd374b1da --- /dev/null +++ b/packages/runtime/src/scr_zlib.h @@ -0,0 +1,25 @@ +/* scriptc runtime — zlib (scr_zlib.c). Included by scr_runtime.h, which says + * where new declarations go. */ +#ifndef SCR_ZLIB_H +#define SCR_ZLIB_H + +#include "scr_bytes.h" + +/* ── 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); + +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); + +#endif /* SCR_ZLIB_H */ diff --git a/packages/runtime/test/headers.test.ts b/packages/runtime/test/headers.test.ts new file mode 100644 index 000000000..4555e658b --- /dev/null +++ b/packages/runtime/test/headers.test.ts @@ -0,0 +1,68 @@ +import { execFile } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { expect, test } from "vitest"; + +const execFileAsync = promisify(execFile); +const srcDir = join(import.meta.dirname, "../src"); + +/** The module headers scr_runtime.h includes, in include order. */ +const MODULE_HEADERS = [ + ...readFileSync(join(srcDir, "scr_runtime.h"), "utf8").matchAll(/^#include "(scr_\w+\.h)"/gm), +].map((m) => m[1]!); + +const DEFINE_SETS = [[], ["-DSCR_LIB"], ["-DSCR_LIB", "-DSCR_THREAD_INSTANCES"], ["-DSCR_WORKERS"]]; + +// Each module header includes what it uses, so a runtime unit can include +// only the headers it needs. A header that relies on an earlier one it does +// not include fails here; -Werror=visibility catches a `struct T *` +// parameter whose tag is not declared at file scope yet. +test("every module header compiles on its own", async () => { + expect(MODULE_HEADERS.length).toBeGreaterThan(40); + const dir = await mkdtemp(join(tmpdir(), "scriptc-runtime-headers-")); + try { + const failures: string[] = []; + await Promise.all( + MODULE_HEADERS.map(async (header) => { + const source = join(dir, `${header}.c`); + await writeFile(source, `#include "${header}"\n`); + for (const defines of DEFINE_SETS) { + try { + await execFileAsync("clang", [ + "-std=c11", + "-fsyntax-only", + "-Werror=visibility", + "-Werror=implicit-function-declaration", + ...(process.platform === "linux" ? ["-D_GNU_SOURCE"] : []), + ...defines, + "-I", + srcDir, + source, + ]); + } catch (error) { + failures.push( + `${header} ${defines.join(" ")}: ${(error as { stderr?: string }).stderr}`, + ); + } + } + }), + ); + expect(failures).toEqual([]); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + +test("scr_runtime.h holds no declarations of its own", () => { + const text = readFileSync(join(srcDir, "scr_runtime.h"), "utf8") + .replace(/\/\*[\s\S]*?\*\//g, "") + .split("\n") + .map((line) => line.trim()) + .filter((line) => line !== ""); + expect(text.filter((line) => !/^#(include "scr_\w+\.h"|ifndef|define|endif)/.test(line))).toEqual( + [], + ); +}); diff --git a/scripts/runtime-header-hunks.mjs b/scripts/runtime-header-hunks.mjs new file mode 100644 index 000000000..3f7ffa83c --- /dev/null +++ b/scripts/runtime-header-hunks.mjs @@ -0,0 +1,255 @@ +/* Retargets a diff of the single-file packages/runtime/src/scr_runtime.h + * onto the per-module runtime headers that scr_runtime.h now includes. + * + * Each hunk's old side (context and removed lines) is located in exactly one + * module header. When the full old side spans two headers or matches more + * than once, leading and trailing context lines are dropped until a single + * match remains; a hunk whose changed lines span two headers is split at its + * inner context runs. Placed hunks get three lines of context from the + * module header again. Hunks that cannot be placed are listed on stderr, + * left out of the output and make the exit status 1. The output is a patch + * for `git apply` (add `--reject` to keep going past a hunk that no longer + * applies). + * + * Usage: + * node scripts/runtime-header-hunks.mjs [--ref ] [diff-file] + * Reads the diff from stdin when no file is given. Module headers come from + * the working tree, or from with --ref. */ +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { parseArgs } from "node:util"; + +const HEADER_DIR = "packages/runtime/src"; +const UMBRELLA = "scr_runtime.h"; +const repoRoot = join(import.meta.dirname, ".."); + +const { values, positionals } = parseArgs({ + options: { ref: { type: "string" }, help: { type: "boolean", default: false } }, + allowPositionals: true, +}); +if (values.help) { + console.log("Usage: node scripts/runtime-header-hunks.mjs [--ref ] [diff-file]"); + process.exit(0); +} + +function readHeader(name) { + const path = `${HEADER_DIR}/${name}`; + if (values.ref === undefined) return readFileSync(join(repoRoot, path), "utf8"); + return execFileSync("git", ["show", `${values.ref}:${path}`], { + cwd: repoRoot, + encoding: "utf8", + }); +} + +function splitLines(text) { + const lines = text.split("\n"); + if (lines[lines.length - 1] === "") lines.pop(); + return lines; +} + +/** Module headers in umbrella include order: name → lines. */ +function moduleHeaders() { + const headers = new Map(); + for (const line of splitLines(readHeader(UMBRELLA))) { + const include = line.match(/^#include "(scr_[a-z0-9_]+\.h)"/); + if (include !== null) headers.set(include[1], splitLines(readHeader(include[1]))); + } + if (headers.size === 0) throw new Error(`${UMBRELLA} includes no module headers`); + return headers; +} + +/** Hunks of the scr_runtime.h file sections of a unified diff. */ +function parseHunks(diff) { + const hunks = []; + let inUmbrella = false; + let hunk = null; + for (const line of diff.split("\n")) { + if (line.startsWith("diff --git ")) { + inUmbrella = false; + hunk = null; + } else if (line.startsWith("+++ ")) { + inUmbrella = line.endsWith(`${HEADER_DIR}/${UMBRELLA}`); + hunk = null; + } else if (line.startsWith("--- ")) { + hunk = null; + } else if (inUmbrella && line.startsWith("@@")) { + const m = line.match(/^@@ -(\d+)(?:,\d+)? \+\d+(?:,\d+)? @@(.*)$/); + if (m === null) throw new Error(`unreadable hunk header: ${line}`); + hunk = { oldStart: Number(m[1]), section: m[2], lines: [] }; + hunks.push(hunk); + } else if (hunk !== null && /^[ +-]/.test(line)) { + hunk.lines.push(line); + } else if (hunk !== null && line === "") { + // Some tools drop the leading space of empty context lines. + hunk.lines.push(" "); + } + } + return hunks; +} + +const oldSide = (lines) => lines.filter((l) => l[0] !== "+").map((l) => l.slice(1)); + +/** Every start index where `needle` occurs in `hay`. */ +function findAll(hay, needle) { + const found = []; + for (let i = 0; i + needle.length <= hay.length; i++) { + let j = 0; + while (j < needle.length && hay[i + j] === needle[j]) j++; + if (j === needle.length) found.push(i); + } + return found; +} + +/** Refill context from the header to three lines on each side: git apply + * anchors a hunk without leading or trailing context to the start or end + * of the file. */ +function withContext(match, lines, text) { + const context = 3; + let lead = 0; + while (lead < lines.length && lines[lead][0] === " ") lead++; + let trail = 0; + while (trail < lines.length - lead && lines[lines.length - 1 - trail][0] === " ") trail++; + const before = Math.min(Math.max(context - lead, 0), match.at); + const end = match.at + oldSide(lines).length; + const after = Math.min(Math.max(context - trail, 0), text.length - end); + return { + name: match.name, + at: match.at - before, + lines: [ + ...text.slice(match.at - before, match.at).map((l) => ` ${l}`), + ...lines, + ...text.slice(end, end + after).map((l) => ` ${l}`), + ], + }; +} + +/** Locate one hunk: the trimmed hunk lines plus the header and 0-based line. */ +function locate(hunk, headers) { + const lines = hunk.lines; + let lead = 0; + while (lead < lines.length && lines[lead][0] === " ") lead++; + let trail = 0; + while (trail < lines.length - lead && lines[lines.length - 1 - trail][0] === " ") trail++; + // Try the least trimming first. + for (let drop = 0; drop <= lead + trail; drop++) { + for (let dl = Math.min(drop, lead); dl >= 0 && drop - dl <= trail; dl--) { + const kept = lines.slice(dl, lines.length - (drop - dl)); + const needle = oldSide(kept); + // A pure insertion needs at least one context line to anchor it. + if (needle.length === 0) continue; + const matches = []; + for (const [name, text] of headers) + for (const at of findAll(text, needle)) matches.push({ name, at }); + if (matches.length === 1) return withContext(matches[0], kept, headers.get(matches[0].name)); + } + } + return null; +} + +/** Merge hunks (sorted by position) whose old sides overlap. The overlap + * must be context in both. */ +function mergeOverlapping(list, name) { + const out = []; + for (const p of list) { + const prev = out[out.length - 1]; + const prevEnd = prev === undefined ? -1 : prev.at + oldSide(prev.lines).length; + if (prev === undefined || p.at >= prevEnd) { + out.push({ ...p, lines: [...p.lines] }); + continue; + } + const overlap = prevEnd - p.at; + const prevTail = prev.lines.filter((l) => l[0] !== "+").slice(-overlap); + let skip = 0; + for (let seen = 0; seen < overlap && skip < p.lines.length; skip++) { + if (p.lines[skip][0] !== " ") break; + seen++; + } + if (skip < Math.min(overlap, p.lines.length) || prevTail.some((l) => l[0] !== " ")) + throw new Error(`hunks overlap in ${name} near line ${p.at + 1}`); + prev.lines.push(...p.lines.slice(skip)); + } + return out; +} + +function formatPatch(placed) { + const byHeader = new Map(); + for (const p of placed) { + if (!byHeader.has(p.name)) byHeader.set(p.name, []); + byHeader.get(p.name).push(p); + } + let out = ""; + for (const [name, list] of byHeader) { + list.sort((a, b) => a.at - b.at); + const merged = mergeOverlapping(list, name); + const path = `${HEADER_DIR}/${name}`; + out += `diff --git a/${path} b/${path}\n--- a/${path}\n+++ b/${path}\n`; + let shift = 0; + for (const p of merged) { + const oldCount = p.lines.filter((l) => l[0] !== "+").length; + const newCount = p.lines.filter((l) => l[0] !== "-").length; + out += `@@ -${p.at + 1},${oldCount} +${p.at + 1 + shift},${newCount} @@${p.section}\n`; + out += p.lines.join("\n") + "\n"; + shift += newCount - oldCount; + } + } + return out; +} + +/** Split a hunk at its inner context runs: one piece per run of changed + * lines, each with the context on both sides of it. */ +function pieces(hunk) { + const runs = []; + for (let i = 0; i < hunk.lines.length;) { + if (hunk.lines[i][0] === " ") { + i++; + continue; + } + let j = i; + while (j < hunk.lines.length && hunk.lines[j][0] !== " ") j++; + runs.push([i, j]); + i = j; + } + return runs.map((_, k) => { + const start = k === 0 ? 0 : runs[k - 1][1]; + const end = k === runs.length - 1 ? hunk.lines.length : runs[k + 1][0]; + return { ...hunk, lines: hunk.lines.slice(start, end) }; + }); +} + +const diff = readFileSync(positionals[0] ?? 0, "utf8"); +const headers = moduleHeaders(); +const hunks = parseHunks(diff); +const placed = []; +let missed = 0; +for (const hunk of hunks) { + const whole = locate(hunk, headers); + const parts = whole !== null ? [{ ...whole, section: hunk.section }] : []; + if (whole === null) { + // The hunk spans two headers: place each changed run on its own. + // Runs that land next to each other merge again in formatPatch. + for (const piece of pieces(hunk)) { + const where = locate(piece, headers); + if (where === null) { + parts.length = 0; + break; + } + parts.push({ ...where, section: hunk.section }); + } + } + if (parts.length === 0) { + missed++; + process.stderr.write(`not placed: ${UMBRELLA} hunk at old line ${hunk.oldStart}\n`); + process.stderr.write(hunk.lines.map((l) => ` ${l}`).join("\n") + "\n"); + continue; + } + for (const part of parts) { + placed.push(part); + process.stderr.write( + `${UMBRELLA}:${hunk.oldStart} -> ${HEADER_DIR}/${part.name}:${part.at + 1}\n`, + ); + } +} +process.stdout.write(formatPatch(placed)); +process.stderr.write(`${hunks.length - missed} of ${hunks.length} hunks placed\n`); +process.exitCode = missed === 0 ? 0 : 1;