Files
scriptc/packages/runtime/src/scr_array.h
T
Malte Ubl c863eaa5a7 Speed up regex exec results and parseInt of numeric captures
exec/match results now allocate their slots inline with the array header
(scr_arr_new_inline): one allocation and one free per match instead of a
header plus a separately grown dense block. The capture fill loop keeps
the storage pointers in registers instead of reloading them after every
slot store.

parseInt(digits, 10) of a short all-digit string (1-15 digits, the shape
of numeric fields and regex captures) now converts directly. The general
path no longer divides per digit to detect u64 overflow (one bound per
call; the exact bignum fallback is unchanged) and skips fmod for integral
radixes in 2..36.

The new corpus program pins match-array mutation past the captured length
(push, length, unshift, sort, splice, reverse, slice) with index/input,
and parseInt across lengths, signs, prefixes, overflow and radix forms.
2026-10-15 04:46:38 -07:00

429 lines
22 KiB
C

/* 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);
/* scr_arr_new(elem, cap) for a known final length: the `cap` slots share
* the array's own block (one allocation). Returns +1. */
ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap);
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 */