mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
An untyped call of an array method went through the whole dispatch ladder on every call: the receiver-kind tests for handles, proxies, typed refs, objects, functions and primitives, then the array section's prologue (the hole scan and the frozen, sealed and extensibility tables) before the method itself. A dense array without holes, named properties or a prototype of its own, whose length and elements are writable, now runs push, pop, shift, at, includes and indexOf directly from the call site's cached method id. An argument that could run user code (a non-number index, a fromIndex) still takes the ladder, as does every other receiver. A statement-position push calls an entry that does not box the new length. Calls on arrays that the direct path refuses now also follow Node: - an array's own method, or one on its custom prototype chain (Object.setPrototypeOf), runs instead of the builtin; - push on a sealed or non-extensible array throws "Cannot add property N, object is not extensible", and pop or shift on a sealed array "Cannot delete property 'N' of [object Array]".
749 lines
38 KiB
C
749 lines
38 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,
|
|
/* An SCR_ELEM_REF type whose elements are compact class instances in the
|
|
* program-lifetime region (SCR_ARR_OPS_COMPACT): 4-byte slots holding the
|
|
* address over 8, 0 for NULL. Compact instances are never counted or
|
|
* traced, so these arrays retain, release and trace nothing. */
|
|
SCR_ELEM_CREF,
|
|
/* Numbers that the compiler proves are always int32 (never -0), in
|
|
* 4-byte slots: arrays that only compiled element accesses ever reach
|
|
* (backend/llvm int32 arrays). Every runtime slot is the value's double
|
|
* bits, as for SCR_ELEM_F64: scr_arr_dense_get and scr_arr_dense_put
|
|
* convert, so the paths compiled code calls need no case of their own. */
|
|
SCR_ELEM_I32,
|
|
} 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;
|
|
/* Registry id (scr_arr_ops_table), 0 until scr_arr_ops_register assigns
|
|
* it on the first array of the type. Descriptors are writable for it. */
|
|
uint32_t id;
|
|
/* SCR_ARR_OPS_*. On 64-bit targets the field takes the struct's former
|
|
* tail padding, so descriptors emitted without it read 0 here; on 32-bit
|
|
* targets it lies past their end and is never read (scr_arr_ops_compact). */
|
|
uint32_t flags;
|
|
} ScrArrOps;
|
|
|
|
/* The elements are compact class instances in the program-lifetime region:
|
|
* arrays of the type are SCR_ELEM_CREF (llvm/shapes.ts arrOps). */
|
|
#define SCR_ARR_OPS_COMPACT 1u
|
|
|
|
/* Whether `ops` describes compact class elements. Compact instances exist
|
|
* only on 64-bit targets. */
|
|
static inline bool scr_arr_ops_compact(const ScrArrOps *ops) {
|
|
#if UINTPTR_MAX > 0xffffffffu
|
|
return (ops->flags & SCR_ARR_OPS_COMPACT) != 0;
|
|
#else
|
|
(void)ops;
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
/* `release` does nothing: the element type's instances live until the
|
|
* process exits (llvm/shapes.ts arrOps). Destroying such an array frees only
|
|
* its storage. */
|
|
#define SCR_ARR_OPS_RELEASE_NOTHING 2u
|
|
|
|
/* Whether `ops` has SCR_ARR_OPS_RELEASE_NOTHING. 32-bit descriptors end
|
|
* before the flags word, so they never have it. */
|
|
static inline bool scr_arr_ops_release_nothing_flag(const ScrArrOps *ops) {
|
|
#if UINTPTR_MAX > 0xffffffffu
|
|
return (ops->flags & SCR_ARR_OPS_RELEASE_NOTHING) != 0;
|
|
#else
|
|
(void)ops;
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
typedef struct ScrArr {
|
|
size_t rc; /* SIZE_MAX = immortal (unused for arrays; kept per convention) */
|
|
/* Length and dense capacity are at most SCR_ARR_MAX_LENGTH (2^32 - 1);
|
|
* sparse entries cover indices >= cap. */
|
|
uint32_t len;
|
|
uint32_t 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;
|
|
/* The low SCR_ARR_OPS_SHIFT bits: storage flags (SCR_ARR_INLINE_SLOTS,
|
|
* SCR_ARR_HEAD_ROOM, SCR_ARR_UNSHIFTED, SCR_ARR_HAS_EXT, SCR_ARR_STATES)
|
|
* and SCR_ARR_FROZEN. The rest: the
|
|
* registry id of the element type's ops (scr_arr_ops), 0 for every kind
|
|
* but SCR_ELEM_REF and SCR_ELEM_CREF. */
|
|
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;
|
|
/* Dense values: `cap` 8-byte slots, `cap` bytes for SCR_ELEM_BOOL or
|
|
* `cap` 4-byte slots for SCR_ELEM_CREF and SCR_ELEM_I32
|
|
* (scr_arr_slot_width), followed in the same allocation, when the array
|
|
* has states (SCR_ARR_STATES), by one state byte per slot in reverse slot
|
|
* order (scr_arr_states_end). Sparse entries and properties keep 8-byte
|
|
* slots for every kind; a bool slot is 0 or 1 wherever it lives, a CREF
|
|
* slot's 8-byte form is the pointer and an I32 slot's the double's bits
|
|
* (scr_arr_dense_get). Rarely
|
|
* used storage (sparse indices, noncanonical numeric properties,
|
|
* match-result metadata) lives out of line, and an array that has it
|
|
* (SCR_ARR_HAS_EXT) keeps a pointer to it in the word before the dense
|
|
* allocation (scr_arr_ext_of). */
|
|
uint64_t *data;
|
|
} 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;
|
|
/* Heterogeneous named properties of match results. Until a result's
|
|
* metadata is first needed, this is the match source handle that the
|
|
* results of one exec or matchAll call share (SCR_DYNH_MATCH_SOURCE,
|
|
* scr_regex.c), and match_index is the result's UTF-16 start index. */
|
|
struct ScrDyn *metadata;
|
|
double match_index;
|
|
/* The element ops of an array whose ops have no registry id
|
|
* (SCR_ARR_OPS_OVERFLOW). */
|
|
const ScrArrOps *ops;
|
|
} 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 len and
|
|
* cap (fields 1 and 2, i32), spare (field 5, i16, for SCR_ARR_STATES),
|
|
* filled (field 6, i32) and data (field 7), and finds the state of slot i
|
|
* of an array with states at data + cap * (slot width + 1) - 1 - i. */
|
|
_Static_assert(offsetof(ScrArr, len) == sizeof(size_t) && sizeof(((ScrArr *)0)->len) == 4,
|
|
"emitted length reads load ScrArr.len as %ScrArr field 1, an i32");
|
|
_Static_assert(offsetof(ScrArr, cap) == sizeof(size_t) + 4 && sizeof(((ScrArr *)0)->cap) == 4,
|
|
"emitted capacity reads load ScrArr.cap as %ScrArr field 2, an i32");
|
|
_Static_assert(offsetof(ScrArr, cyc) == sizeof(size_t) + 9,
|
|
"inline RC fast paths read ScrArr.cyc as %ScrArr field 4");
|
|
_Static_assert(offsetof(ScrArr, spare) == sizeof(size_t) + 10,
|
|
"inline element access reads ScrArr.spare as %ScrArr field 5");
|
|
_Static_assert(offsetof(ScrArr, filled) == sizeof(size_t) + 12,
|
|
"inline element access reads ScrArr.filled as %ScrArr field 6");
|
|
_Static_assert(offsetof(ScrArr, data) == sizeof(size_t) + 16,
|
|
"inline element reads use ScrArr.data as %ScrArr field 7");
|
|
_Static_assert(sizeof(ScrArr) == sizeof(size_t) + 16 + sizeof(void *),
|
|
"ScrArr is 32 bytes on 64-bit targets");
|
|
|
|
/* Storage flags in `spare`. Inline slots: the dense slots live in the
|
|
* array's own block, after the header. Head room: `data` has advanced past
|
|
* vacated slots of its allocation, and the slot before `data` holds their
|
|
* count. Unshifted: an unshift had to move every element. Has ext: the word
|
|
* before the dense allocation (head room included) points at the array's
|
|
* ScrArrExt; such an array always has an allocation of its own, without
|
|
* inline slots. States: the dense slots carry state bytes (see below). */
|
|
#define SCR_ARR_INLINE_SLOTS 1u
|
|
#define SCR_ARR_HEAD_ROOM 2u
|
|
#define SCR_ARR_UNSHIFTED 4u
|
|
#define SCR_ARR_HAS_EXT 8u
|
|
#define SCR_ARR_STATES 16u
|
|
/* Object.freeze of the array (scr_arr_freeze). Emitted guards test it
|
|
* before the writes of array types a program may freeze; the runtime's own
|
|
* writes assume it is clear. */
|
|
#define SCR_ARR_FROZEN 32u
|
|
|
|
/* An array starts without state bytes. Without them, every index below
|
|
* `len` is a present value in dense storage: len <= cap, there are no
|
|
* sparse entries, and `filled` equals `len` whenever emitted code can read
|
|
* the array (its inline reads load a state only at or past `filled`). The
|
|
* first hole or present `undefined` below the length,
|
|
* a length set past the elements, or a sparse entry gives the array its
|
|
* states (scr_arr_add_states), which it keeps. */
|
|
static inline bool scr_arr_has_states(const ScrArr *a) { return (a->spare & SCR_ARR_STATES) != 0; }
|
|
|
|
static inline bool scr_arr_frozen(const ScrArr *a) { return (a->spare & SCR_ARR_FROZEN) != 0; }
|
|
/* Sets SCR_ARR_FROZEN; immortal arrays are left as they are. */
|
|
void scr_arr_freeze(ScrArr *a);
|
|
bool scr_arr_is_frozen(const ScrArr *a);
|
|
/* Throws Node's TypeError for the first write that the array operation
|
|
* `op` makes to a frozen array of length `len`, or answers false when the
|
|
* operation writes nothing. op: 0 index write (x = index), 1 push (y =
|
|
* argument count), 2 pop, 3 length write, 4 unshift (y = argument count),
|
|
* 5 delete (x = index), 6 shift, 7 reverse or sort, 8 fill (x = start, y =
|
|
* end), 9 copyWithin (x = target, y = start, z = end), 10 splice (x =
|
|
* start, y = delete count, z = item count). */
|
|
bool scr_array_frozen_error(int op, size_t len, double x, double y, double z);
|
|
/* Node's TypeErrors for a write that a non-extensible or sealed array
|
|
* refuses: adding the element at `index`, or deleting it. */
|
|
void scr_array_not_extensible_error(double index);
|
|
void scr_array_undeletable_error(double index);
|
|
|
|
/* The start of the dense slots' allocation: `data` less any head room. */
|
|
static inline uint64_t *scr_arr_block(const ScrArr *a) {
|
|
return (a->spare & SCR_ARR_HEAD_ROOM) ? a->data - (size_t)a->data[-1] : a->data;
|
|
}
|
|
|
|
/* Bytes before the dense allocation of an array with out-of-line storage:
|
|
* its pointer, in a word that keeps the 8-byte slots aligned on every
|
|
* target. */
|
|
#define SCR_ARR_EXT_PREFIX sizeof(uint64_t)
|
|
|
|
/* The out-of-line storage of `a`, or NULL while it has none. */
|
|
static inline struct ScrArrExt *scr_arr_ext_of(const ScrArr *a) {
|
|
if (!(a->spare & SCR_ARR_HAS_EXT)) return NULL;
|
|
struct ScrArrExt *x;
|
|
memcpy(&x, (const char *)scr_arr_block(a) - SCR_ARR_EXT_PREFIX, sizeof x);
|
|
return x;
|
|
}
|
|
|
|
/* Element ops by registry id: `spare >> SCR_ARR_OPS_SHIFT` of an array.
|
|
* Id 0 is no ops; ids are assigned on each descriptor's first array, in
|
|
* any thread. A program with more element types than the field holds keeps
|
|
* the ops of the rest in the array's ext (SCR_ARR_OPS_OVERFLOW). */
|
|
#define SCR_ARR_OPS_SHIFT 6
|
|
#define SCR_ARR_OPS_OVERFLOW (UINT16_MAX >> SCR_ARR_OPS_SHIFT)
|
|
extern const ScrArrOps *scr_arr_ops_table[SCR_ARR_OPS_OVERFLOW];
|
|
/* By registry id: 1 when the descriptor has SCR_ARR_OPS_RELEASE_NOTHING.
|
|
* Written before the id is published; the overflow id stays 0. */
|
|
extern uint8_t scr_arr_ops_release_nothing[SCR_ARR_OPS_OVERFLOW + 1];
|
|
/* Assigns the id on a descriptor's first array; out of line, so the id
|
|
* load in scr_arr_ops_id stays the only cost on later arrays. */
|
|
__attribute__((cold, noinline)) uint32_t scr_arr_ops_register(ScrArrOps *ops);
|
|
|
|
/* The registry id an array of element ops `ops` stores (0 for NULL). */
|
|
static inline uint32_t scr_arr_ops_id(const ScrArrOps *ops) {
|
|
if (!ops) return 0;
|
|
uint32_t id = __atomic_load_n(&ops->id, __ATOMIC_ACQUIRE);
|
|
return id ? id : scr_arr_ops_register((ScrArrOps *)ops);
|
|
}
|
|
|
|
/* The element ops of an array whose ops have no registry id. */
|
|
const ScrArrOps *scr_arr_ops_overflow(const ScrArr *a);
|
|
|
|
/* The element ops of `a`: NULL for every kind but SCR_ELEM_REF and
|
|
* SCR_ELEM_CREF. */
|
|
static inline const ScrArrOps *scr_arr_ops(const ScrArr *a) {
|
|
uint32_t id = (uint32_t)a->spare >> SCR_ARR_OPS_SHIFT;
|
|
if (__builtin_expect(id == SCR_ARR_OPS_OVERFLOW, 0)) return scr_arr_ops_overflow(a);
|
|
return scr_arr_ops_table[id];
|
|
}
|
|
|
|
/* Bytes per dense value slot of element kind `elem`. */
|
|
static inline size_t scr_elem_slot_width(uint8_t elem) {
|
|
if (elem == SCR_ELEM_BOOL) return 1;
|
|
if (__builtin_expect(elem == SCR_ELEM_CREF || elem == SCR_ELEM_I32, 0)) return sizeof(uint32_t);
|
|
return sizeof(uint64_t);
|
|
}
|
|
|
|
/* Bytes per dense value slot. */
|
|
static inline size_t scr_arr_slot_width(const ScrArr *a) {
|
|
return scr_elem_slot_width(a->elem);
|
|
}
|
|
|
|
/* The dense states of an array with states, one byte per slot, share the
|
|
* values' allocation (inline slots included). They follow the `cap` value
|
|
* slots in reverse slot order: the state of slot i is the byte i + 1 before
|
|
* scr_arr_states_end(a). Removing slots from the front (head room: `data`
|
|
* advances, `cap` drops) leaves every remaining state at its address. The
|
|
* address is computed on integers, so a NULL `data` stays NULL. Arrays
|
|
* without states have no bytes there. */
|
|
static inline uint8_t *scr_arr_states_end(const ScrArr *a) {
|
|
return (uint8_t *)((uintptr_t)a->data + (uintptr_t)a->cap * (scr_arr_slot_width(a) + 1));
|
|
}
|
|
|
|
/* scr_arr_states_end for an array whose slots are known to be `width`
|
|
* bytes wide: a constant multiple, with no element-kind test. */
|
|
static inline uint8_t *scr_arr_states_end_w(const ScrArr *a, size_t width) {
|
|
return (uint8_t *)((uintptr_t)a->data + (uintptr_t)a->cap * (width + 1));
|
|
}
|
|
|
|
/* The state of dense slot `index` (< cap), given the end of the states. */
|
|
static inline uint8_t *scr_arr_state_in(const uint8_t *end, size_t index) {
|
|
return (uint8_t *)end - 1 - index;
|
|
}
|
|
|
|
static inline uint8_t *scr_arr_state_ptr(const ScrArr *a, size_t index) {
|
|
return scr_arr_state_in(scr_arr_states_end(a), index);
|
|
}
|
|
|
|
/* The states of dense slots [from, from + count) are contiguous, in reverse
|
|
* order; this is their first byte (the state of slot from + count - 1). */
|
|
static inline uint8_t *scr_arr_state_run(const ScrArr *a, size_t from, size_t count) {
|
|
return scr_arr_states_end(a) - from - count;
|
|
}
|
|
|
|
/* The state of dense slot `index` (< cap), with or without state bytes. */
|
|
static inline uint8_t scr_arr_dense_state(const ScrArr *a, size_t index) {
|
|
if (!scr_arr_has_states(a)) return index < a->len ? SCR_ARR_VALUE : SCR_ARR_HOLE;
|
|
return *scr_arr_state_ptr(a, index);
|
|
}
|
|
|
|
/* The dense states of `a` for a scan (scr_arr_state_in), or NULL when it
|
|
* has none: every index below its length is then a value. */
|
|
static inline const uint8_t *scr_arr_scan_states(const ScrArr *a) {
|
|
return scr_arr_has_states(a) ? scr_arr_states_end(a) : NULL;
|
|
}
|
|
|
|
/* Give `a` state bytes: present values below the length, holes above it.
|
|
* May move the dense slots. */
|
|
void scr_arr_add_states(ScrArr *a);
|
|
|
|
static inline void scr_arr_need_states(ScrArr *a) {
|
|
if (!scr_arr_has_states(a)) scr_arr_add_states(a);
|
|
}
|
|
/* A CREF slot's pointer, and the slot of a pointer (NULL or a compact
|
|
* instance below 32 GiB). */
|
|
static inline uint64_t scr_cref_decode(uint32_t ref) { return (uint64_t)ref << 3; }
|
|
static inline uint32_t scr_cref_encode(uint64_t slot) { return (uint32_t)(slot >> 3); }
|
|
|
|
/* The bits of `d` as a slot, and back. */
|
|
static inline uint64_t scr_arr_f64_slot(double d) {
|
|
uint64_t slot;
|
|
memcpy(&slot, &d, sizeof slot);
|
|
return slot;
|
|
}
|
|
|
|
static inline double scr_arr_slot_f64(uint64_t slot) {
|
|
double d;
|
|
memcpy(&d, &slot, sizeof d);
|
|
return d;
|
|
}
|
|
|
|
/* An int32 array's slot for a double: its stores are proven int32s that
|
|
* are not -0 (and a cleared slot is +0), so anything else is a compiler
|
|
* bug. */
|
|
static inline int32_t scr_arr_i32_of(uint64_t slot) {
|
|
double d = scr_arr_slot_f64(slot);
|
|
if (__builtin_expect(!(d >= -2147483648.0 && d <= 2147483647.0 && d == (double)(int32_t)d) ||
|
|
slot == UINT64_C(0x8000000000000000),
|
|
0))
|
|
__builtin_trap();
|
|
return (int32_t)d;
|
|
}
|
|
|
|
/* 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];
|
|
if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0))
|
|
return scr_cref_decode(((const uint32_t *)a->data)[index]);
|
|
if (__builtin_expect(a->elem == SCR_ELEM_I32, 0))
|
|
return scr_arr_f64_slot((double)((const int32_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 if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0))
|
|
((uint32_t *)a->data)[index] = scr_cref_encode(slot);
|
|
else if (__builtin_expect(a->elem == SCR_ELEM_I32, 0))
|
|
((int32_t *)a->data)[index] = scr_arr_i32_of(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),
|
|
* given the end of the states. 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 *states_end, size_t f, size_t end) {
|
|
const uint64_t values = 0x0101010101010101ull * SCR_ARR_VALUE;
|
|
while (end - f >= 8) {
|
|
uint64_t word;
|
|
memcpy(&word, states_end - f - 8, sizeof word);
|
|
if (word != values) break;
|
|
f += 8;
|
|
}
|
|
while (f < end && *scr_arr_state_in(states_end, 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 (!scr_arr_has_states(a)) f = end;
|
|
else if (f < end) f = scr_arr_value_run(scr_arr_states_end(a), 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);
|
|
/* An int32 array's storage as doubles, in place (SCR_ELEM_I32 to
|
|
* SCR_ELEM_F64); no-op for any other kind. */
|
|
void scr_arr_widen_i32(ScrArr *a);
|
|
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). With `states`, the array starts
|
|
* with states (for holes or present undefined). Returns +1. */
|
|
ScrArr *scr_arr_new_inline(ScrElemKind elem, size_t cap, bool states);
|
|
|
|
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 commit of a live array view calls this on `a`, the array converted
|
|
* from `view`, before `target` takes its storage: `a` takes the view's named
|
|
* (non-index) properties as its metadata and the noncanonical numeric
|
|
* properties of `target`, so neither is lost when the storage is swapped. */
|
|
void scr_arr_adopt_view_properties(ScrArr *a, struct ScrDyn *view, ScrArr *target);
|
|
/* True when `key` is the string spelling of a number that is not an array
|
|
* index ("-1", "1.5", "NaN", "Infinity"), or "undefined", with the number in
|
|
* *out (scr_arr_undefined_key for "undefined"). Arrays keep such properties
|
|
* in their noncanonical property table. */
|
|
bool scr_arr_numeric_key(const char *key, size_t len, double *out);
|
|
/* The number compiled code passes as the key of an element access whose
|
|
* index is undefined at run time: a NaN with its own payload, which the
|
|
* property table spells "undefined" (JavaScript names that property, not
|
|
* "NaN"). */
|
|
double scr_arr_undefined_key(void);
|
|
/* Whether the array still has the named or noncanonical numeric property
|
|
* `key`: `for...in` skips a key deleted before its turn. Borrows both. */
|
|
bool scr_arr_has_named_key(ScrArr *a, ScrStr *key);
|
|
/* The enumerable string keys `for...in` visits after the indices: the
|
|
* noncanonical numeric properties, then the named properties in insertion
|
|
* order. +1. */
|
|
ScrArr *scr_arr_named_keys(ScrArr *a);
|
|
/* Whether the array has properties besides its elements (noncanonical
|
|
* numeric keys, match metadata, named properties): util.inspect lists
|
|
* them after the elements. */
|
|
bool scr_arr_has_properties(const ScrArr *a);
|
|
/* Installed by scr_regex.c when it records a pending match source; this
|
|
* unit links without the regex runtime. Writes the pending metadata of
|
|
* `a` into its dynamic copy or materializes it, answering whether it
|
|
* wrote. */
|
|
extern SCR_TL bool (*scr_arr_match_copy)(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, and the elements of an array with head room (left
|
|
* by shift) to the start of theirs, 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);
|
|
/* The same append for Array.prototype.concat's fresh result: dense storage
|
|
* grows to exactly the combined length instead of geometrically. */
|
|
double scr_arr_concat_copy_exact(ScrArr *a, const ScrArr *src);
|
|
/* Exact dense capacity for an array literal about to copy `src` and then
|
|
* `plain` more elements (a spread position). */
|
|
void scr_arr_reserve_spread(ScrArr *a, const ScrArr *src, size_t plain);
|
|
/* Exact dense capacity for `n` elements of an array that is filled to that
|
|
* length next (Array.from({ length: n })); no effect for other lengths. */
|
|
void scr_arr_reserve_len(ScrArr *a, double n);
|
|
|
|
/* 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);
|
|
/* Default ordering of reference elements by their String() text, which
|
|
* `text` answers (+1) for each element once (Date arrays). Like
|
|
* scr_arr_sort_primitive otherwise. */
|
|
ScrArr *scr_arr_sort_by_text(ScrArr *a, bool copy, ScrStr *(*text)(void *));
|
|
/* 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. An `order` from 2 says `f`
|
|
* orders reference elements by several such keys, the first deciding: the
|
|
* low byte is the key count (2..8), each bit from bit 8 marks a key that
|
|
* orders descending, and `key` reads key j of an element (element, j ->
|
|
* number). */
|
|
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; the rest stay in place behind head room
|
|
* (amortized O(1)). 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 */
|