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 54b3ee939..9a7ccbb2f 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..36efe6fef --- /dev/null +++ b/packages/runtime/src/scr_bytes.h @@ -0,0 +1,448 @@ +/* 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. */ + bool untransferable; /* A Buffer pool slab: its ArrayBuffer cannot be transferred. */ + 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); +/* The Buffer pool (Node's lib/buffer.js allocate/fromStringFast/ + * fromArrayLike): Buffer.allocUnsafe, Buffer.from of strings, arrays and + * array-likes, and Buffer.concat return views into a per-thread slab of + * Buffer.poolSize bytes when the size is below poolSize >>> 1. Views start + * at 8-byte aligned offsets; a request that does not fit starts a new + * slab. A slab lives while any of its views does. All return +1 Buffers. */ +double scr_buffer_pool_size(void); +void scr_buffer_set_pool_size(double size); +ScrBytes *scr_buffer_alloc(double size); /* zeroed, never pooled */ +ScrBytes *scr_buffer_alloc_unsafe(double size); /* pooled */ +ScrBytes *scr_buffer_alloc_unsafe_slow(double size); /* never pooled */ +ScrBytes *scr_buffer_from_str(const ScrStr *s, const ScrStr *enc); +ScrBytes *scr_buffer_from_str_checked(const ScrStr *s, const ScrStr *enc); +ScrBytes *scr_buffer_from_bytes(const ScrBytes *src); /* a copy of any typed array's values */ +ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */ +ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding); +ScrBytes *scr_buffer_concat(const ScrArr *list); +ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total); +/* Drops the pool's slab reference and restores Buffer.poolSize (process + * exit audits and library session resets). */ +void scr_buffer_pool_reset(void); +/* A pooled copy of an unpooled u8 Buffer below the pool threshold; others + * are returned as they are. Consumes `b`. */ +ScrBytes *scr_buffer_pool_adopt(ScrBytes *b); +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..777959f25 --- /dev/null +++ b/packages/runtime/src/scr_crypto.h @@ -0,0 +1,165 @@ +/* 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); +/* Secret KeyObjects (scr_lib.c). */ +ScrDyn *scr_crypto_create_secret_key(ScrDyn *key, ScrDyn *encoding); +ScrDyn *scr_crypto_generate_key_sync(ScrDyn *type, ScrDyn *options); +ScrBytes *scr_crypto_key_object_input(ScrDyn *key); +bool scr_crypto_is_key_object(const ScrDyn *value); +const unsigned char *scr_key_object_bytes(const ScrDyn *value, size_t *len); +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); +void scr_crypto_generate_key_async(ScrDyn *type, ScrDyn *options, 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..bf7262ab6 --- /dev/null +++ b/packages/runtime/src/scr_dyn.h @@ -0,0 +1,1081 @@ +/* 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_KEY_OBJECT, /* node:crypto secret KeyObject (scr_lib.c) */ + 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 */ +/* The same, naming a Buffer pool slab's ArrayBuffer as Node does when it is + * the transfer list's first member (borrowed). */ +ScrDyn *scr_structured_clone_transfer_fail_with(const ScrDyn *first); +/* 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; + /* The @@toStringTag when it differs from cls: String() answers + * "[object ]" and util.inspect prints ` [] `. */ + const char *to_string_tag; + /* The view has no properties, so util.inspect prints it in full at any + * depth, as Node prints `{}`. */ + bool inspect_view_empty; +} 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 dab8ce6a4..3cb2de978 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -4,9168 +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); -/* Secret KeyObjects (scr_lib.c). */ -ScrDyn *scr_crypto_create_secret_key(ScrDyn *key, ScrDyn *encoding); -ScrDyn *scr_crypto_generate_key_sync(ScrDyn *type, ScrDyn *options); -ScrBytes *scr_crypto_key_object_input(ScrDyn *key); -bool scr_crypto_is_key_object(const ScrDyn *value); -const unsigned char *scr_key_object_bytes(const ScrDyn *value, size_t *len); -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); -void scr_crypto_generate_key_async(ScrDyn *type, ScrDyn *options, 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_KEY_OBJECT, /* node:crypto secret KeyObject (scr_lib.c) */ - 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 */ -/* The same, naming a Buffer pool slab's ArrayBuffer as Node does when it is - * the transfer list's first member (borrowed). */ -ScrDyn *scr_structured_clone_transfer_fail_with(const ScrDyn *first); -/* 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; - /* The @@toStringTag when it differs from cls: String() answers - * "[object ]" and util.inspect prints ` [] `. */ - const char *to_string_tag; - /* The view has no properties, so util.inspect prints it in full at any - * depth, as Node prints `{}`. */ - bool inspect_view_empty; -} 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. */ - bool untransferable; /* A Buffer pool slab: its ArrayBuffer cannot be transferred. */ - 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); -/* The Buffer pool (Node's lib/buffer.js allocate/fromStringFast/ - * fromArrayLike): Buffer.allocUnsafe, Buffer.from of strings, arrays and - * array-likes, and Buffer.concat return views into a per-thread slab of - * Buffer.poolSize bytes when the size is below poolSize >>> 1. Views start - * at 8-byte aligned offsets; a request that does not fit starts a new - * slab. A slab lives while any of its views does. All return +1 Buffers. */ -double scr_buffer_pool_size(void); -void scr_buffer_set_pool_size(double size); -ScrBytes *scr_buffer_alloc(double size); /* zeroed, never pooled */ -ScrBytes *scr_buffer_alloc_unsafe(double size); /* pooled */ -ScrBytes *scr_buffer_alloc_unsafe_slow(double size); /* never pooled */ -ScrBytes *scr_buffer_from_str(const ScrStr *s, const ScrStr *enc); -ScrBytes *scr_buffer_from_str_checked(const ScrStr *s, const ScrStr *enc); -ScrBytes *scr_buffer_from_bytes(const ScrBytes *src); /* a copy of any typed array's values */ -ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */ -ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding); -ScrBytes *scr_buffer_concat(const ScrArr *list); -ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total); -/* Drops the pool's slab reference and restores Buffer.poolSize (process - * exit audits and library session resets). */ -void scr_buffer_pool_reset(void); -/* A pooled copy of an unpooled u8 Buffer below the pool threshold; others - * are returned as they are. Consumes `b`. */ -ScrBytes *scr_buffer_pool_adopt(ScrBytes *b); -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;