Merge main

This commit is contained in:
Chris Tate
2026-10-15 03:59:47 -05:00
57 changed files with 10154 additions and 9161 deletions
+4
View File
@@ -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-<platform>` and `@scriptc/runtime-<platform>` 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_<unit>.h` for `scr_<unit>.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 <diff>`. 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.
@@ -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<string>(),
): Promise<string> {
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<string, CProto>; dataSyms: Set<string> }> {
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) {}`,
);
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5550-locale-compare-ascii-words.ts"
],
"diags": []
}
+425
View File
@@ -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 <i> out of bounds (length <n>)
* to stderr and abort()s in the proven-presence accessors. `scr_arr_has` is
* the shared presence query used before those accessors when the language
* surface permits an undefined result.
*
* Ownership: _get_ref returns +1 (retains before returning); _set_ref and
* _push_ref take ownership of the new element (and _set_ref releases the
* old one); _pop_ref transfers the element's reference out to the caller.
*/
typedef enum {
SCR_ELEM_F64,
SCR_ELEM_BOOL,
SCR_ELEM_STR, /* slots are ScrStr* — released with the array */
SCR_ELEM_ARR, /* slots are ScrArr* — released recursively */
SCR_ELEM_BYTES, /* slots are ScrBytes* — released with the array */
/* Slots are void* to values the runtime cannot lay out itself — records,
* class instances, unions, and cycle-capable inner arrays. The element
* type's RC entry points arrive at construction (scr_arr_new_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 */
+168
View File
@@ -0,0 +1,168 @@
/* 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 */
/* AssertionError properties: a failure path records actual, expected and
* operator before it throws (scr_assert.c). */
void scr_assert_fail(ScrStr *message, bool generated); /* takes ownership; always throws */
void scr_assert_record(ScrDyn *actual, ScrDyn *expected, const char *op); /* values move in */
void scr_assert_record_dyn(ScrDyn *actual, ScrDyn *expected, ScrStr *op); /* borrows */
const char *scr_assert_eq_operator(bool negated, bool deep);
ScrError *scr_assert_error_new(ScrDyn *options);
ScrStr *scr_assert_inspect_str(const ScrStr *s); /* util.inspect quoting; +1 */
void scr_assert_ok(bool pass, ScrStr *message, bool generated);
bool scr_assert_same_value_f64(double a, double b); /* Object.is */
/* deepStrictEqual over cyclic values: pair memo (enter answers true for
* a pair already being compared — Node's coinductive memo; leave pops).
* The compiler-emitted helpers over cycle-capable types wrap with these. */
bool scr_assert_deq_enter(const void *a, const void *b);
void scr_assert_deq_leave(void);
void scr_assert_eq_f64(double a, double b, bool negated, bool deep, ScrStr *msg, bool has_msg);
void scr_assert_eq_str(ScrStr *a, ScrStr *b, bool negated, bool deep, ScrStr *msg, bool has_msg);
void scr_assert_eq_bool(bool a, bool b, bool negated, bool deep, ScrStr *msg, bool has_msg);
/* BigInt scalar equality (scr_bigint_assert.c): included only when both
* optional runtime families are active, preserving their independent
* link gates. */
void scr_assert_eq_bigint(ScrBigInt *a, ScrBigInt *b, bool negated, bool deep,
ScrStr *msg, bool has_msg);
void scr_assert_deep_result(bool equal, bool negated, ScrStr *msg, bool has_msg);
/* deepStrictEqual's verdict over two typed-array/Buffer values of one
* static elem kind: brands_eq (Node compares prototypes — Buffer vs
* Uint8Array differ; a compile-time answer from the two arguments' checker
* types) AND same element count AND byte-identical storage. Both sides
* always evaluate (they are ordinary borrowed arguments), so a
* compile-time brand mismatch still runs their side effects. Never
* throws. */
bool scr_assert_bytes_deep_eq(const ScrBytes *a, const ScrBytes *b, bool brands_eq);
/* strictEqual/notStrictEqual over bytes values: reference identity
* (Object.is on objects), with Node's object-comparison headers —
* "Values have same structure but are not reference-equal:" when the
* contents (and static brands) match, the reference-equal expectation
* header otherwise. Header-only messages, the composite stance
* (SEMANTICS.md 102). Borrows a/b/msg; throws on the failing verdict. */
void scr_assert_ref_eq_bytes(const ScrBytes *a, const ScrBytes *b, bool negated,
bool brands_eq, ScrStr *msg, bool has_msg);
/* strictEqual/notStrictEqual over function values: reference identity
* with the same object-comparison headers (two distinct functions are
* never structure-equal, so the failing equal form always expects
* reference equality). Header-only messages (SEMANTICS.md 102). */
void scr_assert_ref_eq_fn(const ScrClosure *a, const ScrClosure *b, bool negated,
ScrStr *msg, bool has_msg);
/* The failing-EQUAL-operator message assembler (createErrDiff's scalar
* slice): exported for the spoke files that carry their own value
* inspection (scr_symbol.c's symbol comparison — the assert.match
* precedent). `ia`/`ib` are the inspected actual/expected bytes;
* `quotes` counts string-typed sides (2 quote chars each, excluded from
* the 12-char short-form budget); `strings` enables the stacked form's
* first-difference `^` indicator. Throws; borrows msg. */
void scr_assert_eq_fail(const char *ia, size_t la, const char *ib, size_t lb,
int quotes, bool both_zero, bool strings, bool deep,
ScrStr *msg, bool has_msg);
/* The failing NOT-equal-operator twin: `insp` is the inspected actual. */
void scr_assert_neq_fail(const char *insp, size_t ilen, bool deep,
ScrStr *msg, bool has_msg);
/* strictEqual/notStrictEqual/deepStrictEqual/notDeepStrictEqual where
* either operand is a checked-dynamic value (the frontend boxes a static
* side into the checked-dynamic tree first). Strict pair: SameValue over the dyn kinds —
* Object.is numbers, byte-equal strings, units by kind, node identity
* for arrays/objects/bytes, BOXED-CLOSURE identity for functions. Deep
* pair: the structural dyn walk (per-element arrays, key-set objects,
* brand-aware bytes via the buffer flavor bit, closure identity for
* functions). Generated messages reproduce assertion_error.js — the
* scalar simple/stacked forms byte-exactly, composite values rendered
* compact:false/sorted through the checked-dynamic tree and diffed with the real myers
* line printer (the design note atop scr_assert.c's dyn section lists
* the divergences). Borrows everything; throws on the failing verdict. */
void scr_assert_eq_dyn(ScrDyn *a, ScrDyn *b, bool negated, bool deep,
ScrStr *msg, bool has_msg);
/* assert.equal/notEqual over the frontend's Abstract Equality verdict;
* actual/expected are borrowed util.inspect renderings. */
void scr_assert_loose_result(bool equal, bool negated, ScrStr *actual,
ScrStr *expected, ScrStr *msg, bool has_msg);
/* assert.throws / assert.rejects whose callback returned (or whose
* promise fulfilled): "Missing expected exception|rejection" with Node's
* details — ` (${expected.name})` when the expected class/shape carries a
* name (has_ename), then `: message` or `.`. */
/* Node's expectsError over an error-INSTANCE expected: the expected dyn
* error's keys (minus the %error marker) each deep-compare against the
* caught value's; mismatches throw the deep-equal AssertionError. */
void scr_assert_expects_err_dyn(ScrDyn *actual, ScrDyn *expected, ScrStr *msg, bool has_msg);
void scr_assert_throws_none(bool rejection, ScrStr *ename, bool has_ename,
ScrStr *msg, bool has_msg);
void scr_assert_throws_mismatch(ScrStr *expected_name, ScrError *err,
ScrStr *msg, bool has_msg);
/* assert.throws(fn, <object shape>) — Node's expectedException over the
* static error surface (name/message/code). A tiny per-call accumulator:
* begin stashes the caught error, each slot call adds one expected key
* (key ids: 0 code, 1 message, 2 name — inspect's SORTED render order),
* and end runs the comparison, throwing Node's deep-equal Comparison
* diff (byte-exact: the bounded key set makes the full rendering
* enumerable) or the custom message. Regex-valued slots enter
* through scr_regex.c (scr_assert_shape_re) which tests eagerly and
* stores the verdict + rendered source, so this file stays libregexp-free;
* scr_assert_shape_actual is its borrowed key-value getter (NULL =
* absent). Straight-line calls between begin and end — no fiber
* suspension, so the accumulator is safely a static. */
void scr_assert_shape_begin(ScrError *err);
void scr_assert_shape_str(int key, ScrStr *v);
void scr_assert_shape_slot_re(int key, bool matched, ScrStr *rendered /* moves */);
ScrStr *scr_assert_shape_actual(int key); /* borrowed; NULL = absent */
void scr_assert_shape_end(ScrStr *msg, bool has_msg);
/* assert.doesNotReject whose rejection MATCHED (or had no expected):
* "Got unwanted rejection[: message].\nActual message: \"...\"". */
void scr_assert_unwanted_rejection(ScrError *err, ScrStr *msg, bool has_msg);
/* 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 */
+389
View File
@@ -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<T>[] entries array and the pre-capacity (empty,
* cap >= ps->len) values array — NULL values/store for Promise<void>
* entries, whose result fulfills void — retains what it keeps, and
* returns the result promise +1. Node-exact: 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 */
+72
View File
@@ -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 */
+83
View File
@@ -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 */
+423
View File
@@ -0,0 +1,423 @@
/* scriptc runtime — typed arrays and Buffer (scr_bytes.c). Included by
* scr_runtime.h, which says where new declarations go. */
#ifndef SCR_BYTES_H
#define SCR_BYTES_H
#include "scr_events.h"
/* ── typed arrays / Buffer (scr_bytes.c) ──────────────────────────────
* ONE runtime representation for the supported numeric typed arrays,
* Node's Buffer (a Uint8Array subclass), and DataView: a refcounted,
* MUTABLE, fixed-length element buffer. An ScrBytes either OWNS its
* storage (backing == NULL, byteOffset 0) or is a VIEW: its `data` points
* INTO an owner's storage and its `backing` retains that owner (chain
* depth is always exactly 1 — views over views resolve to the owner at
* construction), so reads/writes through the view alias the source
* JS-exactly. Views come from DataView (scr_dataview_new) and from
* subarray()/Buffer-slice() (scr_bytes_subarray — Buffer's slice is
* subarray's deprecated alias in Node); only the plain typed arrays'
* slice() copies (scr_bytes_slice, matching JS).
* Elements are scalars only and the backing edge is acyclic by
* construction (owners point at nothing): never part of a cycle, no
* trace. Element reads widen to double; writes coerce JS-exactly (ToUint8
* / ToUint32 modular truncation, double→float rounding for f32).
* Out-of-bounds typed ELEMENT reads trap like arrays; invalid writes are
* ignored as in JavaScript. The Node-shaped operations
* (construction lengths, set(), the read/write numeric families, the
* DataView constructor and getters) THROW catchable RangeErrors exactly
* where Node throws. */
typedef enum ScrBytesElem {
SCR_BYTES_U8, /* Uint8Array / Buffer */
SCR_BYTES_U32, /* Uint32Array */
SCR_BYTES_F32, /* Float32Array */
SCR_BYTES_I32, /* Int32Array (reads sign-extend; writes ToInt32-wrap) */
SCR_BYTES_F64, /* Float64Array */
SCR_BYTES_I8, /* Int8Array */
SCR_BYTES_U16, /* Uint16Array */
SCR_BYTES_I16, /* Int16Array */
SCR_BYTES_U8C, /* Uint8ClampedArray */
} ScrBytesElem;
typedef struct ScrSharedBytes ScrSharedBytes;
typedef struct ScrBytes {
size_t rc;
size_t len; /* ELEMENT count, fixed at construction */
ScrBytesElem elem;
uint8_t *data; /* len * elem_size bytes; owned unless backing is set */
/* NULL for owners. A view (DataView, subarray, Buffer-slice) sets this
* to the retained OWNER it aliases (chain depth is always exactly 1:
* views over views resolve to the owner at construction) and its `data`
* points into backing->data — released, never freed. */
struct ScrBytes *backing;
bool is_buffer; /* Buffer brand belongs to the view, not its backing. */
bool is_data_view; /* DataView is distinct from a numeric typed array. */
bool external; /* Root aliases foreign memory; releasing it never frees data. */
ScrSharedBytes *shared; /* Neutral backing, borrowed by views from their local owner. */
} ScrBytes;
/* Internal scoped locking is only for leaf memory operations. Never hold this
* guard across a script callback, blocking I/O, or a fiber suspension. */
void scr_shared_enter(void);
void scr_shared_leave(void);
bool scr_shared_guard(const ScrBytes *, const ScrBytes *);
void scr_shared_guard_release(bool *);
#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES))
#define SCR_SHARED_GUARD(a, b) bool scr_shared_held __attribute__((cleanup(scr_shared_guard_release))) = scr_shared_guard(a, b)
#else
#define SCR_SHARED_GUARD(a, b) ((void)0)
#endif
ScrSharedBytes *scr_shared_retain(ScrSharedBytes *);
void scr_shared_release(ScrSharedBytes *);
void scr_bytes_make_shared(ScrBytes *);
ScrBytes *scr_shared_wrap(ScrSharedBytes *);
void scr_bytes_read(const ScrBytes *, size_t, void *, size_t);
void scr_bytes_write(ScrBytes *, size_t, const void *, size_t);
ScrStr *scr_bytes_string(const ScrBytes *, size_t, size_t);
ScrBytes *scr_bytes_snapshot(const ScrBytes *);
void scr_bytes_snapshot_release(ScrBytes **);
ScrBytes *scr_bytes_local_copy(ScrBytes *); /* +1; shared inputs are copied */
void scr_bytes_require_unshared(const ScrBytes *);
/* Snapshot before passing byte pointers to I/O or callback-capable consumers.
* The copy is local, so neither a lock nor shared raw memory escapes. */
#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES))
#define SCR_BYTES_SNAPSHOT(name) \
ScrBytes *scr_snapshot_##name __attribute__((cleanup(scr_bytes_snapshot_release))) = scr_bytes_snapshot(name); \
if (scr_snapshot_##name) name = scr_snapshot_##name
#else
#define SCR_BYTES_SNAPSHOT(name) ((void)0)
#endif
ScrDyn *scr_shared_array_buffer_new(ScrDyn *);
ScrDyn *scr_worker_data(void);
ScrDyn *scr_worker_parent_port(void);
ScrDyn *scr_worker_new(double root, ScrStr *filename, ScrDyn *options);
bool scr_shared_array_buffer_is(const ScrDyn *);
bool scr_buffer_storage_is(const ScrDyn *);
ScrBytes *scr_bytes_from_external(void *data, size_t length);
ScrDyn *scr_ffi_memory_module(ScrDyn *catalog);
ScrDyn *scr_ffi_argument(ScrDyn *value, ScrStr *type);
size_t scr_bytes_elem_size(ScrBytesElem elem); /* 1, 2, 4, or 8 */
const char *scr_bytes_elem_name(ScrBytesElem elem);
double scr_bytes_to_u8_clamp(double value);
/* node:string_decoder's StringDecoder (scr_bytes.c, beside the decoders
* it shares): the decoder value is a record holding the CANONICAL
* encoding name and an f64 packing the pending partial sequence (count +
* up to 3 raw bytes — every encoding buffers at most 3); the compiler's
* interned %strdec helpers thread both through these pure functions.
* write answers the decoded complete prefix of pending+chunk, next the
* packed new pending, end the buffered partial's flush (utf8 replacement
* chars, base64's padded remainder, utf16le's held bytes; the stateless
* latin1/ascii/hex flush nothing). Node-exact per encoding — utf8's
* incomplete-tail walk, base64's mod-3 groups, utf16le's odd-byte and
* trailing-lead-surrogate holds. None throws. */
ScrStr *scr_strdec_write(const ScrStr *enc, double pending, const ScrBytes *chunk);
double scr_strdec_next(const ScrStr *enc, double pending, const ScrBytes *chunk);
ScrStr *scr_strdec_end(const ScrStr *enc, double pending);
/* `new Uint8Array(n)` / Buffer.alloc(n): zero-filled. n goes through
* ToIndex like JS (NaN → 0, truncate toward zero; negative or > 2^53-1
* THROWS Node's "Invalid typed array length" RangeError catchably and
* returns NULL with the exception pending). */
ScrBytes *scr_bytes_new(ScrBytesElem elem, double n); /* +1 */
/* Shared u8 view over exactly the source view's raw bytes. Borrows; +1. */
ScrBytes *scr_bytes_raw_view(ScrBytes *bytes);
ScrBytes *scr_bytes_as_buffer(ScrBytes *bytes); /* borrows; +1, Buffer factory only */
/* Views retain the root allocation; offsets are relative to its complete
* byte storage, including when src is itself a subarray. */
ScrBytes *scr_bytes_buffer_view(ScrBytes *src, ScrBytesElem elem,
double offset, bool has_len, double length);
ScrDyn *scr_array_buffer_constructor(void);
ScrDyn *scr_array_buffer_new(ScrDyn *length);
ScrDyn *scr_array_buffer_from_bytes(ScrBytes *view);
bool scr_array_buffer_is(const ScrDyn *value);
bool scr_array_buffer_is_view(const ScrDyn *value);
double scr_array_buffer_byte_length_getter(void);
ScrDyn *scr_array_buffer_byte_length_descriptor(ScrDyn *getter);
ScrBytes *scr_array_buffer_view(ScrBytesElem elem, const ScrDyn *buffer,
const ScrDyn *offset, const ScrDyn *length);
ScrBytes *scr_array_buffer_view_u8c(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_i8(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_u16(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_i16(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_u8(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_u32(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_i32(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_f32(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_f64(ScrDyn *, ScrDyn *, ScrDyn *);
ScrBytes *scr_array_buffer_view_dv(ScrDyn *, ScrDyn *, ScrDyn *);
/* Native callback boundary: exact owned u8 copy. NULL with len == 0 is an
* empty span. */
ScrBytes *scr_bytes_from_data(const uint8_t *data, size_t len); /* +1 */
ScrBytes *scr_bytes_take_data(uint8_t *data, size_t len); /* +1, consumes malloc storage */
/* Same-element-kind copy, preserving the raw representation. Cross-kind
* construction uses scr_bytes_convert. Borrows src. Never throws. */
ScrBytes *scr_bytes_copy(const ScrBytes *src); /* +1 */
/* `new Uint8Array([1, 2, 3])` / Buffer.from(number[]): each f64 element
* coerces per the element kind (ToUint8/ToUint32/float). Borrows arr
* (SCR_ELEM_F64). Never throws. */
ScrBytes *scr_bytes_from_arr(ScrBytesElem elem, const ScrArr *arr); /* +1 */
static inline ScrBytes *scr_bytes_retain(ScrBytes *b) {
if (b->rc != SIZE_MAX) b->rc++;
return b;
}
void scr_bytes_release(ScrBytes *b); /* NULL-tolerant */
void *scr_bytes_retain_v(void *b);
void scr_bytes_release_v(void *b);
#ifdef SCR_RC_AUDIT
long scr_bytes_live_count(void);
#endif
double scr_bytes_len(const ScrBytes *b); /* element count */
double scr_bytes_byte_len(const ScrBytes *b); /* len * elem size */
/* `.byteOffset`: 0 for owners (scriptc typed arrays always own their
* whole storage — SEMANTICS.md notes the divergence from Node's Buffer
* pooling), the view's offset into its owner's storage for a DataView. */
double scr_bytes_byte_offset(const ScrBytes *b);
/* `new DataView(src.buffer, byteOffset?, byteLength?)` — src is the bytes
* value whose `.buffer` the program named (any elem kind; a DataView src
* resolves to ITS owner, so offsets stay relative to the one buffer,
* exactly like JS). Both indices go through ToIndex (NaN → 0, truncate
* toward zero); a bad offset THROWS Node's "Start offset %s is outside
* the bounds of the buffer" and a bad/overflowing length "Invalid
* DataView length %s" (catchable RangeErrors, NULL result with the
* exception pending). has_len distinguishes an omitted byteLength (view
* to the end of the buffer) from an explicit one. Borrows src; the
* result retains the owner. */
ScrBytes *scr_dataview_new(ScrBytes *src, double byte_off, bool has_len, double byte_len); /* +1 */
/* DataView getters. LE flag false = big-endian (the JS default). The
* BIGU64/BIGI64 kinds answer the composed Number(view.getBigUint64/
* getBigInt64(...)) lowering: the 8-byte integer converted to double
* (round-to-nearest-even — exactly Number(bigint)). Offsets go through
* ToIndex; any invalid or out-of-range offset THROWS Node's "Offset is
* outside the bounds of the DataView" RangeError catchably (0 result
* with the exception pending). */
typedef enum ScrDataViewGet {
SCR_DV_U8, SCR_DV_I8, SCR_DV_U16, SCR_DV_I16, SCR_DV_U32, SCR_DV_I32,
SCR_DV_F32, SCR_DV_F64, SCR_DV_BIGU64, SCR_DV_BIGI64,
} ScrDataViewGet;
double scr_dataview_get(const ScrBytes *b, double byte_off, ScrDataViewGet kind, bool le);
/* DataView setters (the integer/float kinds only — the BIG kinds never
* lower: bigint arguments have no representation). Values coerce
* JS-exactly: the integer kinds by modular truncation (ToUint32's residue
* — the narrower widths store its low bytes, the same 2^width residue),
* F32 by double→float round-to-nearest-even. Offsets go through ToIndex
* with the getters' one RangeError. */
void scr_dataview_set(ScrBytes *b, double byte_off, double value, ScrDataViewGet kind, bool le);
/* Element read/write. Invalid typed reads trap; invalid writes are ignored.
* Checked reads preserve JavaScript's undefined result. Valid writes coerce
* JS-exactly: u8/u32 by modular truncation (NaN/±Infinity → 0, truncate
* toward zero, wrap mod 2^8/2^32), f32 by double→float rounding. */
double scr_bytes_get(const ScrBytes *b, double i);
double scr_bytes_get_or_nan(const ScrBytes *b, double i); /* NaN when the index is invalid */
void scr_bytes_set(ScrBytes *b, double i, double v);
/* Replace a live typed-array capsule's fixed-size payload from its dyn
* snapshot. Source and target have the same compiler-checked bytes type. */
void scr_bytes_copy_contents(ScrBytes *dst, const ScrBytes *src);
/* TypedArray.prototype.slice(start, end): relative indices clamp like
* string/array slice (ToIntegerOrInfinity, negatives from the end); the
* result is a fresh same-kind copy. Never throws. */
ScrBytes *scr_bytes_slice(const ScrBytes *b, double start, double end); /* +1 */
/* Independent element-coerced copy; same-kind copies preserve raw bits. */
ScrBytes *scr_bytes_convert(ScrBytesElem elem, const ScrBytes *src); /* +1 */
/* Checked native constructor/from input; no JavaScript engine required.
* `from` selects iterable/array-like semantics, otherwise primitives are
* converted to lengths. Borrows source, returns +1 or NULL+pending. */
ScrBytes *scr_bytes_from_dyn(ScrBytesElem elem, const ScrDyn *value, bool from);
/* In-place, overlapping copy with relative/clamped element indices;
* returns the retained receiver, including for a shared subarray view. */
ScrBytes *scr_bytes_copy_within(ScrBytes *b, double target, double start, double end); /* +1 */
/* ES2023 typed-array copying methods. Both preserve the receiver's element
* kind and return a fresh +1 owner. with() raises Node's catchable
* "Invalid typed array index" RangeError for an invalid relative index. */
ScrBytes *scr_bytes_to_reversed(const ScrBytes *b); /* +1 */
/* In-place numeric sort of a typed array: order 0 is the default
* TypedArray order (NaN last, -0 before +0); 1 or -1 is a recognized
* numeric comparator's, refused (false, nothing written) when a float
* element is NaN or -0. Runs no script code. */
bool scr_bytes_sort_numeric(ScrBytes *b, int32_t order);
/* Comparator sort of a typed array with the comparison schedule Node
* uses; `f` is a borrowed closure of `arity` (0..2) number parameters.
* A comparator exception stays pending and nothing is written back. */
void scr_bytes_sort_compare(ScrBytes *b, struct ScrClosure *f, uint32_t arity);
ScrBytes *scr_bytes_with(const ScrBytes *b, double index, double value); /* +1 */
/* Numeric typed-array iteration drained into number[], and Uint8Array.join.
* Inputs borrowed; results fresh +1. */
ScrArr *scr_bytes_to_arr(const ScrBytes *b); /* +1 */
ScrStr *scr_bytes_join(const ScrBytes *b, const ScrStr *separator); /* +1 */
/* TypedArray.prototype.fill on non-u8 receivers: per-element fill with
* the element write's coercion, slice-clamped relative indices; answers
* the receiver +1 (chaining). Never throws. */
ScrBytes *scr_bytes_fill_elem(ScrBytes *b, double v, double start, double end); /* +1 */
/* TypedArray.prototype.subarray(start, end) — and Buffer's slice(), its
* deprecated alias: a same-elem VIEW aliasing the receiver's storage
* (mutations visible both ways, JS-exactly). The view retains the OWNER
* (chain depth exactly 1, the DataView rule) and its byteOffset composes.
* Same index clamping as slice; never throws. */
ScrBytes *scr_bytes_subarray(ScrBytes *b, double start, double end); /* +1 */
/* dst.set(src, offset): overlapping-safe, element-coerced copy.
* offset goes through ToIntegerOrInfinity; a negative offset or
* src.len + offset > dst.len THROWS Node's "offset is out of bounds"
* RangeError catchably. */
void scr_bytes_set_from(ScrBytes *dst, const ScrBytes *src, double offset);
/* Array-like source with sequential element coercion and catchable errors.
* Earlier writes remain visible when a later coercion throws. */
void scr_bytes_set_from_dyn(ScrBytes *dst, const ScrDyn *src, double offset);
/* buf.toString(enc) on u8 bytes: "utf8" decodes with WHATWG per-maximal-
* subpart U+FFFD replacement (Node-exact for invalid sequences), "hex" is
* lowercase pairs, "base64"/"base64url" the standard/url alphabets (url
* unpadded), "latin1" maps bytes to U+00XX, "ascii" masks to & 0x7f, and
* "utf16le" decodes LE code units (surrogate pairs combine; a LONE
* surrogate stays lone; an odd tail byte drops). Aliases arrive NORMALIZED (the compiler folds "binary",
* "ucs2", "utf-8", ...); NULL selects the default UTF-8 encoding for
* implicit native coercions. Borrows both; +1 result. Never throws (the
* compiler fences other encodings). */
ScrStr *scr_bytes_to_str(const ScrBytes *b, const ScrStr *enc);
ScrStr *scr_bytes_to_str_range(const ScrBytes *b, const ScrStr *enc, double start, double end);
/* Runtime-valued Buffer.toString encoding: aliases/case canonicalize;
* unknown names throw ERR_UNKNOWN_ENCODING. +1, or NULL when throwing. */
ScrStr *scr_bytes_to_str_checked(const ScrBytes *b, const ScrStr *enc);
ScrStr *scr_bytes_to_str_checked_range(const ScrBytes *b, const ScrStr *enc, double start, double end);
/* WHATWG TextDecoder.decode over u8 bytes (utf-8, default options): the
* same replacement decode as toString("utf8") with the leading BOM
* stripped. Borrows; +1; never throws. */
ScrStr *scr_text_decode(const ScrBytes *b);
double scr_text_decoder_encoding(const ScrDyn *label);
ScrStr *scr_text_decoder_name(double encoding);
ScrDyn *scr_text_encode_into(const ScrDyn *source, const ScrDyn *destination);
bool scr_buffer_is_ascii(const ScrDyn *input);
bool scr_buffer_is_utf8(const ScrDyn *input);
ScrBytes *scr_buffer_transcode(const ScrDyn *source, const ScrDyn *from, const ScrDyn *to);
ScrBytes *scr_bytes_buffer_source(const ScrDyn *value);
ScrStr *scr_text_decode_buffer_source(const ScrDyn *value);
ScrStr *scr_text_decode_options(const ScrBytes *b, bool fatal, bool ignore_bom);
ScrStr *scr_text_decode_stream(ScrDyn *state, const ScrBytes *b, double encoding, bool fatal, bool ignore_bom, bool stream);
ScrStr *scr_text_decode_legacy_options(const ScrBytes *b, double encoding, bool fatal, bool ignore_bom);
/* TextDecoder with a compile-time WHATWG legacy-encoding id. The frontend
* owns label canonicalization and emits only the ids understood by
* scr_bytes.c; keeping this separate from scr_text_decode means default
* UTF-8 users do not retain the legacy mapping tables. Borrows; +1. */
ScrStr *scr_text_decode_legacy(const ScrBytes *b, double encoding);
/* Buffer.from(string, enc): "utf8" copies the bytes; "hex" parses pairs
* and stops at the first invalid/odd tail (Node-lenient); "base64" and
* "base64url" decode the standard AND url-safe alphabets, skipping
* invalid bytes (Node-lenient); "latin1"/"ascii" write each UTF-16
* unit's low byte (astral code points contribute their two surrogates');
* "utf16le" writes LE code units, surrogate pairs included. Borrows
* both; +1 u8 result. Never throws. */
ScrBytes *scr_bytes_from_str(const ScrStr *s, const ScrStr *enc);
ScrBytes *scr_bytes_from_str_checked(const ScrStr *s, const ScrStr *enc);
/* Buffer.byteLength(string, enc) — enc NORMALIZED like from_str — and
* Buffer.isEncoding(name) (case-insensitive over Node's alias set).
* Borrow; never throw. */
double scr_bytes_byte_length_str(ScrStr *s, const ScrStr *enc);
double scr_buffer_byte_length_dyn(ScrDyn *value, ScrStr *enc);
bool scr_bytes_is_encoding(const ScrStr *s);
/* equals / compare / indexOf-lastIndexOf / fill / copy / swap / write on
* u8 bytes, Node-exact (corpus 1663). compare and copy take nargs — the
* count of PRESENT index args (defaults skip validation like Node);
* their ladders and fill/write's mirror validateOffset ('an integer',
* then '>= 0 && <= max' — Node spells '&&' here) and copy's C++ ladder
* (fractionals truncate without an error; targetStart/sourceEnd check only
* >= 0). indexOf coerces byteOffset (NaN = search everything) and steps
* by `align` (2 for utf16le needles — matches land on even offsets).
* fill repeats the pattern over [offset, end) and answers the RECEIVER
* (+1, chaining); the string form zero-fills on an empty pattern where
* the bytes form throws Node's TypeError. swap reverses w-byte groups in
* place (+1 receiver; ERR_INVALID_BUFFER_SIZE off the width). write
* encodes and truncates at unit boundaries (whole UTF-8 code points,
* even utf16le lengths), returning the bytes written. concat_len
* truncates/zero-pads the concatenation to the validated total. */
bool scr_bytes_equals(const ScrBytes *a, const ScrBytes *b);
double scr_bytes_compare(const ScrBytes *src, const ScrBytes *target, double nargs,
double ts, double te, double ss, double se);
/* Node's validateOffset ladder (buffer.js): non-integers (±Infinity and
* NaN included) throw the 'an integer' ERR_OUT_OF_RANGE RangeError, the
* rest the '>= 0 && <= max' render; max < 0 drops the upper bound.
* Returns false after arming the pending throw. */
bool scr_bytes_validate_off(const char *name, double value, double max);
/* The checked-dynamic compare/equals validators (scr_bytes_io.c): Node's
* argument ladder over dyn-boxed arguments — a non-bytes value throws
* ERR_INVALID_ARG_TYPE with the API's own argument name ("buf1"/"buf2",
* "otherBuffer", "target"), non-number offsets ERR_INVALID_ARG_TYPE
* "of type number", out-of-range numbers the validateOffset RangeError;
* an undefined offset takes its Node default. All arguments BORROWED. */
double scr_buffer_compare_chk(const ScrDyn *a, const ScrDyn *b);
ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding); /* +1 or NULL+pending; borrowed args */
bool scr_bytes_equals_chk(const ScrBytes *recv, const ScrDyn *other);
double scr_bytes_compare_chk(const ScrBytes *src, const ScrDyn *target,
const ScrDyn *ts, const ScrDyn *te,
const ScrDyn *ss, const ScrDyn *se);
/* new Buffer(number, encoding)'s string-arm type error (always throws;
* borrowed). */
ScrBytes *scr_buffer_new_string_fail(const ScrDyn *got);
/* fs._toUnixTimestamp over a dyn time value: numeric strings and finite
* numbers coerce (negatives answer now/1000), the rest throw Node's
* ERR_INVALID_ARG_TYPE. Borrowed. */
double scr_fs_to_unix_timestamp(const ScrDyn *t);
double scr_fs_timestamp_value(const ScrDyn *t, const char *name);
void scr_fs_utimes_checked(const ScrDyn *path, const ScrDyn *atime, const ScrDyn *mtime);
void scr_fs_lutimes_checked(const ScrDyn *path, const ScrDyn *atime, const ScrDyn *mtime);
void scr_fs_futimes_checked(const ScrDyn *fd, const ScrDyn *atime, const ScrDyn *mtime);
/* The fs argument-validation ladders (the fs.*Chk libCalls): Node-order
* validation over dyn values with Node's exact typed errors; a pass
* meets the real operation where one exists (mkdtempSync, macOS
* lchmodSync) or the compiler-rendered fence. All borrowed; the Chk
* forms without results always leave an exception pending. */
ScrDyn *scr_fs_exists_async(const ScrDyn *path, const ScrDyn *cb);
void scr_fs_mkdtemp_chk(const ScrDyn *prefix, const ScrDyn *cb, const ScrStr *fence);
ScrStr *scr_fs_mkdtemp_sync_chk(const ScrDyn *prefix, const ScrDyn *opts, const ScrStr *fence);
void scr_fs_read_file_chk(const ScrDyn *path, const ScrDyn *opts, const ScrDyn *cb, const ScrStr *fence);
void scr_fs_opendir_chk(const ScrDyn *path, const ScrDyn *opts, const ScrStr *fence);
void scr_fs_watch_file_chk(const ScrDyn *path, const ScrDyn *listener, const ScrStr *fence);
void scr_fs_lchmod_chk(const ScrDyn *path, const ScrDyn *mode, const ScrDyn *cb, const ScrStr *fence);
ScrDyn *scr_fs_lchmod_sync_chk(const ScrDyn *path, const ScrDyn *mode);
ScrPromise *scr_fsp_lchmod_chk(const ScrDyn *path, const ScrDyn *mode);
void scr_fs_read_chk(const ScrDyn *fd, const ScrDyn *buffer, const ScrDyn *offset,
const ScrDyn *length, const ScrDyn *position, const ScrStr *fence);
void scr_fs_stream_opts_chk(const ScrDyn *path, const ScrDyn *opts, const ScrStr *fence);
/* The checked-dynamic max-listeners ladders (scr_events_emitter.c). */
ScrEmitter *scr_emitter_set_max_chk(ScrEmitter *em, const ScrDyn *n);
void scr_emitter_set_default_max_chk(const ScrDyn *n, const ScrStr *name);
double scr_bytes_index_of(const ScrBytes *b, const ScrBytes *needle, double off, double align, bool fwd);
double scr_bytes_index_of_num(const ScrBytes *b, double v, double off, bool fwd);
ScrBytes *scr_bytes_fill(ScrBytes *b, const ScrBytes *pattern, double nargs, double offset, double end);
ScrBytes *scr_bytes_fill_num(ScrBytes *b, double v, double nargs, double offset, double end);
ScrBytes *scr_bytes_fill_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc,
double nargs, double offset, double end);
double scr_bytes_copy_into(const ScrBytes *src, ScrBytes *dst, double nargs,
double ts, double ss, double se);
ScrBytes *scr_bytes_swap(ScrBytes *b, double width);
double scr_bytes_write_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc,
double offset, double len, bool has_len);
ScrBytes *scr_bytes_concat_len(const ScrArr *list, double total);
/* Buffer.concat(list): list is a SCR_ELEM_BYTES array of u8 bytes values;
* the result is one fresh copy of their concatenation. Borrows the list.
* Never throws. */
ScrBytes *scr_bytes_concat(const ScrArr *list); /* +1 */
#endif /* SCR_BYTES_H */
+289
View File
@@ -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: <cmd>[\n<stderr>]"), spawn
* failure throws "spawnSync <file> ENOENT", a timeout SIGTERMs the child
* and throws "spawnSync <file> ETIMEDOUT" (messages only — Node's
* status/stdout/stderr error properties don't exist, SEMANTICS.md).
* shell=true runs /bin/sh -c (args must be ["-c", cmd]); has_input false
* means the input option is ABSENT (stdin from /dev/null — Node's model
* for an undefined member), true feeds the bytes to a stdin pipe ("" is
* the pipe with immediate EOF); cwd "" inherits; has_env false inherits
* environ, true REPLACES it with the [k,v,...] pairs; timeout_ms <= 0
* disables; stdout_mode 1 captures / 0 ignores / 2 inherits the parent's
* fd, plus bit 4 = the CHILD'S STDIN inherits (stdio "inherit"; the
* flag rides stdout_mode because the signature predates it); stderr_mode
* 0 captures AND echoes to the parent stderr after completion (Node's
* no-stdio-option default), 1 captures only, 2 ignores, 3 inherits.
* Result is the captured utf8 stdout (+1) — "" when nothing captures
* (Node answers null there; discarded results cannot tell). All args
* borrowed. */
ScrStr *scr_exec_sync(ScrStr *cmd, ScrArr *args, bool shell, ScrStr *input,
bool has_input, ScrStr *cwd, bool has_env, ScrArr *env_pairs,
double timeout_ms, double stdout_mode, double stderr_mode);
/* The promisified-execFile capture (cp.execCapture): the same core in the
* async shape — both streams captured (no echo), Node's ASYNC error
* messages on the throw paths ("Command failed: <cmd>\n<stderr>" with the
* unconditional newline, "spawn <file> <ERR>" with .code, timeouts as
* ordinary SIGTERM command failures). +1 ScrSpawnRes on success (stdout +
* stderr; status unused). All args borrowed. */
ScrSpawnRes *scr_exec_capture(ScrStr *cmd, ScrArr *args, ScrStr *cwd,
bool has_env, ScrArr *env_pairs, double timeout_ms);
ScrSpawnRes *scr_spawn_res_retain(ScrSpawnRes *r);
void scr_spawn_res_release(ScrSpawnRes *r);
void *scr_spawn_res_retain_v(void *p);
void scr_spawn_res_release_v(void *p);
bool scr_spawn_res_has_status(ScrSpawnRes *r);
double scr_spawn_res_status(ScrSpawnRes *r);
ScrStr *scr_spawn_res_stdout(ScrSpawnRes *r); /* +1 */
ScrStr *scr_spawn_res_stderr(ScrSpawnRes *r); /* +1 */
/* result.signal — the termination signal's name when a signal killed the
* child (a timeout's killSignal included), the null arm otherwise; the
* emitter wraps type-directedly over the has/get pair like status. */
bool scr_spawn_res_has_signal(ScrSpawnRes *r);
ScrStr *scr_spawn_res_signal(ScrSpawnRes *r); /* +1; has_signal only */
/* Node's `error` property: +1 %Error ("spawnSync <file> ENOENT", `code`
* stamped) when the spawn itself failed, NULL otherwise (the undefined
* arm — the emitter wraps type-directedly, the envGet convention). */
ScrError *scr_spawn_res_error(ScrSpawnRes *r);
/* argv construction shared by spawnSync and spawn: a NULL-terminated
* vector borrowing the strings (alive while cmd/args are); free() the
* vector only. */
char **scr_child_argv(ScrStr *cmd, ScrArr *args);
/* The asynchronous child: spawn starts it immediately (stdio all on
* /dev/null — the compiler fences everything but { stdio: "ignore" })
* and registers it with the event loop, which sweeps waitpid(WNOHANG) at
* quiescence (scr_children_poll) and fires listeners at reap; between
* turns the loop's sleep waits on a kqueue PROC filter, so an exit wakes
* it immediately (scr_children_wait; ~1ms polling is the fallback). "exit"
* fires once with the code (or none — signal death → the null arm);
* "error" fires ONLY for spawn failure, and with no listener registered
* it prints the error and exits 1 (Node's unhandled-'error' behavior).
* Listener callbacks MOVE in and are released when the child settles; a
* listener registered after settling is released and never fires. The
* adapters bridge the runtime's fixed firing ABI to the callback's
* compiled shape: exit_thunk0/err_thunk0 ignore the payload (zero-param
* listeners), err_thunk_error constructs the %Error instance, and the
* `(code: number | null)` shape is compiler-emitted (union tags are
* program data). */
typedef struct ScrChild ScrChild;
/* A piped child-output stream (child.stdout / child.stderr — stdio mode
* 3): refcounted like the child; 'data' fires one Buffer chunk per
* read(2) (≤64KB, consumer-driven — no listener, no read: the pipe is
* the buffer), 'end' fires once at EOF and always BEFORE the child's
* own 'exit' (Node's pinned ordering; the settle path drains first). A
* flowing stream (data consumer, not yet EOF) keeps the loop alive. */
typedef struct ScrChildStream ScrChildStream;
/* A piped child-input writer (child.stdin — stdio mode 3 on fd 0).
* write copies borrowed string/bytes data into a nonblocking queue and
* returns false once buffered data reaches the stream high-water mark;
* drain fires when that queue empties. end closes after queued data and
* then fires finish; destroy closes immediately without finish. Errors
* are asynchronous and use the child error adapters. */
typedef struct ScrChildWriter ScrChildWriter;
/* The data adapter: the chunk arrives BORROWED (multiple listeners see
* the same chunk); adapters retain what they keep. */
typedef void (*ScrChildStreamDataFn)(ScrClosure *cb, ScrBytes *chunk);
typedef void (*ScrChildStreamDataStrFn)(ScrClosure *cb, ScrStr *chunk);
/* signal_name: the termination signal's name (static storage) when a
* signal killed the child, NULL otherwise — Node's second exit-listener
* parameter; zero/one-param adapters ignore it. */
typedef void (*ScrChildExitFn)(ScrClosure *cb, bool has_code, double code,
const char *signal_name);
typedef void (*ScrChildErrFn)(ScrClosure *cb, ScrStr *msg);
/* execFile's fixed runtime firing ABI. error/stdout/stderr arrive as +1
* values and MOVE through the compiler-emitted adapter into the callback
* (or are released there when the callback omits a suffix). */
typedef void (*ScrExecFileFn)(ScrClosure *cb, ScrError *error,
ScrStr *stdout_value, ScrStr *stderr_value);
/* JSON IPC callback ABIs. Message values are borrowed checked-dynamic
* trees; send errors are +1 and move through the adapter (NULL = success). */
typedef void (*ScrIpcMessageFn)(ScrClosure *cb, ScrDyn *message);
typedef void (*ScrIpcSendFn)(ScrClosure *cb, ScrError *error);
ScrChild *scr_spawn(ScrStr *cmd, ScrArr *args); /* +1, never throws */
ScrChild *scr_fork(double target, ScrArr *args, double in_mode,
double out_mode, double err_mode, bool has_env,
ScrArr *env_pairs, ScrStr *cwd); /* +1 */
/* The options form (cp.spawnOpts): PER-SLOT stdio modes — 0 = ignore
* (/dev/null), 1 = inherit, 2 = fd (out/err only: out_fd/err_fd dup2
* into the child's slot, the openSync daemon-log idiom), 3 = pipe
* (stdin's write end becomes child.stdin; out/err read ends become the
* child.stdout/stderr streams);
* detached = POSIX_SPAWN_SETSID (own session/process group); shell runs
* the complete command through /bin/sh -c or cmd.exe /d /s /c; env
* REPLACES the child environment when has_env ([k,v,...] pairs); cwd ""
* = inherit. */
ScrChild *scr_spawn_opts(ScrStr *cmd, ScrArr *args, double in_mode,
double out_mode, double err_mode, double out_fd,
double err_fd, bool detached, bool shell, bool has_env,
ScrArr *env_pairs, ScrStr *cwd);
/* 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 */
+148
View File
@@ -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 */
+77
View File
@@ -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 */
+399
View File
@@ -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 <stdarg.h> /* va_list in the emitter listener adapters */
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h> /* memcpy in the inline slot accessors */
#include <sys/types.h> /* ssize_t in the transport ops table */
/* ── libc shims ─────────────────────────────────────────────────────────
* Win32's missing POSIX/BSD functions live in scr_win.c. Zig's musl sysroot
* additionally lacks arc4random_buf; scr_musl.c supplies it from Linux's
* getrandom syscall. Both files are selected by native-toolchain.ts only for their target. */
#if defined(_WIN32) && defined(_MSC_VER)
/* MSVC's C headers do not define POSIX ssize_t. Keep the runtime's byte-count
* interfaces pointer-sized on Windows without requiring a Windows SDK header
* (and leave the Zig/MinGW compatibility route's native typedef intact). */
typedef intptr_t ssize_t;
#endif
#ifdef _WIN32
#include <time.h> /* time_t / struct tm for the gmtime_r shim */
char *stpcpy(char *dst, const char *src);
void arc4random_buf(void *buf, size_t n);
struct tm *gmtime_r(const time_t *t, struct tm *out);
char *strcasestr(const char *hay, const char *needle);
#elif defined(SCR_MUSL)
void arc4random_buf(void *buf, size_t n);
#endif
/* ── 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 <errno.h>
#include <stdlib.h>
#include <sys/random.h>
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:
*
* <ret> (*)(void *ctx, <params...>)
*
* Registration is a pure store like the sink's (no entry prologue, no
* poison guard, legal before init); latest wins, NULL clears, and
* registrations persist across init/reset. While a host callback is active,
* its registration entry is rejected before name dispatch or a store, just
* like every runtime-touching ABI entry. Slots are per-copy of this
* state, exactly the sink's story: per-archive under abi.localize_runtime,
* per-thread instance under abi.instance_per_thread (SCR_TL) — a callback
* registered on thread T fires only for T's instance. The host's callback
* runs on the calling thread inside the entry's dynamic extent and must
* NOT call back into any library entry (exports, init, reset, collect, sink
* registration, or callback registration) or unwind/longjmp across library
* frames: read the borrowed buffers, copy what outlives the call, return.
* A re-entry is a detected SC4026 trap: it poisons only this library
* instance, delivers exactly once to the already-registered sink, names the
* attempted inner ABI symbol in structured field 2, then aborts if the sink
* returns. A later host-loop turn may enter normally after the callback has
* returned. Buffer parameters are borrowed for the duration of the call
* only. */
#define SCR_LIB_MAX_CALLBACKS 32 /* keep in step with LIB_MAX_CALLBACKS (library/library-profile.ts) */
/* The stored shape: generated call sites cast a slot's pointer to the
* channel's typed shape before calling. */
typedef void (*ScrLibCbFn)(void);
void scr_library_cb_set(size_t slot, ScrLibCbFn fn, void *ctx);
/* The call-site fetch: the registered pointer, or the funnel trap with
* trap_msg (never returns NULL). */
ScrLibCbFn scr_library_cb_require(size_t slot, const char *trap_msg);
void *scr_library_cb_ctx(size_t slot);
/* Generated typed call sites bracket only the actual host-function call.
* End is reached only after a normal return; an illegal unwind deliberately
* leaves the depth active so the next ABI entry is rejected. */
void scr_library_callback_begin(void);
void scr_library_callback_end(void);
/* Registration wrappers bypass scr_library_entry because their normal path
* is a pure store. They call this first so callback-time registration is
* rejected before dispatch, NULL handling, or mutation. */
void scr_library_callback_entry_guard(const char *entry_symbol);
/* Entry prologue: aborts deterministically when the library is poisoned (a
* trap already fired — no profile entry may run again; recovery is process
* restart). reset_arena additionally drops the result arena (the
* auto-reset posture, and the reset/collect entries' shared body).
* entry_symbol is the generated entry's external symbol exactly as the
* host linked it (a static string in the program TU): the prologue records
* it in the funnel's current-entry slot so a detected trap's structured
* message can name the trapping entry. A host callback's attempted nested
* entry is rejected first and replaces this slot with that inner symbol.
* Init and the mode entries (reset, collect) record theirs too. The two
* profile identity getters are the explicit pure-data exception: they touch
* no mutable runtime state and remain callable before init and after poison. */
void scr_library_entry(bool reset_arena, const char *entry_symbol);
void scr_library_arena_reset(void);
/* The mode-provided collect entry's body: arena reset + a full cycle
* collection (snapshot-invariant by construction — collection frees only
* unreachable cycles). */
void scr_library_collect(void);
/* Full session reset, called by the generated init entry AFTER the program
* TU released and zeroed its globals: pending-exception clear, arena
* reset, the reset registry's drains (the units' repointed atexit halves),
* the library's interned process values, a cycle collection, and — under
* SCR_RC_AUDIT — the zero-live-heap assertion (a failure is a trap through
* the sink, never _Exit). */
void scr_library_reset(void);
/* Where a unit would atexit() a lazy teardown, library builds register it
* here instead (called on every scr_library_reset, registered once). */
void scr_library_register_reset(void (*fn)(void));
/* An escaped exception at an entry boundary: renders the same "Uncaught
* ..." text the executable epilogue prints, releases the payload, and
* routes the text through the trap funnel. No-op when nothing is pending.
* The ratified verbatim rule: a thrown message that ALREADY begins with the
* structured marker 0x01 (a thrown string, or an Error whose .message
* starts with it) is delivered 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 */
+165
View File
@@ -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 */
+79
View File
@@ -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:<name>:start|end|asyncStart|asyncEnd|error), handled
* as a 1-based f64 exactly like Channel. subscribe/unsubscribe walk the
* handlers object's five event keys (truthy slots route to the per-channel
* add/remove — a non-function slot throws there, Node's order);
* unsubscribe answers Node's all-found conjunction. traceSync/traceCallback
* mirror Node's publish choreography over the checked-dynamic tree: ctx must be an OBJ (the
* lowering builds the default `{}`), fn/thisArg/args are dyn values (args
* an ARR of the call arguments); the traced call binds thisArg as the
* ambient receiver; result/error stamp onto ctx before end/error publish;
* a traced throw and a subscriber throw both propagate (MAY THROW). All
* arguments BORROWED; trace results +1 (or NULL with the exception
* pending). traceCallback wraps args[position] (JS-style negative
* indexing) in a native wrapper publishing error/result + asyncStart/
* asyncEnd around the original callback, throwing Node's
* ERR_INVALID_ARG_TYPE when the slot is not a function. */
double scr_dc_tracing_channel(ScrStr *name);
double scr_dc_tracing_channel_of(double start, double end, double async_start,
double async_end, double error);
double scr_dc_tc_channel(double h, double idx); /* 0=start..4=error */
bool scr_dc_tc_has_subscribers(double h);
void scr_dc_tc_subscribe(double h, ScrDyn *handlers);
bool scr_dc_tc_unsubscribe(double h, ScrDyn *handlers);
ScrDyn *scr_dc_tc_trace_sync(double h, ScrDyn *fn, ScrDyn *ctx, ScrDyn *this_arg, ScrDyn *args);
ScrDyn *scr_dc_tc_trace_callback(double h, ScrDyn *fn, double position, ScrDyn *ctx,
ScrDyn *this_arg, ScrDyn *args);
/* bindStore / runStores (the AsyncLocalStorage integration): per-store
* SET semantics (a re-bind replaces the transform; a non-function
* transform means identity); runStores enters every bound store with
* transform(data), publishes inside the entered contexts, runs fn with
* thisArg bound and arguments forwarded, and restores in reverse. The
* tracing choreography enters the start channel's stores around
* traceSync/traceCallback/tracePromise bodies and the asyncStart
* channel's around traceCallback's wrapped callback. */
void scr_dc_chan_bind_store(double handle, double als_id, ScrDyn *transform);
bool scr_dc_chan_unbind_store(double handle, double als_id);
ScrDyn *scr_dc_chan_run_stores(double handle, ScrDyn *data, ScrDyn *fn, ScrDyn *this_arg,
ScrDyn *args);
/* tracePromise: publishes start, runs fn (throw → error + end + rethrow,
* NULL result with the exception pending), wraps a non-promise result,
* publishes end, and returns the REACTION promise (+1) — a fiber that
* awaits the traced promise, stamps ctx.result/ctx.error, publishes
* asyncStart/asyncEnd (plus error first on rejection), and settles the
* result with the passed-through outcome. No-subscriber calls skip the
* choreography (Node's early exit) but still answer a promise. */
ScrPromise *scr_dc_tc_trace_promise(double h, ScrDyn *fn, ScrDyn *ctx, ScrDyn *this_arg,
ScrDyn *args);
#endif /* SCR_DC_H */
+72
View File
@@ -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 */
File diff suppressed because it is too large Load Diff
+266
View File
@@ -0,0 +1,266 @@
/* 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_ASSERTION = 9, /* node:assert's AssertionError — the Error layout */
SCR_ERR_COUNT = 10,
};
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 "<name> 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 */
+164
View File
@@ -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<type>` 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<type>` 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 */
+56
View File
@@ -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 */
+149
View File
@@ -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 */
+199
View File
@@ -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 <value>" to stderr (strings raw, numbers JS-exact, booleans
* true/false, ref payloads as "[object]"), and releases the payload. */
void scr_exc_print_uncaught(void);
/* Optional native process exception registry. Hook returns 1 when handled,
* 0 when unhandled, and -1 when a handler itself threw (Node exit 7). */
extern 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 */
+103
View File
@@ -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 */
+62
View File
@@ -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 */
+184
View File
@@ -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<IteratorResult>. */
ScrPromise *scr_async_gen_next_none(ScrGen *g);
ScrPromise *scr_async_gen_next_f64(ScrGen *g, double value);
ScrPromise *scr_async_gen_next_bool(ScrGen *g, bool value);
ScrPromise *scr_async_gen_next_ref(ScrGen *g, void *value,
void (*release)(void *));
ScrPromise *scr_async_gen_return_none(ScrGen *g);
ScrPromise *scr_async_gen_return_f64(ScrGen *g, double value);
ScrPromise *scr_async_gen_return_bool(ScrGen *g, bool value);
ScrPromise *scr_async_gen_return_ref(ScrGen *g, void *value,
void (*release)(void *));
ScrPromise *scr_async_gen_throw(ScrGen *g);
/* IN slot (consumer stores before resume; body takes after the yield). */
void scr_gen_in_f64(ScrGen *g, double v);
void scr_gen_in_bool(ScrGen *g, bool v);
void scr_gen_in_ref(ScrGen *g, void *v, void (*release)(void *)); /* moves */
void scr_gen_in_none(ScrGen *g);
double scr_gen_take_in_f64(void); /* body side: the running fiber's gen */
bool scr_gen_take_in_bool(void);
void *scr_gen_take_in_ref(void); /* +1 moved out; NONE → NULL */
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<Response handle> */
ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init); /* borrowed args; +1 Response handle or NULL pending */
ScrPromise *scr_fetch_response_json(ScrDyn *response); /* +1 promise<dyn> */
ScrPromise *scr_fetch_response_text(ScrDyn *response); /* +1 promise<dyn> */
ScrPromise *scr_fetch_response_array_buffer(ScrDyn *response); /* +1 promise<dyn> */
ScrPromise *scr_fetch_response_bytes(ScrDyn *response); /* +1 promise<dyn> */
ScrDyn *scr_fetch_abort_controller_new(void); /* +1 AbortController handle */
/* Borrowed number; +1 AbortSignal handle or NULL pending. */
ScrDyn *scr_fetch_abort_timeout(ScrDyn *delay);
ScrDyn *scr_fetch_abort_now(ScrDyn *reason); /* borrowed reason; +1 handle */
ScrDyn *scr_fetch_abort_any(ScrDyn *signals); /* borrowed array; +1 handle or NULL pending */
ScrDyn *scr_fetch_stream_new(ScrDyn *source); /* borrowed source; +1 handle or NULL pending */
ScrDyn *scr_fetch_stream_from(ScrDyn *iterable); /* borrowed iterable; +1 handle or NULL pending */
ScrDyn *scr_fetch_stream_from_array(
ScrArr *iterable,
ScrDyn *(*item)(ScrArr *, double)); /* borrowed array/callback; +1 handle */
ScrDyn *scr_fetch_stream_from_bytes(
ScrBytes *iterable); /* borrowed bytes; +1 handle */
ScrDyn *scr_fetch_stream_from_string(
ScrStr *iterable); /* borrowed string; +1 handle */
ScrPromise *scr_fetch_reader_read(ScrDyn *reader); /* borrowed handle; +1 */
#endif /* SCR_GENERATOR_H */
+349
View File
@@ -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 */
+207
View File
@@ -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 */
+128
View File
@@ -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); /* <Buffer aa bb ...>, 50-byte cap */
/* The stackless error form: [Name: message] / [Name], plus the stamped
* code slot as its one extra property ({ code: 'X' }); [Name] beyond
* depth when a code makes it composite. */
ScrStr *scr_insp_error(ScrError *e, double recurse, double depth);
/* The checked-dynamic tree: shape lives in the value, so the whole
* traversal is here. dyn-boxed bytes render in the checked-dynamic tree's documented
* Uint8Array identity. */
ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth);
/* format's %s / rest-args twin: dyn strings pass verbatim. */
ScrStr *scr_insp_dyn_s(ScrDyn *d, double depth);
void scr_insp_begin(double recurse);
void scr_insp_entry(ScrStr *s, bool is_num);
/* Circular references, Node-exact (<ref *N> / [Circular *N]): the
* compiler-emitted helpers over CYCLE-CAPABLE composites call circ_check
* FIRST (a value already on the traversal stack answers its circular id,
* assigned in discovery order — the caller renders scr_insp_circular and
* descends no further), seen_push after begin, and ref_wrap around end's
* result (pops the stack; values in the circular map gain the "<ref *N> "
* prefix). State resets at each top-level value's first frame
* (scr_insp_begin(1)). ref_wrap BORROWS s and returns +1. */
double scr_insp_circ_check(const void *v);
void scr_insp_seen_push(const void *v);
ScrStr *scr_insp_circular(double id);
ScrStr *scr_insp_ref_wrap(const void *v, ScrStr *s);
ScrStr *scr_insp_more_items(double remaining); /* "... N more items" */
ScrStr *scr_insp_key(ScrStr *k); /* bare-or-quoted property-name ladder; +1 */
ScrStr *scr_insp_end(ScrStr *base, ScrStr *b0, ScrStr *b1, double recurse,
bool array_extras, bool trailing_more);
/* 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 */
+135
View File
@@ -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 */
+416
View File
@@ -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.<NAME>: +1 fresh string when the variable is
* set, NULL when absent. The compiler wraps the result into the interned
* `string | undefined` union type-directedly (the runtime knows no tags);
* the name is borrowed. Never throws. */
ScrStr *scr_env_get(const ScrStr *name);
/* setenv(3) behind process.env.NAME = v: later env reads and spawned
* children observe the write, like Node. Both borrowed. Never throws. */
void scr_env_set(const ScrStr *name, const ScrStr *value);
void scr_env_unset(const ScrStr *name); /* delete process.env.NAME */
/* 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 */
+313
View File
@@ -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<K, V> with the compact-dict layout: a dense, insertion-ordered
* entries array (deletions become tombstones; compaction happens on growth,
* and NEVER while an iteration is active) plus an open-addressing bucket
* table of entry indices. That is what gives JS's observable semantics for
* free: forEach visits in insertion order, entries added during iteration
* are visited (they append), deleted entries are skipped (tombstones), and
* a delete + re-add moves the key to the end (Node-verified).
*
* 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 */
+342
View File
@@ -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 */
+150
View File
@@ -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 */
+34
View File
@@ -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 */
+81
View File
@@ -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<NetworkInterfaceInfo[]> record inline. Row
* selection and fields follow libuv/Node (IFF_UP && IFF_RUNNING,
* AF_INET/AF_INET6; internal = IFF_LOOPBACK; MAC from the link-level
* sibling entry, zeros when absent; cidr = address/<netmask prefix>, NULL
* for a split netmask — Node's null). String accessors return +1; a
* getifaddrs failure yields an empty snapshot. */
typedef struct ScrIfaddrs ScrIfaddrs;
ScrIfaddrs *scr_os_ifaddrs(void);
size_t scr_os_ifaddrs_count(const ScrIfaddrs *s);
ScrStr *scr_os_ifaddrs_name(const ScrIfaddrs *s, size_t i);
ScrStr *scr_os_ifaddrs_address(const ScrIfaddrs *s, size_t i);
ScrStr *scr_os_ifaddrs_netmask(const ScrIfaddrs *s, size_t i);
ScrStr *scr_os_ifaddrs_family(const ScrIfaddrs *s, size_t i); /* "IPv4"/"IPv6" */
ScrStr *scr_os_ifaddrs_mac(const ScrIfaddrs *s, size_t i);
bool scr_os_ifaddrs_internal(const ScrIfaddrs *s, size_t i);
bool scr_os_ifaddrs_ipv6(const ScrIfaddrs *s, size_t i);
ScrStr *scr_os_ifaddrs_cidr(const ScrIfaddrs *s, size_t i); /* +1 or NULL */
double scr_os_ifaddrs_scopeid(const ScrIfaddrs *s, size_t i);
void scr_os_ifaddrs_free(ScrIfaddrs *s);
#endif /* SCR_OS_H */
+65
View File
@@ -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 */
+120
View File
@@ -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 */
+493
View File
@@ -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 <stdlib.h>
#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 */
+113
View File
@@ -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<dyn>: 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 */
+98
View File
@@ -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 */
+111
View File
@@ -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 */
File diff suppressed because it is too large Load Diff
+340
View File
@@ -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 */
+151 -17
View File
@@ -3302,6 +3302,9 @@ static SCR_TL uint16_t scr_coll_ascii_primaries[128];
* the single-pass primary comparison. */
static SCR_TL bool scr_coll_ascii_one_pass;
static SCR_TL bool scr_coll_ascii_levels;
/* The covered ASCII bytes are exactly TAB..CR and 0x20..0x7E: the
* single-pass comparison then checks eight bytes at a time. */
static SCR_TL bool scr_coll_ascii_printable;
/* Code points below 0x180 that start a contraction, as a bit set; higher
* ones are looked up in scr_coll_contractions. */
#define SCR_COLL_BITS_LIMIT 0x180
@@ -3335,6 +3338,10 @@ static __attribute__((noinline)) void scr_coll_ascii_init(void) {
for (uint32_t c = 0; c < 128; c++) scr_coll_ascii_plain[c] = scr_coll_ascii_table[c].ok && !in_contraction[c];
scr_coll_ascii_one_pass = !zero_primary && !contraction;
scr_coll_ascii_levels = !contraction;
uint64_t printable[2] = {0, 0};
for (uint32_t c = 0; c < 128; c++)
if ((c >= 0x09 && c <= 0x0d) || (c >= 0x20 && c <= 0x7e)) printable[c >> 6] |= UINT64_C(1) << (c & 63);
scr_coll_ascii_printable = scr_coll_ascii_mask[0] == printable[0] && scr_coll_ascii_mask[1] == printable[1];
for (uint32_t c = 0; c < SCR_COLL_SCAN_LIMIT; c++) {
const ScrCollEntry *entry = scr_coll_lookup(c);
uint8_t klass = entry == NULL || entry->n == 255 ? 2 : 0;
@@ -3537,7 +3544,7 @@ static bool scr_coll_ascii_tail(const unsigned char *p, size_t from, size_t len)
* over the rest of both strings, because an uncovered byte anywhere moves
* the whole comparison to code-point order. Returns -1 or 1, 0 when the
* primaries are all equal, or 2 when a byte is not covered. */
static int scr_coll_ascii_primary(const ScrStr *a, const ScrStr *b) {
static int scr_coll_ascii_primary_bytes(const ScrStr *a, const ScrStr *b) {
const unsigned char *x = (const unsigned char *)a->data, *y = (const unsigned char *)b->data;
size_t n = a->len, m = b->len, k = n < m ? n : m, i = 0;
int r = 0;
@@ -3558,6 +3565,104 @@ static int scr_coll_ascii_primary(const ScrStr *a, const ScrStr *b) {
return r;
}
#define SCR_COLL_ONES UINT64_C(0x0101010101010101)
#define SCR_COLL_HIGHS (SCR_COLL_ONES * 0x80)
/* The 0 < n < 8 bytes at offset i of s's data in one word, the other lanes
* zero. On little-endian targets: the eight bytes ending at the last of
* them, shifted down; they start in the string's header when i + n < 8, so
* the load stays inside the object. Elsewhere: fixed-size loads (a
* variable-size memcpy is a library call). */
_Static_assert(offsetof(ScrStr, data) >= 8, "a short load may start in the header");
static inline uint64_t scr_coll_partial(const ScrStr *s, size_t i, size_t n) {
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
uint64_t v;
memcpy(&v, (const unsigned char *)s + offsetof(ScrStr, data) + i + n - 8, 8);
return v >> (8 * (8 - n));
#else
const unsigned char *p = (const unsigned char *)s->data + i;
uint64_t w = 0;
size_t o = 0;
if (n & 4) {
uint32_t v;
memcpy(&v, p, 4);
w = v;
o = 4;
}
if (n & 2) {
uint16_t v;
memcpy(&v, p + o, 2);
w |= (uint64_t)v << (8 * o);
o += 2;
}
if (n & 1) w |= (uint64_t)p[o] << (8 * o);
return w;
#endif
}
/* Up to eight bytes at offset i of s's data (n > 0 of them) as a word,
* padded with spaces (a covered byte). */
static inline uint64_t scr_coll_word(const ScrStr *s, size_t i, size_t n) {
uint64_t w;
if (n >= 8) {
memcpy(&w, s->data + i, 8);
return w;
}
return scr_coll_partial(s, i, n) | (SCR_COLL_ONES * 0x20) << (8 * n);
}
/* Whether a byte of w is outside 0x20..0x7E: below 0x20, DEL, or non-ASCII. */
static inline bool scr_coll_word_unusual(uint64_t w) {
return (((w - SCR_COLL_ONES * 0x20) & ~w) | (w + SCR_COLL_ONES) | w) & SCR_COLL_HIGHS;
}
/* 0 when s's bytes from offset `from` on are all 0x20..0x7E, 2 when one of
* them is non-ASCII, 1 otherwise. */
static inline int scr_coll_printable_tail(const ScrStr *s, size_t from) {
for (size_t i = from; i < s->len; i += 8) {
uint64_t w = scr_coll_word(s, i, s->len - i);
if (w & SCR_COLL_HIGHS) return 2;
if (scr_coll_word_unusual(w)) return 1;
}
return 0;
}
/* scr_coll_ascii_primary_bytes eight bytes at a time when the covered ASCII
* bytes are TAB..CR and 0x20..0x7E (scr_coll_ascii_printable): words of
* printable bytes need no per-byte check, and equal words have equal
* primaries. A non-ASCII byte anywhere answers 2 at once; a word with any
* other byte outside 0x20..0x7E (a tab, say) sends the comparison to the
* per-byte pass. */
static int scr_coll_ascii_primary(const ScrStr *a, const ScrStr *b) {
if (!scr_coll_ascii_printable) return scr_coll_ascii_primary_bytes(a, b);
const unsigned char *x = (const unsigned char *)a->data, *y = (const unsigned char *)b->data;
size_t n = a->len, m = b->len, k = n < m ? n : m, i = 0;
int r = 0;
for (; i < k; i += 8) {
uint64_t wx = scr_coll_word(a, i, k - i), wy = scr_coll_word(b, i, k - i);
if ((wx | wy) & SCR_COLL_HIGHS) return 2; /* a non-ASCII byte */
if (scr_coll_word_unusual(wx) || scr_coll_word_unusual(wy)) return scr_coll_ascii_primary_bytes(a, b);
if (wx == wy) continue;
size_t end = k - i < 8 ? k : i + 8;
for (size_t j = i; j < end; j++) {
unsigned pc = scr_coll_ascii_primaries[x[j]], pd = scr_coll_ascii_primaries[y[j]];
if (pc != pd) {
r = pc < pd ? -1 : 1;
break;
}
}
if (r != 0) {
i = end;
break;
}
}
if (i > k) i = k;
int tail = scr_coll_printable_tail(a, i) | scr_coll_printable_tail(b, i);
if (tail != 0) return tail & 2 ? 2 : scr_coll_ascii_primary_bytes(a, b);
if (r == 0 && n != m) r = n < m ? -1 : 1;
return r;
}
static bool scr_coll_ascii_covered(const ScrCollAscii *table, const ScrStr *s) {
const unsigned char *p = (const unsigned char *)s->data;
bool ok = true;
@@ -3585,22 +3690,28 @@ static int scr_coll_ascii_level(const ScrCollAscii *table, const ScrStr *a, cons
}
}
double scr_str_locale_compare(ScrStr *a, ScrStr *b) {
/* Equal bytes have equal elements at every level. */
if (a == b || (a->len == b->len && memcmp(a->data, b->data, a->len) == 0)) return 0;
const ScrCollAscii *table = scr_coll_ascii();
if (scr_coll_ascii_one_pass) {
int r = scr_coll_ascii_primary(a, b);
if (r != 2) {
for (int level = 1; level < 3 && r == 0; level++) r = scr_coll_ascii_level(table, a, b, level);
return (double)r;
}
} else if (scr_coll_ascii_levels && scr_coll_ascii_covered(table, a) &&
scr_coll_ascii_covered(table, b)) {
int r = 0;
for (int level = 0; level < 3 && r == 0; level++) r = scr_coll_ascii_level(table, a, b, level);
return (double)r;
}
/* Whether two strings have the same bytes; strings of up to 16 bytes
* compare as two words. */
static inline bool scr_coll_same_bytes(const ScrStr *a, const ScrStr *b) {
size_t n = a->len;
if (n != b->len) return false;
const unsigned char *x = (const unsigned char *)a->data, *y = (const unsigned char *)b->data;
if (n > 16) return memcmp(x, y, n) == 0;
if (n < 8) return n == 0 || scr_coll_partial(a, 0, n) == scr_coll_partial(b, 0, n);
uint64_t x0, y0, x1, y1;
memcpy(&x0, x, 8);
memcpy(&y0, y, 8);
memcpy(&x1, x + n - 8, 8);
memcpy(&y1, y + n - 8, 8);
return x0 == y0 && x1 == y1;
}
/* Strings outside the ASCII paths: a scan finds uncovered characters
* (code-point order) and characters that need element lists. Out of line,
* so the ASCII paths do not set up the element buffers' stack frame. */
static __attribute__((noinline)) double scr_coll_compare_lists(const ScrCollAscii *table, ScrStr *a,
ScrStr *b) {
if (scr_coll_same_bytes(a, b)) return 0;
int scan_a = scr_coll_scan(a);
if (scan_a == 2) return scr_str_cmp(a, b) < 0 ? -1 : 1; /* bytes differ (checked above) */
int scan_b = scr_coll_scan(b);
@@ -3628,3 +3739,26 @@ double scr_str_locale_compare(ScrStr *a, ScrStr *b) {
if (eb.e != eb.inline_buf) free(eb.e);
return (double)r;
}
/* Equal bytes have equal elements at every level, so each path checks for
* them before comparing secondary weights; the single-pass ASCII path does
* so only after the primaries tie. */
double scr_str_locale_compare(ScrStr *a, ScrStr *b) {
if (a == b) return 0;
const ScrCollAscii *table = scr_coll_ascii();
if (scr_coll_ascii_one_pass) {
int r = scr_coll_ascii_primary(a, b);
if (r != 2) {
if (r == 0 && scr_coll_same_bytes(a, b)) return 0;
for (int level = 1; level < 3 && r == 0; level++) r = scr_coll_ascii_level(table, a, b, level);
return (double)r;
}
} else if (scr_coll_ascii_levels && scr_coll_ascii_covered(table, a) &&
scr_coll_ascii_covered(table, b)) {
if (scr_coll_same_bytes(a, b)) return 0;
int r = 0;
for (int level = 0; level < 3 && r == 0; level++) r = scr_coll_ascii_level(table, a, b, level);
return (double)r;
}
return scr_coll_compare_lists(table, a, b);
}
+264
View File
@@ -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 */
+255
View File
@@ -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 */
+31
View File
@@ -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 */
+45
View File
@@ -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 */
+136
View File
@@ -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 */
+91
View File
@@ -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 */
+112
View File
@@ -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 */
+38
View File
@@ -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 */
+25
View File
@@ -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 */
+68
View File
@@ -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(
[],
);
});
+7 -1
View File
@@ -30,7 +30,9 @@ export const fileExclusions = [
// Pull requests run the full suite on Linux. Their macOS lane keeps the
// darwin clang/libc/kqueue oracles: the Node differential corpus, plus every
// file whose assertions run only on darwin or take darwin-specific branches.
// file whose assertions run only on darwin or take darwin-specific branches
// (including the Mach-O library localization arm and the arm64 runtime
// string oracle).
// Pushes to main and the nightly schedule still run the full suite on macOS.
export const darwinCorpusFiles = ["tests/harness/differential.test.ts"];
export const darwinFiles = [
@@ -42,9 +44,13 @@ export const darwinFiles = [
"packages/compiler/test/native-codegen-integration.test.ts",
"packages/compiler/test/source-debugging.test.ts",
"packages/runtime-darwin-arm64/test/archive.test.ts",
"packages/runtime/test/string.test.ts",
"tests/harness/ffi-callback-pool.test.ts",
"tests/harness/ffi-catalog.test.ts",
"tests/harness/ffi-frameworks.test.ts",
"tests/harness/library-callbacks.test.ts",
"tests/harness/library-dispatch.test.ts",
"tests/harness/library-multi.test.ts",
"tests/harness/native-object-example.test.ts",
"tests/harness/runtime-tree-shaking.test.ts",
"tests/harness/terminal-geometry.test.ts",
+255
View File
@@ -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 <rev>] [diff-file]
* Reads the diff from stdin when no file is given. Module headers come from
* the working tree, or from <rev> 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 <rev>] [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;
@@ -0,0 +1,35 @@
// localeCompare over ASCII strings of every length from 0 to 24 (across
// the eight-byte words the runtime compares at a time): a case, letter,
// digit or punctuation difference at each position, a tab, newline or
// carriage return at each position, and an accented letter at each
// position, against Node.
function sign(x: number): number {
return x < 0 ? -1 : x > 0 ? 1 : 0;
}
const base = "abcdefghijklmnopqrstuvwxyz";
const swaps = ["A", "b", "0", "-", " ", "z", "\t", "\n", "\r", "é", "É"];
let out = "";
let equal = 0;
for (let n = 0; n <= 24; n++) {
const s = base.slice(0, n);
for (let i = 0; i <= n; i++) {
for (const w of swaps) {
const t = s.slice(0, i) + w + s.slice(i + 1);
const u = s.slice(0, i) + w + s.slice(i);
for (const [x, y] of [[s, t], [t, s], [s, u], [u, s], [t, u], [t.toUpperCase(), t]]) {
const r = sign(x!.localeCompare(y!));
out += r + 1;
if (r === 0) equal++;
}
}
}
}
let hash = 2166136261;
for (let i = 0; i < out.length; i++) hash = Math.imul(hash ^ out.charCodeAt(i), 16777619) >>> 0;
console.log(out.length, equal, hash.toString(16));
// Equal strings built separately, and prefixes of each other.
const words = ["", "a", "ab", "abcdefg", "abcdefgh", "abcdefghi", "abcdefghijklmnop", "abcdefghijklmnopq"];
for (const w of words) {
const copy = (w + "x").slice(0, w.length);
console.log(JSON.stringify(w), w.localeCompare(copy), w.localeCompare(w + "a"), (w + "a").localeCompare(w), w.toUpperCase().localeCompare(copy));
}