Files
scriptc/packages/runtime/src/scr_map.c
T
Malte UblandClaude Opus 5.5 06b0bb2931 Write borrowed string slices into stack buffers
A slice read only by lending consumers (string equality and ordering,
length/charCodeAt/indexOf/includes/startsWith/endsWith, Number/unary
plus/parseInt/parseFloat, typed string-keyed Map/Set get/has/set/add/
delete keys, string switch discriminants, and string parameters their
callee only lends) is written by scr_str_slice_scratch into a per-site
stack buffer as an immortal ASCII ScrStr marked as scratch (cap < len),
instead of a heap allocation. Stable locals initialized by such a slice
qualify when every read lends. Map/Set insertion stores a heap copy of a
scratch key, and probes cache the key hash in the scratch header, so the
counting idiom allocates only for new keys. Longer, non-ASCII, and
interned results keep the owned slice; coroutines and debug builds are
unchanged. Slice bounds are normalized without a libm trunc call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:13:11 -07:00

1575 lines
64 KiB
C

/* ES Map<K, V> — the compact-dict design (see scr_runtime.h for the API
* contract): a dense, insertion-ordered entries array plus an open-addressing
* bucket table of entry indices. Deletions tombstone their entry (key and
* value released immediately; the bucket slot keeps pointing at the dead
* entry so probe chains stay intact); tombstones are compacted away when the
* entries array grows — but never while an iteration is active (iter_depth),
* which is what keeps the forEach desugar's plain indices stable under
* arbitrary mutation from the callback.
*
* Cycle capability is per-map, decided at construction: either key_trace
* or val_trace means that side carries a collector header and could point
* back at this map. Such maps allocate with a hidden cycle header, trace
* each live edge on the corresponding side, and release only the untraced
* complement during collector teardown. Maps with neither trace adapter
* keep the lean 1-word header and never touch the collector.
*/
#include "scr_runtime.h"
#include "scr_key.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Live heap-map count for the RC audit lane (-DSCR_RC_AUDIT); same contract
* as scr_str_live_count in scr_string.c. */
#ifdef SCR_RC_AUDIT
static SCR_TL long scr_live_maps = 0;
long scr_map_live_count(void) { return scr_live_maps; }
/* A published map or set is immortal: it is never freed, so it leaves the count. */
void scr_map_live_forget(void) { scr_live_maps--; }
#endif
#define SCR_MAP_EMPTY SIZE_MAX
static void scr_map_oom(void) {
scr_trap("scriptc: out of memory\n");
}
/* ── key normalization + hashing (SameValueZero) ───────────────────────
* -0 normalizes to +0 (JS Map stores the +0 key: [...m.keys()] shows 0
* after set(-0)) and every NaN collapses to one canonical bit pattern, so
* hashing the bit pattern IS SameValueZero hashing. */
static uint64_t scr_map_f64_bits(double k) {
if (k != k) return UINT64_C(0x7ff8000000000000); /* canonical NaN */
if (k == 0) k = 0; /* -0 -> +0 */
uint64_t bits;
memcpy(&bits, &k, sizeof bits);
return bits;
}
/* String keys hash to 32 bits so heap strings can cache the value in their
* header (see SCR_STR_HASH_CACHE). The widened form keeps independent bits
* for the bucket index (low half) and the compact fingerprint (top byte),
* and is never zero, so it doubles as a live-entry marker. Spans and
* strings share this function, so equal bytes always hash alike. */
static inline uint64_t scr_map_hash_widen(uint32_t h) {
return ((uint64_t)h << 32) | h;
}
static inline uint32_t scr_map_hash_fold(const char *bytes, size_t length) {
uint64_t h = scr_key_hash(bytes, length);
uint32_t folded = (uint32_t)(h ^ (h >> 32));
return folded ? folded : 1;
}
static inline uint64_t scr_map_hash_bytes(const char *bytes, size_t length) {
return scr_map_hash_widen(scr_map_hash_fold(bytes, length));
}
/* Heap strings remember their hash: repeated lookups with one key object
* (a stored neighbor, a parsed identifier, a get followed by a set) skip
* the byte walk. Immortal strings may live in read-only memory and are
* shared by every thread, so they are never written. */
static inline uint64_t scr_map_hash_str(const ScrStr *k) {
#if SCR_STR_HASH_CACHE
uint32_t h = k->hash;
if (__builtin_expect(h != 0, 1)) return scr_map_hash_widen(h);
h = scr_map_hash_fold(k->data, k->len);
/* A scratch slice is a writable stack copy (scr_str_is_scratch). */
if (k->rc != SIZE_MAX || scr_str_is_scratch(k)) ((ScrStr *)k)->hash = h;
return scr_map_hash_widen(h);
#else
return scr_map_hash_bytes(k->data, k->len);
#endif
}
/* Mix the complete normalized number or identity word. Float exponents
* and aligned pointers need high bits to reach the low bucket-index bits. */
static uint64_t scr_map_hash_word(uint64_t value) {
return scr_key_mix(value);
}
/* ── slot packing (8-byte slots, like ScrArr) ──────────────────────────── */
static uint64_t scr_map_slot_from_f64(double v) {
uint64_t s;
memcpy(&s, &v, sizeof s);
return s;
}
static double scr_map_slot_to_f64(uint64_t s) {
double v;
memcpy(&v, &s, sizeof v);
return v;
}
static uint64_t scr_map_slot_from_ptr(void *p) { return (uint64_t)(uintptr_t)p; }
static void *scr_map_slot_to_ptr(uint64_t s) { return (void *)(uintptr_t)s; }
/* Union boxes are a compiler representation, not JavaScript objects. Keep
* the box as the owned key, but hash and compare its reference payload. */
static uint64_t scr_map_identity(const ScrMap *m, uint64_t key) {
if (m->key_kind == SCR_MAP_KEY_UNION_REF) {
const ScrUnion *u = (const ScrUnion *)scr_map_slot_to_ptr(key);
return scr_map_slot_from_ptr(scr_union_peek(u));
}
return key;
}
/* Checked-dynamic keys use the JavaScript value, not the temporary box.
* Typed capsules preserve a native object's identity across repeated
* crossings; numbers retain SameValueZero and strings compare by content. */
static uint64_t scr_map_hash_dyn(const ScrDyn *d) {
uint64_t value;
uint64_t kind = (uint64_t)d->kind;
switch (d->kind) {
case SCR_DYN_UNDEF:
case SCR_DYN_NULL: value = 0; break;
case SCR_DYN_BOOL: value = d->v.b; break;
case SCR_DYN_NUM: value = scr_map_f64_bits(d->v.num); break;
case SCR_DYN_BIGINT: return scr_bigint_hash(d->v.bigint);
case SCR_DYN_STR: return scr_map_hash_str(d->v.str);
case SCR_DYN_SYMBOL: value = scr_map_slot_from_ptr(d->v.symbol.value); break;
case SCR_DYN_FUNC: value = scr_map_slot_from_ptr(d->v.fn.class_obj ? (void *)d->v.fn.class_obj : (void *)scr_closure_identity(d->v.fn.clo)); break;
case SCR_DYN_BYTES: value = scr_map_slot_from_ptr(d->v.bytes); break;
case SCR_DYN_HANDLE: value = scr_map_slot_from_ptr(d->v.handle.ptr); break;
case SCR_DYN_PROMISE: value = scr_map_slot_from_ptr(d->v.promise); break;
case SCR_DYN_OBJ: value = scr_map_slot_from_ptr(d->v.obj.source_identity ? d->v.obj.source_identity : (void *)d); break;
case SCR_DYN_TYPED_REF:
value = scr_map_slot_from_ptr(d->v.typed_ref.ptr);
// A materialized record and its capsule compare equal; their hashes
// must also agree when a key crosses the boundary in either form.
kind = SCR_DYN_OBJ;
break;
default: value = scr_map_slot_from_ptr((void *)d); break;
}
return scr_map_hash_word(value) ^ kind;
}
static bool scr_map_dyn_eq(const ScrDyn *a, const ScrDyn *b) {
if (a->kind == SCR_DYN_NUM && b->kind == SCR_DYN_NUM) {
return scr_map_f64_bits(a->v.num) == scr_map_f64_bits(b->v.num);
}
if (a->kind == SCR_DYN_TYPED_REF && b->kind == SCR_DYN_TYPED_REF) {
return a->v.typed_ref.ptr == b->v.typed_ref.ptr;
}
return scr_dyn_strict_eq(a, b);
}
static uint64_t scr_map_hash_union(const ScrMap *m, const ScrUnion *key) {
ScrMapKeyKind kind = (ScrMapKeyKind)m->ops->union_keys[key->tag];
switch (kind) {
case SCR_MAP_KEY_STR: return scr_map_hash_str(scr_union_peek(key));
case SCR_MAP_KEY_BIGINT: return scr_bigint_hash(scr_union_peek(key));
case SCR_MAP_KEY_F64: return scr_map_hash_word(scr_map_f64_bits(scr_map_slot_to_f64(key->slot)));
case SCR_MAP_KEY_NULL:
case SCR_MAP_KEY_UNDEFINED: return scr_map_hash_word((uint64_t)kind);
default: return scr_map_hash_word(key->slot);
}
}
static bool scr_map_union_eq(const ScrMap *m, const ScrUnion *a, const ScrUnion *b) {
ScrMapKeyKind kind = (ScrMapKeyKind)m->ops->union_keys[a->tag];
if (kind != (ScrMapKeyKind)m->ops->union_keys[b->tag]) return false;
switch (kind) {
case SCR_MAP_KEY_STR: return scr_str_eq(scr_union_peek(a), scr_union_peek(b));
case SCR_MAP_KEY_BIGINT: return scr_bigint_eq(scr_union_peek(a), scr_union_peek(b));
case SCR_MAP_KEY_F64:
return scr_map_f64_bits(scr_map_slot_to_f64(a->slot)) == scr_map_f64_bits(scr_map_slot_to_f64(b->slot));
case SCR_MAP_KEY_NULL:
case SCR_MAP_KEY_UNDEFINED: return true;
default: return a->slot == b->slot;
}
}
static uint64_t scr_map_hash_key(const ScrMap *m, uint64_t key) {
if (m->key_kind == SCR_MAP_KEY_STR) return scr_map_hash_str((ScrStr *)scr_map_slot_to_ptr(key));
if (m->key_kind == SCR_MAP_KEY_DYN) return scr_map_hash_dyn((ScrDyn *)scr_map_slot_to_ptr(key));
if (m->key_kind == SCR_MAP_KEY_BIGINT) return scr_bigint_hash(scr_map_slot_to_ptr(key));
if (m->key_kind == SCR_MAP_KEY_UNION_VALUE) return scr_map_hash_union(m, scr_map_slot_to_ptr(key));
uint64_t identity = scr_map_identity(m, key);
return scr_map_hash_word(identity);
}
/* Stored keys are pre-normalized, so bit equality IS SameValueZero for f64
* keys (probe keys normalize through the same function). */
static bool scr_map_key_eq(const ScrMap *m, uint64_t stored, uint64_t probe) {
if (m->key_kind == SCR_MAP_KEY_UNION_VALUE) {
return scr_map_union_eq(m, scr_map_slot_to_ptr(stored), scr_map_slot_to_ptr(probe));
}
if (m->key_kind == SCR_MAP_KEY_BIGINT) {
return scr_bigint_eq(scr_map_slot_to_ptr(stored), scr_map_slot_to_ptr(probe));
}
if (m->key_kind == SCR_MAP_KEY_DYN) {
return scr_map_dyn_eq((ScrDyn *)scr_map_slot_to_ptr(stored), (ScrDyn *)scr_map_slot_to_ptr(probe));
}
/* f64 keys are pre-normalized and REF keys are pointers, so bit equality
* IS the honest compare for both (SameValueZero; reference identity). */
if (m->key_kind != SCR_MAP_KEY_STR) return scr_map_identity(m, stored) == scr_map_identity(m, probe);
return scr_str_eq((ScrStr *)scr_map_slot_to_ptr(stored),
(ScrStr *)scr_map_slot_to_ptr(probe));
}
/* ── absent-key filter ─────────────────────────────────────────────────
* Every live entry's hash sets two bits of m->filter, so a lookup whose
* two bits are not both set is absent: most misses on small and medium
* maps return from the header without loading the bucket table or the
* entries. The bits come from hash bits 44..55, apart from the compact
* fingerprint (top byte) and from the bucket index of every table small
* enough for the filter to be selective. Deletions leave their bits (the
* filter only has to cover the live keys); compaction recomputes it from
* the survivors and clear empties it. */
static inline uint64_t scr_map_filter_bits(uint64_t hash) {
return (UINT64_C(1) << (hash >> 44 & 63)) | (UINT64_C(1) << (hash >> 50 & 63));
}
/* ── lookup ────────────────────────────────────────────────────────────
* Returns the matching bucket, or the empty bucket where a new key belongs.
* A zero entry hash marks a tombstone; cached hashes reject unrelated keys
* before equality and survive table growth without hashing the keys again.
* Remap zero to one so every live entry has a nonzero hash. */
typedef enum { SCR_MAP_LOOKUP_STR, SCR_MAP_LOOKUP_SPAN, SCR_MAP_LOOKUP_WORD, SCR_MAP_LOOKUP_GENERIC } ScrMapLookup;
typedef struct { const char *bytes; size_t length; } ScrMapSpan;
/* Choose equality once per operation. Inlining the traversal with a constant
* lookup kind keeps the ordinary string/word probe free of key-kind dispatch,
* while boxed keys retain their complete value/identity semantics. */
static inline bool scr_map_lookup_eq(const ScrMap *m, uint64_t stored, uint64_t key,
ScrMapLookup lookup) {
if (lookup == SCR_MAP_LOOKUP_WORD) return stored == key;
if (lookup == SCR_MAP_LOOKUP_SPAN) {
const ScrStr *a = scr_map_slot_to_ptr(stored);
const ScrMapSpan *b = scr_map_slot_to_ptr(key);
return a->len == b->length && scr_key_equal(a->data, b->bytes, a->len);
}
if (lookup == SCR_MAP_LOOKUP_STR) {
const ScrStr *a = scr_map_slot_to_ptr(stored);
const ScrStr *b = scr_map_slot_to_ptr(key);
return a == b || (a->len == b->len && scr_key_equal(a->data, b->data, a->len));
}
return scr_map_key_eq(m, stored, key);
}
/* Compact tables keep a hash fingerprint beside each 24-bit entry index.
* Rejecting a collision here avoids loading an unrelated ordered entry.
* Larger tables keep full-width indices; collection size remains limited
* only by the allocator. Zero is empty in compact tables, so store index+1.
* The LLVM backend's inline number-key probe (sc_map_entry_f64_hashed)
* reads this encoding for tables of at most 1 << 24 buckets, hashes keys
* as scr_map_find_f64 does, and calls scr_map_entry_f64 for wider tables;
* only the runtime unit test builds a smaller bound. */
#ifndef SCR_MAP_COMPACT_BUCKETS
#define SCR_MAP_COMPACT_BUCKETS ((size_t)1 << 24)
#endif
_Static_assert(SCR_MAP_COMPACT_BUCKETS >= 16 &&
SCR_MAP_COMPACT_BUCKETS <= ((size_t)1 << 24) &&
(SCR_MAP_COMPACT_BUCKETS & (SCR_MAP_COMPACT_BUCKETS - 1)) == 0,
"compact bucket capacity must leave room for every tagged index");
#define SCR_MAP_INDEX_MASK UINT32_C(0x00ffffff)
static bool scr_map_compact_buckets(size_t count) {
return count <= SCR_MAP_COMPACT_BUCKETS;
}
static size_t scr_map_bucket(const ScrMap *m, size_t slot) {
if (scr_map_compact_buckets(m->nbuckets)) {
uint32_t index = ((const uint32_t *)m->buckets)[slot] & SCR_MAP_INDEX_MASK;
return index ? (size_t)index - 1 : SCR_MAP_EMPTY;
}
return ((const size_t *)m->buckets)[slot];
}
static void scr_map_bucket_set(ScrMap *m, size_t slot, size_t entry, uint64_t hash) {
if (scr_map_compact_buckets(m->nbuckets)) {
((uint32_t *)m->buckets)[slot] = (uint32_t)(entry + 1) | (uint32_t)(hash >> 32 & UINT32_C(0xff000000));
} else {
((size_t *)m->buckets)[slot] = entry;
}
}
static inline __attribute__((always_inline)) size_t scr_map_probe_with(
const ScrMap *m, uint64_t hash, uint64_t key, ScrMapLookup lookup) {
if (m->nbuckets == 0) return SCR_MAP_EMPTY;
size_t mask = m->nbuckets - 1;
if (scr_map_compact_buckets(m->nbuckets)) {
const uint32_t *buckets = m->buckets;
uint32_t tag = (uint32_t)(hash >> 32 & UINT32_C(0xff000000));
for (size_t i = hash & mask;; i = (i + 1) & mask) {
uint32_t bucket = buckets[i];
if (!bucket) return i;
if ((bucket & ~SCR_MAP_INDEX_MASK) != tag) continue;
size_t b = (bucket & SCR_MAP_INDEX_MASK) - 1;
if (m->entries[b].hash == hash && scr_map_lookup_eq(m, m->entries[b].key, key, lookup)) return i;
}
}
const size_t *buckets = m->buckets;
for (size_t i = hash & mask;; i = (i + 1) & mask) {
size_t b = buckets[i];
if (b == SCR_MAP_EMPTY) return i;
if (m->entries[b].hash == hash && scr_map_lookup_eq(m, m->entries[b].key, key, lookup)) return i;
}
}
/* The entry index for a key, or SCR_MAP_EMPTY: the probe of
* scr_map_probe_with without returning to the bucket it stopped at. */
static inline __attribute__((always_inline)) size_t scr_map_probe_entry_with(
const ScrMap *m, uint64_t hash, uint64_t key, ScrMapLookup lookup) {
size_t mask = m->nbuckets - 1;
if (scr_map_compact_buckets(m->nbuckets)) {
const uint32_t *buckets = m->buckets;
uint32_t tag = (uint32_t)(hash >> 32 & UINT32_C(0xff000000));
for (size_t i = hash & mask;; i = (i + 1) & mask) {
uint32_t bucket = buckets[i];
if (!bucket) return SCR_MAP_EMPTY;
if ((bucket & ~SCR_MAP_INDEX_MASK) != tag) continue;
size_t b = (bucket & SCR_MAP_INDEX_MASK) - 1;
if (m->entries[b].hash == hash && scr_map_lookup_eq(m, m->entries[b].key, key, lookup)) return b;
}
}
const size_t *buckets = m->buckets;
for (size_t i = hash & mask;; i = (i + 1) & mask) {
size_t b = buckets[i];
if (b == SCR_MAP_EMPTY) return SCR_MAP_EMPTY;
if (m->entries[b].hash == hash && scr_map_lookup_eq(m, m->entries[b].key, key, lookup)) return b;
}
}
/* Up to this many entries a map keeps no bucket table. */
#define SCR_MAP_LINEAR_LIMIT 8
/* Small collections keep only their ordered entries. Cached hashes make
* the bounded scan cheap without allocating a separate bucket table. */
static inline __attribute__((always_inline)) size_t scr_map_find_with(
const ScrMap *m, uint64_t hash, uint64_t key, ScrMapLookup lookup) {
hash = hash ? hash : 1;
/* Also rejects every key of an empty map (its filter is zero). */
uint64_t bits = scr_map_filter_bits(hash);
if ((m->filter & bits) != bits) return SCR_MAP_EMPTY;
if (m->nbuckets == 0) {
for (size_t e = 0; e < m->nentries; e++) {
if (m->entries[e].hash == hash && scr_map_lookup_eq(m, m->entries[e].key, key, lookup)) return e;
}
return SCR_MAP_EMPTY;
}
return scr_map_probe_entry_with(m, hash, key, lookup);
}
static ScrMapLookup scr_map_lookup_kind(const ScrMap *m) {
switch (m->key_kind) {
case SCR_MAP_KEY_STR: return SCR_MAP_LOOKUP_STR;
case SCR_MAP_KEY_F64:
case SCR_MAP_KEY_BOOL:
case SCR_MAP_KEY_REF:
case SCR_MAP_KEY_NULL:
case SCR_MAP_KEY_UNDEFINED: return SCR_MAP_LOOKUP_WORD;
default: return SCR_MAP_LOOKUP_GENERIC;
}
}
/* ── direct-indexed number keys ────────────────────────────────────────
* Number-keyed maps whose keys are small non-negative integers (ids,
* indices) index entries through dense[k] instead of hashing. The entries
* array, tombstones and iteration are unchanged; only the index differs.
* -0 lands on slot 0 (SameValueZero), and NaN, fractions, negatives and
* keys beyond the table are absent by construction. A key that cannot be
* stored densely converts the map to the bucket table; when that table
* must grow, compact integer keys switch it back (scr_map_enter_dense). */
#define SCR_MAP_DENSE_MIN 16
/* Grow the table only while it stays within this factor of the live count
* (32 bytes of index per entry at worst), so sparse keys fall back to the
* bucket table instead of allocating for the largest key. */
#define SCR_MAP_DENSE_SPREAD 8
/* Small maps may cover keys up to this bound regardless of their size. */
#define SCR_MAP_DENSE_FLOOR 64
/* The table slot for a number key, or false when the key cannot have one. */
static inline bool scr_map_dense_slot(double k, size_t limit, size_t *out) {
if (!(k >= 0) || !(k < (double)limit)) return false;
size_t i = (size_t)k;
if ((double)i != k) return false;
*out = i;
return true;
}
static inline size_t scr_map_find_dense(const ScrMap *m, double k) {
size_t i;
if (!scr_map_dense_slot(k, m->ndense, &i)) return SCR_MAP_EMPTY;
uint32_t entry = m->dense[i];
return entry ? (size_t)entry - 1 : SCR_MAP_EMPTY;
}
static size_t scr_map_find(const ScrMap *m, uint64_t hash, uint64_t key) {
if (m->ndense) return scr_map_find_dense(m, scr_map_slot_to_f64(key));
switch (scr_map_lookup_kind(m)) {
case SCR_MAP_LOOKUP_STR: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_STR);
case SCR_MAP_LOOKUP_WORD: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_WORD);
default: return scr_map_find_with(m, hash, key, SCR_MAP_LOOKUP_GENERIC);
}
}
static size_t scr_map_find_f64(const ScrMap *m, double key) {
if (m->ndense) return scr_map_find_dense(m, key);
if (m->nlive == 0) return SCR_MAP_EMPTY;
uint64_t k = scr_map_f64_bits(key);
return scr_map_find_with(m, scr_map_hash_word(k), k, SCR_MAP_LOOKUP_WORD);
}
const ScrMapEntry *scr_map_entry_f64(const ScrMap *m, double key) {
size_t e = scr_map_find_f64(m, key);
return e == SCR_MAP_EMPTY ? NULL : &m->entries[e];
}
/* The key's hash is usually cached, so even the small linear scan rejects
* entries by hash before touching their bytes. */
static inline __attribute__((always_inline)) size_t scr_map_find_str(const ScrMap *m,
const ScrStr *key) {
if (m->nlive == 0) return SCR_MAP_EMPTY;
return scr_map_find_with(m, scr_map_hash_str(key), scr_map_slot_from_ptr((void *)key),
SCR_MAP_LOOKUP_STR);
}
/* A transient input span follows the same hash, equality and ownership
* rules as a string key without allocating or copying the probe. */
static size_t scr_map_find_span(const ScrMap *m, const char *bytes, size_t length) {
if (m->nlive == 0) return SCR_MAP_EMPTY;
ScrMapSpan key = { bytes, length };
return scr_map_find_with(m, scr_map_hash_bytes(bytes, length), scr_map_slot_from_ptr(&key),
SCR_MAP_LOOKUP_SPAN);
}
bool scr_map_get_span_f64(const ScrMap *m, const char *bytes, size_t length, double *out) {
size_t e = scr_map_find_span(m, bytes, length);
if (e == SCR_MAP_EMPTY) return false;
*out = scr_map_slot_to_f64(m->entries[e].val);
return true;
}
bool scr_map_get_span_bool(const ScrMap *m, const char *bytes, size_t length, bool *out) {
size_t e = scr_map_find_span(m, bytes, length);
if (e == SCR_MAP_EMPTY) return false;
*out = m->entries[e].val != 0;
return true;
}
void *scr_map_get_span_ref(const ScrMap *m, const char *bytes, size_t length) {
size_t e = scr_map_find_span(m, bytes, length);
return e == SCR_MAP_EMPTY ? NULL : m->ops->val_retain(scr_map_slot_to_ptr(m->entries[e].val));
}
/* Rebuild the bucket table (size must be a power of two >= 2 * nentries):
* only live entries are inserted, in dense order — dead markers vanish. */
static void scr_map_rebuild_buckets(ScrMap *m, size_t nbuckets) {
bool compact = scr_map_compact_buckets(nbuckets);
size_t bytes = nbuckets * (compact ? sizeof(uint32_t) : sizeof(size_t));
void *buckets = scr_mem_alloc(bytes);
if (!buckets) scr_map_oom();
memset(buckets, compact ? 0 : 0xff, bytes);
scr_mem_free(m->buckets);
m->buckets = buckets;
m->nbuckets = (uint32_t)nbuckets;
size_t mask = nbuckets - 1;
for (size_t e = 0; e < m->nentries; e++) {
uint64_t hash = m->entries[e].hash;
if (!hash) continue;
size_t i = hash & mask;
while (scr_map_bucket(m, i) != SCR_MAP_EMPTY) i = (i + 1) & mask;
scr_map_bucket_set(m, i, e, hash);
}
}
/* Point every live entry's dense slot at it (the table is zeroed first). */
static void scr_map_fill_dense(ScrMap *m) {
memset(m->dense, 0, m->ndense * sizeof *m->dense);
for (size_t e = 0; e < m->nentries; e++) {
if (!m->entries[e].hash) continue;
m->dense[(size_t)scr_map_slot_to_f64(m->entries[e].key)] = (uint32_t)(e + 1);
}
}
/* A table of at least SCR_MAP_DENSE_MIN slots covering index `top`. */
static size_t scr_map_dense_size(size_t top) {
size_t n = SCR_MAP_DENSE_MIN;
while (n <= top) n *= 2;
return n;
}
/* The exclusive key bound a table may cover once it holds `live` entries.
* Entry indices are stored as uint32_t + 1, and the table's length (a
* power of two above every covered key) fits ScrMap.ndense. */
#define SCR_MAP_DENSE_MAX ((size_t)1 << 31)
static size_t scr_map_dense_limit(size_t live) {
if (live > SIZE_MAX / SCR_MAP_DENSE_SPREAD) return SCR_MAP_DENSE_MAX;
size_t limit = SCR_MAP_DENSE_SPREAD * live;
if (limit < SCR_MAP_DENSE_FLOOR) limit = SCR_MAP_DENSE_FLOOR;
return limit < SCR_MAP_DENSE_MAX ? limit : SCR_MAP_DENSE_MAX;
}
static void scr_map_dense_alloc(ScrMap *m, size_t n) {
uint32_t *dense = scr_mem_realloc(m->dense, n * sizeof *dense);
if (!dense) scr_map_oom();
m->dense = dense;
m->ndense = (uint32_t)n;
}
/* Called when a number-key map's linear scan or bucket table would grow to
* take an absent key: index it directly when every live key and the
* incoming key fit a compact table, keep hashing otherwise. One scan per
* growth step keeps this amortized constant per insertion. */
static __attribute__((noinline)) void scr_map_enter_dense(ScrMap *m, double incoming) {
size_t limit = scr_map_dense_limit(m->nlive + 1), top, i;
if (m->nentries >= UINT32_MAX - 1 || !scr_map_dense_slot(incoming, limit, &top)) return;
for (size_t e = 0; e < m->nentries; e++) {
if (!m->entries[e].hash) continue;
if (!scr_map_dense_slot(scr_map_slot_to_f64(m->entries[e].key), limit, &i)) return;
if (i > top) top = i;
}
scr_mem_free(m->buckets);
m->buckets = NULL;
m->nbuckets = 0;
scr_map_dense_alloc(m, scr_map_dense_size(top));
scr_map_fill_dense(m);
}
/* Extend the table to cover a new integral key while it stays compact. */
static __attribute__((noinline)) bool scr_map_grow_dense(ScrMap *m, double k, size_t *out) {
size_t i;
if (!scr_map_dense_slot(k, scr_map_dense_limit(m->nlive + 1), &i)) return false;
size_t old = m->ndense, n = scr_map_dense_size(i);
scr_map_dense_alloc(m, n);
memset(m->dense + old, 0, (n - old) * sizeof *m->dense);
*out = i;
return true;
}
/* Switch to the bucket table; cached entry hashes are already complete. */
static __attribute__((noinline)) void scr_map_leave_dense(ScrMap *m) {
scr_mem_free(m->dense);
m->dense = NULL;
m->ndense = 0;
if (m->nentries < SCR_MAP_LINEAR_LIMIT) return;
size_t nbuckets = 16;
while (nbuckets < 2 * (m->nentries + 1)) {
if (nbuckets > SIZE_MAX / 2 / sizeof(size_t)) scr_map_oom();
nbuckets *= 2;
}
scr_map_rebuild_buckets(m, nbuckets);
}
/* Drop tombstones, preserving insertion order. Only legal when no iteration
* is active (the forEach desugar's indices would shift).
*
* Index tables are sized by the most entries a map has held, so a map that
* shrank keeps rebuilding (and clearing) a table sized for its peak. A
* bucket table more than eight times what the survivors need shrinks to
* twice that, leaving room to double before it grows again. With `reindex`
* the direct index also follows the surviving keys: it shrinks to cover the
* largest one, and keys that became sparse move back to hashing (at twice
* the spread that admits them, so churn cannot flip modes). The append path
* passes false: it holds a slot of the current direct index. */
static void scr_map_compact(ScrMap *m, bool reindex) {
size_t w = 0, top = 0;
uint64_t filter = 0;
bool dense = m->ndense != 0;
for (size_t r = 0; r < m->nentries; r++) {
if (!m->entries[r].hash) continue;
if (dense) {
size_t k = (size_t)scr_map_slot_to_f64(m->entries[r].key);
if (k > top) top = k;
}
filter |= scr_map_filter_bits(m->entries[r].hash);
m->entries[w++] = m->entries[r];
}
m->nentries = (uint32_t)w;
m->filter = filter;
m->head = 0;
if (m->nbuckets > 0) {
size_t want = 16;
while (want < 2 * (w + 1)) want *= 2;
scr_map_rebuild_buckets(m, m->nbuckets / 8 > want ? 2 * want : m->nbuckets);
return;
}
if (!dense) return;
if (reindex) {
if (w == 0) {
scr_mem_free(m->dense);
m->dense = NULL;
m->ndense = 0;
return;
}
size_t limit = scr_map_dense_limit(w);
if (top / 2 >= limit) {
scr_map_leave_dense(m);
return;
}
size_t need = scr_map_dense_size(top);
if (need < m->ndense) scr_map_dense_alloc(m, need);
}
scr_map_fill_dense(m);
}
/* The most entries a map holds, tombstones included: its 32-bit counts and
* its bucket count (a power of two at least twice the entries) stay in
* range. */
#define SCR_MAP_MAX_ENTRIES ((size_t)1 << 30)
/* The first entry gets an array of one, so a one-entry map keeps no unused
* slots; the second grows it to four, and capacity doubles after that. */
static void scr_map_grow_entries(ScrMap *m) {
size_t cap = m->ecap == 0 ? 1 : m->ecap < 4 ? 4 : m->ecap;
while (cap < m->nentries + 1) cap *= 2;
if (cap > SCR_MAP_MAX_ENTRIES) scr_map_oom();
ScrMapEntry *entries = scr_mem_realloc(m->entries, cap * sizeof *entries);
if (!entries) scr_map_oom();
m->entries = entries;
m->ecap = (uint32_t)cap;
}
/* Make room to append one entry. Prefers compaction (tombstone-heavy maps
* reuse their storage) and grows otherwise; while an iteration is active it
* ONLY grows — indices must stay stable under callback mutation. */
static void scr_map_reserve_append(ScrMap *m) {
if (m->ndense) {
/* Directly indexed: the table is sized by keys, not entry count. */
if (m->nentries < m->ecap) return;
if (m->iter_depth == 0 && m->nentries > 0 && m->nlive <= m->nentries / 2) scr_map_compact(m, false);
if (m->nentries == m->ecap) scr_map_grow_entries(m);
return;
}
if (m->nentries < m->ecap &&
((m->nbuckets == 0 && m->nentries < SCR_MAP_LINEAR_LIMIT) ||
m->nbuckets >= 2 * (m->nentries + 1))) return;
if (m->iter_depth == 0 && m->nentries > 0 &&
(m->nbuckets == 0 ? m->nlive < m->nentries : m->nlive <= m->nentries / 2)) {
scr_map_compact(m, false);
}
if (m->nentries == m->ecap) scr_map_grow_entries(m);
if (m->nbuckets == 0 && m->nentries < SCR_MAP_LINEAR_LIMIT) return;
if (m->nbuckets < 2 * (m->nentries + 1)) {
size_t nbuckets = m->nbuckets ? m->nbuckets : 16;
while (nbuckets < 2 * (m->nentries + 1)) {
if (nbuckets > SIZE_MAX / 2 / sizeof(size_t)) scr_map_oom();
nbuckets *= 2;
}
scr_map_rebuild_buckets(m, nbuckets);
}
}
/* ── entry release helpers ─────────────────────────────────────────────── */
static bool scr_map_ref_key(ScrMapKeyKind kind) {
return kind == SCR_MAP_KEY_REF || kind == SCR_MAP_KEY_UNION_REF || kind == SCR_MAP_KEY_DYN ||
kind == SCR_MAP_KEY_BIGINT || kind == SCR_MAP_KEY_UNION_VALUE;
}
static void scr_map_release_key(ScrMap *m, uint64_t key) {
if (m->key_kind == SCR_MAP_KEY_STR) {
scr_str_release((ScrStr *)scr_map_slot_to_ptr(key));
} else if (scr_map_ref_key(m->key_kind)) {
m->ops->key_release(scr_map_slot_to_ptr(key));
}
}
static void scr_map_release_val(ScrMap *m, uint64_t val) {
if (m->val_kind == SCR_MAP_VAL_REF) m->ops->val_release(scr_map_slot_to_ptr(val));
}
/* ── lifecycle ─────────────────────────────────────────────────────────── */
static void scr_map_trace(void *o, ScrTraceVisit visit, void *ctx) {
ScrMap *m = (ScrMap *)o;
bool keys = m->traced & SCR_MAP_TRACED_KEY, vals = m->traced & SCR_MAP_TRACED_VAL;
for (size_t e = 0; e < m->nentries; e++) {
if (!m->entries[e].hash) continue;
if (keys) visit(scr_map_slot_to_ptr(m->entries[e].key), ctx);
if (vals) visit(scr_map_slot_to_ptr(m->entries[e].val), ctx);
}
}
/* The collector accounts for traced edges. Release only their complement:
* an identity-key map may trace keys, values, or both independently. */
static void scr_map_gcfree(void *o) {
ScrMap *m = (ScrMap *)o;
bool keys = m->traced & SCR_MAP_TRACED_KEY, vals = m->traced & SCR_MAP_TRACED_VAL;
scr_weak_dispose(m);
for (size_t e = 0; e < m->nentries; e++) {
if (!m->entries[e].hash) continue;
if (!keys) scr_map_release_key(m, m->entries[e].key);
if (!vals) scr_map_release_val(m, m->entries[e].val);
}
scr_mem_free(m->entries);
scr_mem_free(m->buckets);
scr_mem_free(m->dense);
#ifdef SCR_RC_AUDIT
scr_live_maps--;
#endif
scr_cyc_free(m);
}
/* Map construction inlines into its callers; keeping the header allocation
* out of line keeps those call sites as small as the libc call they had. */
static __attribute__((noinline)) ScrMap *scr_map_alloc_plain(void) {
return scr_mem_calloc(sizeof(ScrMap));
}
static const ScrMapOps scr_map_no_ops = {0};
ScrMap *scr_map_new(ScrMapKeyKind key_kind, ScrMapValKind val_kind, const ScrMapOps *ops) {
if (!ops) ops = &scr_map_no_ops;
unsigned traced = (ops->key_trace ? SCR_MAP_TRACED_KEY : 0) | (ops->val_trace ? SCR_MAP_TRACED_VAL : 0);
ScrMap *m;
if (traced) {
/* Either side can point back: collector header + trace. */
static ScrCycType cyc_type = { &scr_map_trace, &scr_map_gcfree, SCR_CYC_KIND_MAP };
m = scr_cyc_alloc(sizeof *m, &cyc_type);
} else {
m = scr_map_alloc_plain();
if (!m) scr_map_oom();
}
m->rc = 1;
m->key_kind = (uint8_t)key_kind;
m->val_kind = (uint8_t)val_kind;
m->traced = (uint8_t)traced;
m->ops = ops;
#ifdef SCR_RC_AUDIT
scr_live_maps++;
#endif
return m;
}
ScrMap *scr_map_retain(ScrMap *m) {
if (m->rc != SIZE_MAX) {
m->rc++;
}
return m;
}
static void scr_map_destroy(void *object) {
ScrMap *m = object;
scr_weak_dispose(m);
for (size_t e = 0; e < m->nentries; e++) {
if (!m->entries[e].hash) continue;
scr_map_release_key(m, m->entries[e].key);
scr_map_release_val(m, m->entries[e].val);
}
scr_mem_free(m->entries);
scr_mem_free(m->buckets);
scr_mem_free(m->dense);
#ifdef SCR_RC_AUDIT
scr_live_maps--;
#endif
if (m->traced) scr_cyc_free(m);
else scr_mem_free(m);
}
void scr_map_release(ScrMap *m) {
if (!m || m->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */
if (--m->rc == 0) {
if (m->traced) scr_cyc_dead(m);
scr_rc_destroy(m, scr_map_destroy);
} else if (m->traced) {
scr_cyc_released(m); /* possible cycle root; may collect — m is done */
}
}
void *scr_map_retain_v(void *m) { return scr_map_retain((ScrMap *)m); }
void scr_map_release_v(void *m) { scr_map_release((ScrMap *)m); }
void scr_map_trace_v(void *m, ScrTraceVisit visit, void *ctx) {
scr_map_trace(m, visit, ctx);
}
double scr_map_size(const ScrMap *m) { return (double)m->nlive; }
/* Storage clear() keeps regardless of how many entries it removed. */
#define SCR_MAP_CLEAR_KEEP 256
/* clear() costs time proportional to the entries it walks (live entries
* and tombstones appended since the last reset), never to the peak size of
* the index tables:
* - the direct index is zero exactly at the absent keys (deletes clear
* their slots), so only the live entries' slots are reset;
* - the bucket table is reset only while it is proportional to the
* entries that pointed into it;
* - storage far larger than the entries just removed is freed, returning
* the map to its initial empty state the way V8 reallocates a cleared
* table. Regrowth is amortized against the insertions that need it.
* A map without entries has empty tables already: clearing it does no
* work and keeps its storage for the next fill. */
void scr_map_clear(ScrMap *m) {
size_t walked = m->nentries;
if (walked == 0) return;
uint32_t *dense = m->ndense ? m->dense : NULL;
for (size_t e = 0; e < walked; e++) {
if (!m->entries[e].hash) continue;
m->entries[e].hash = 0;
if (dense) dense[(size_t)scr_map_slot_to_f64(m->entries[e].key)] = 0;
scr_map_release_key(m, m->entries[e].key);
scr_map_release_val(m, m->entries[e].val);
}
m->nlive = 0;
m->filter = 0;
m->head = (uint32_t)walked;
if (m->iter_depth == 0) {
/* Full reset; an active iteration instead keeps the (now all-dead)
* entries so its indices stay stable — entries added by the callback
* after the clear append past them and ARE visited (Node-exact). */
m->nentries = 0;
m->head = 0;
if (m->ecap > SCR_MAP_CLEAR_KEEP && m->ecap / 8 > walked) {
scr_mem_free(m->entries);
m->entries = NULL;
m->ecap = 0;
}
}
/* Without either index, a map is a linear scan; dead entries kept for an
* active iteration are skipped by every later rebuild. */
if (m->ndense > SCR_MAP_CLEAR_KEEP && m->ndense / 32 > walked) {
scr_mem_free(m->dense);
m->dense = NULL;
m->ndense = 0;
}
if (m->nbuckets) {
if (m->nbuckets > SCR_MAP_CLEAR_KEEP && m->nbuckets / 8 > walked) {
scr_mem_free(m->buckets);
m->buckets = NULL;
m->nbuckets = 0;
} else {
bool compact = scr_map_compact_buckets(m->nbuckets);
memset(m->buckets, compact ? 0 : 0xff,
m->nbuckets * (compact ? sizeof(uint32_t) : sizeof(size_t)));
}
}
}
/* ── has / delete ──────────────────────────────────────────────────────── */
bool scr_map_has_f64(const ScrMap *m, double key) {
return scr_map_find_f64(m, key) != SCR_MAP_EMPTY;
}
bool scr_map_has_str(const ScrMap *m, const ScrStr *key) {
return scr_map_find_str(m, key) != SCR_MAP_EMPTY;
}
/* Tombstones beyond this share of the entries are compacted on delete, so
* iteration and later rebuilds cost O(live) rather than O(peak). V8 shrinks
* an ordered hash table at the same occupancy. Small maps keep theirs. */
#define SCR_MAP_SHRINK_MIN 64
static bool scr_map_delete_found(ScrMap *m, size_t e) {
if (e == SCR_MAP_EMPTY) return false;
uint64_t key = m->entries[e].key, val = m->entries[e].val;
m->entries[e].hash = 0; /* bucket slot stays: probe chains intact */
m->nlive--;
if (m->ndense) m->dense[(size_t)scr_map_slot_to_f64(key)] = 0;
/* Each such compaction follows deletes of three quarters of the entries,
* so it is amortized constant per delete. It runs before the releases:
* they may drop the last reference to this map. */
if (__builtin_expect(m->iter_depth == 0 && m->nentries >= SCR_MAP_SHRINK_MIN &&
m->nlive < m->nentries / 4, 0)) {
scr_map_compact(m, true);
}
scr_map_release_key(m, key);
scr_map_release_val(m, val);
return true;
}
bool scr_map_delete_f64(ScrMap *m, double key) {
return scr_map_delete_found(m, scr_map_find_f64(m, key));
}
bool scr_map_delete_str(ScrMap *m, const ScrStr *key) {
return scr_map_delete_found(m, scr_map_find_str(m, key));
}
/* REF keys: identity hashing over the pointer bits (see the header's
* SCR_MAP_KEY_REF note). Probes never retain; storage does. */
static size_t scr_map_find_ref(const ScrMap *m, const void *key) {
if (m->nlive == 0) return SCR_MAP_EMPTY;
uint64_t k = scr_map_slot_from_ptr((void *)key);
if (m->key_kind == SCR_MAP_KEY_REF) {
return scr_map_find_with(m, scr_map_hash_word(k), k, SCR_MAP_LOOKUP_WORD);
}
return scr_map_find(m, scr_map_hash_key(m, k), k);
}
bool scr_map_has_ref(const ScrMap *m, const void *key) {
return scr_map_find_ref(m, key) != SCR_MAP_EMPTY;
}
bool scr_map_delete_ref(ScrMap *m, const void *key) {
return scr_map_delete_found(m, scr_map_find_ref(m, key));
}
/* ── set ───────────────────────────────────────────────────────────────
* Overwrite keeps the entry (insertion position preserved, stored key kept
* — only the value is replaced-and-released). A new key appends: reserve
* space FIRST (compaction/growth may rebuild buckets), then probe for the
* insertion slot. */
/* Append a key known to be absent. slot is the empty bucket found by the
* probe (unused for linear storage). */
static void scr_map_append(ScrMap *m, uint64_t hash, uint64_t key, uint64_t val, size_t slot) {
size_t old_entries = m->nentries, old_buckets = m->nbuckets;
scr_map_reserve_append(m);
/* Growth or compaction can move the insertion bucket. Otherwise keep the
* empty slot from the first probe instead of walking the chain twice. */
if (m->nbuckets && (m->nentries != old_entries || m->nbuckets != old_buckets)) {
size_t mask = m->nbuckets - 1;
slot = hash & mask;
while (scr_map_bucket(m, slot) != SCR_MAP_EMPTY) slot = (slot + 1) & mask;
}
size_t idx = m->nentries++;
m->entries[idx].key = key;
m->entries[idx].val = val;
m->entries[idx].hash = hash;
m->filter |= scr_map_filter_bits(hash);
m->nlive++;
if (m->key_kind == SCR_MAP_KEY_STR) {
ScrStr *k = scr_map_slot_to_ptr(key); /* key is borrowed */
if (__builtin_expect(scr_str_is_scratch(k), 0))
m->entries[idx].key = scr_map_slot_from_ptr(scr_str_scratch_keep(k));
else
scr_str_retain(k);
} else if (m->key_kind == SCR_MAP_KEY_DYN) {
const ScrDyn *d = (ScrDyn *)scr_map_slot_to_ptr(key);
if (d->kind == SCR_DYN_NUM && d->v.num == 0) {
/* Iteration returns +0 even when the inserted key was -0. Never
* mutate the caller's shared box while normalizing a stored key. */
m->entries[idx].key = scr_map_slot_from_ptr(scr_dyn_new_num(0));
} else {
m->ops->key_retain(scr_map_slot_to_ptr(key));
}
} else if (m->key_kind == SCR_MAP_KEY_UNION_VALUE &&
m->ops->union_keys[((ScrUnion *)scr_map_slot_to_ptr(key))->tag] == SCR_MAP_KEY_F64 &&
((ScrUnion *)scr_map_slot_to_ptr(key))->slot == UINT64_C(0x8000000000000000)) {
/* Normalize stored -0 without mutating the caller's immutable box. */
ScrUnion *u = scr_map_slot_to_ptr(key);
m->entries[idx].key = scr_map_slot_from_ptr(scr_union_new_f64(u->tag, 0));
} else if (scr_map_ref_key(m->key_kind)) {
m->ops->key_retain(scr_map_slot_to_ptr(key)); /* key is borrowed */
}
if (m->nbuckets) scr_map_bucket_set(m, slot, idx, hash);
}
/* The probe and overwrite are specialized by lookup kind like reads; the
* append (allocation, growth, key ownership) stays shared. */
static inline __attribute__((always_inline)) void scr_map_set_with(
ScrMap *m, uint64_t hash, uint64_t key, uint64_t val, ScrMapLookup lookup) {
hash = hash ? hash : 1;
size_t slot = SCR_MAP_EMPTY, e;
if (m->nbuckets == 0) {
e = scr_map_find_with(m, hash, key, lookup);
} else {
slot = scr_map_probe_with(m, hash, key, lookup);
e = scr_map_bucket(m, slot);
}
if (e != SCR_MAP_EMPTY) {
uint64_t old = m->entries[e].val;
m->entries[e].val = val; /* unlink before releasing (cycle collector) */
scr_map_release_val(m, old);
return;
}
scr_map_append(m, hash, key, val, slot);
}
static void scr_map_set_str_slot(ScrMap *m, uint64_t hash, uint64_t key, uint64_t val) {
scr_map_set_with(m, hash, key, val, SCR_MAP_LOOKUP_STR);
}
/* One out-of-line copy of the word-key insert serves both the generic
* dispatcher and the number-key path below. */
static __attribute__((noinline)) void scr_map_set_word(ScrMap *m, uint64_t hash, uint64_t key,
uint64_t val) {
scr_map_set_with(m, hash, key, val, SCR_MAP_LOOKUP_WORD);
}
/* A number-key map with at least this many entries moves to the direct
* index when its keys allow it, so small integer keys are indexed before
* the linear scan ends. */
#define SCR_MAP_DENSE_AFTER 4
/* Whether appending one more entry would grow the lookup index rather than
* reuse tombstones: scr_map_reserve_append compacts a linear map with a
* dead entry, or a bucket table at most half live, whenever no iteration
* is active. A linear map counts as growing from SCR_MAP_DENSE_AFTER
* entries. Only real growth may move a number-key map to the direct
* index; churn on a small map keeps its compact storage. */
static inline bool scr_map_append_grows_index(const ScrMap *m) {
bool compacts = m->iter_depth == 0 && m->nentries > 0;
if (m->nbuckets == 0)
return m->nentries >= SCR_MAP_DENSE_AFTER && !(compacts && m->nlive < m->nentries);
return m->nbuckets < 2 * (m->nentries + 1) && !(compacts && m->nlive <= m->nentries / 2);
}
/* Number keys arrive normalized (+0 for -0, canonical NaN). A directly
* indexed map overwrites or appends through its table; a key the table
* cannot hold converts it to hashing first. When an append would grow a
* hashed map's index and the key could be indexed directly, the lookup that
* decides whether the key is new also overwrites a key that is already
* present. */
static void scr_map_set_f64_slot(ScrMap *m, uint64_t key, uint64_t val) {
double k = scr_map_slot_to_f64(key);
size_t slot;
if (!m->ndense && scr_map_append_grows_index(m) &&
scr_map_dense_slot(k, scr_map_dense_limit(m->nlive + 1), &slot)) {
size_t e = scr_map_find_f64(m, k);
if (e != SCR_MAP_EMPTY) {
uint64_t old = m->entries[e].val;
m->entries[e].val = val; /* unlink before releasing (cycle collector) */
scr_map_release_val(m, old);
return;
}
scr_map_enter_dense(m, k); /* the append would grow the index; the key fits a table */
}
if (m->ndense) {
size_t i;
if (scr_map_dense_slot(k, m->ndense, &i) || scr_map_grow_dense(m, k, &i)) {
uint32_t entry = m->dense[i];
if (entry) {
uint64_t old = m->entries[entry - 1].val;
m->entries[entry - 1].val = val; /* unlink before releasing (cycle collector) */
scr_map_release_val(m, old);
return;
}
if (m->nentries < UINT32_MAX - 1) {
scr_map_reserve_append(m); /* may compact, which refills the table */
size_t idx = m->nentries++;
uint64_t hash = scr_map_hash_word(key);
m->entries[idx].key = key;
m->entries[idx].val = val;
m->entries[idx].hash = hash ? hash : 1;
m->filter |= scr_map_filter_bits(m->entries[idx].hash);
m->nlive++;
m->dense[i] = (uint32_t)(idx + 1);
return;
}
}
scr_map_leave_dense(m);
}
scr_map_set_word(m, scr_map_hash_word(key), key, val);
}
static void scr_map_set(ScrMap *m, uint64_t hash, uint64_t key, uint64_t val) {
if (m->key_kind == SCR_MAP_KEY_F64) {
scr_map_set_f64_slot(m, key, val);
return;
}
switch (scr_map_lookup_kind(m)) {
case SCR_MAP_LOOKUP_STR: scr_map_set_str_slot(m, hash, key, val); return;
case SCR_MAP_LOOKUP_WORD: scr_map_set_word(m, hash, key, val); return;
default: scr_map_set_with(m, hash, key, val, SCR_MAP_LOOKUP_GENERIC); return;
}
}
static void scr_map_set_f64_key(ScrMap *m, double key, uint64_t val) {
scr_map_set_f64_slot(m, scr_map_f64_bits(key), val); /* stores +0 for -0, canonical NaN */
}
static void scr_map_set_str_key(ScrMap *m, ScrStr *key, uint64_t val) {
scr_map_set_str_slot(m, scr_map_hash_str(key), scr_map_slot_from_ptr(key), val);
}
void scr_map_set_f64_f64(ScrMap *m, double key, double v) {
scr_map_set_f64_key(m, key, scr_map_slot_from_f64(v));
}
void scr_map_set_f64_bool(ScrMap *m, double key, bool v) {
scr_map_set_f64_key(m, key, (uint64_t)(v ? 1 : 0));
}
void scr_map_set_f64_ref(ScrMap *m, double key, void *v) {
scr_map_set_f64_key(m, key, scr_map_slot_from_ptr(v));
}
void scr_map_set_str_f64(ScrMap *m, ScrStr *key, double v) {
scr_map_set_str_key(m, key, scr_map_slot_from_f64(v));
}
void scr_map_set_str_bool(ScrMap *m, ScrStr *key, bool v) {
scr_map_set_str_key(m, key, (uint64_t)(v ? 1 : 0));
}
void scr_map_set_str_ref(ScrMap *m, ScrStr *key, void *v) {
scr_map_set_str_key(m, key, scr_map_slot_from_ptr(v));
}
void scr_map_set_ref_f64(ScrMap *m, void *key, double v) {
uint64_t k = scr_map_slot_from_ptr(key);
scr_map_set(m, scr_map_hash_key(m, k), k, scr_map_slot_from_f64(v));
}
void scr_map_set_ref_bool(ScrMap *m, void *key, bool v) {
uint64_t k = scr_map_slot_from_ptr(key);
scr_map_set(m, scr_map_hash_key(m, k), k, (uint64_t)(v ? 1 : 0));
}
void scr_map_set_ref_ref(ScrMap *m, void *key, void *v) {
uint64_t k = scr_map_slot_from_ptr(key);
scr_map_set(m, scr_map_hash_key(m, k), k, scr_map_slot_from_ptr(v));
}
/* ── get ───────────────────────────────────────────────────────────────── */
bool scr_map_get_f64_f64(const ScrMap *m, double key, double *out) {
size_t e = scr_map_find_f64(m, key);
if (e == SCR_MAP_EMPTY) return false;
*out = scr_map_slot_to_f64(m->entries[e].val);
return true;
}
bool scr_map_get_f64_bool(const ScrMap *m, double key, bool *out) {
size_t e = scr_map_find_f64(m, key);
if (e == SCR_MAP_EMPTY) return false;
*out = m->entries[e].val != 0;
return true;
}
void *scr_map_get_f64_ref(const ScrMap *m, double key) {
size_t e = scr_map_find_f64(m, key);
if (e == SCR_MAP_EMPTY) return NULL;
return m->ops->val_retain(scr_map_slot_to_ptr(m->entries[e].val)); /* +1 */
}
bool scr_map_get_str_f64(const ScrMap *m, const ScrStr *key, double *out) {
size_t e = scr_map_find_str(m, key);
if (e == SCR_MAP_EMPTY) return false;
*out = scr_map_slot_to_f64(m->entries[e].val);
return true;
}
bool scr_map_get_str_bool(const ScrMap *m, const ScrStr *key, bool *out) {
size_t e = scr_map_find_str(m, key);
if (e == SCR_MAP_EMPTY) return false;
*out = m->entries[e].val != 0;
return true;
}
static __attribute__((noinline)) const ScrMapEntry *scr_map_entry_str_slow(const ScrMap *m,
const ScrStr *key) {
size_t e = scr_map_find_str(m, key);
return e == SCR_MAP_EMPTY ? NULL : &m->entries[e];
}
/* A stored key with the probe's hash but another identity: compare bytes.
* Equal hashes of different strings are rare enough to answer by the
* complete lookup. */
static __attribute__((noinline)) const ScrMapEntry *scr_map_entry_str_bytes(const ScrMap *m,
const ScrStr *key,
const ScrMapEntry *e) {
const ScrStr *stored = scr_map_slot_to_ptr(e->key);
if (stored->len == key->len && scr_key_equal(stored->data, key->data, key->len)) return e;
return scr_map_entry_str_slow(m, key);
}
/* Every string-key read of generated code lands here. The common shapes, a
* key with a cached hash probing linear or compact-bucket storage, run
* without a stack frame: a hash match on another string object tail-calls
* the byte comparison, and anything else (an uncached hash, wide buckets)
* the complete lookup. */
const ScrMapEntry *scr_map_entry_str(const ScrMap *m, const ScrStr *key) {
#if SCR_STR_HASH_CACHE
uint32_t h = key->hash;
size_t nbuckets = m->nbuckets;
if (__builtin_expect(h != 0 && scr_map_compact_buckets(nbuckets), 1)) {
uint64_t hash = scr_map_hash_widen(h);
uint64_t bits = scr_map_filter_bits(hash);
if ((m->filter & bits) != bits) return NULL;
const ScrMapEntry *entries = m->entries;
if (nbuckets == 0) {
for (size_t e = 0, n = m->nentries; e < n; e++) {
if (entries[e].hash != hash) continue;
if (entries[e].key == scr_map_slot_from_ptr((void *)key)) return &entries[e];
return scr_map_entry_str_bytes(m, key, &entries[e]);
}
return NULL;
}
const uint32_t *buckets = m->buckets;
uint32_t tag = (uint32_t)(hash >> 32 & UINT32_C(0xff000000));
size_t mask = nbuckets - 1;
for (size_t i = hash & mask;; i = (i + 1) & mask) {
uint32_t bucket = buckets[i];
if (!bucket) return NULL;
if ((bucket & ~SCR_MAP_INDEX_MASK) != tag) continue;
const ScrMapEntry *e = &entries[(bucket & SCR_MAP_INDEX_MASK) - 1];
if (e->hash != hash) continue;
if (e->key == scr_map_slot_from_ptr((void *)key)) return e;
return scr_map_entry_str_bytes(m, key, e);
}
}
#endif
return scr_map_entry_str_slow(m, key);
}
void *scr_map_get_str_ref(const ScrMap *m, const ScrStr *key) {
size_t e = scr_map_find_str(m, key);
if (e == SCR_MAP_EMPTY) return NULL;
return m->ops->val_retain(scr_map_slot_to_ptr(m->entries[e].val)); /* +1 */
}
bool scr_map_get_ref_f64(const ScrMap *m, const void *key, double *out) {
size_t e = scr_map_find_ref(m, key);
if (e == SCR_MAP_EMPTY) return false;
*out = scr_map_slot_to_f64(m->entries[e].val);
return true;
}
bool scr_map_get_ref_bool(const ScrMap *m, const void *key, bool *out) {
size_t e = scr_map_find_ref(m, key);
if (e == SCR_MAP_EMPTY) return false;
*out = m->entries[e].val != 0;
return true;
}
void *scr_map_get_ref_ref(const ScrMap *m, const void *key) {
size_t e = scr_map_find_ref(m, key);
if (e == SCR_MAP_EMPTY) return NULL;
return m->ops->val_retain(scr_map_slot_to_ptr(m->entries[e].val));
}
/* Boolean keys use unboxed 0/1 slots and the same probe path. */
static size_t scr_map_find_bool(const ScrMap *m, bool key) {
if (m->nlive == 0) return SCR_MAP_EMPTY;
uint64_t slot = key ? 1 : 0;
return scr_map_find_with(m, scr_map_hash_word(slot), slot, SCR_MAP_LOOKUP_WORD);
}
bool scr_map_has_bool(const ScrMap *m, bool key) {
return scr_map_find_bool(m, key) != SCR_MAP_EMPTY;
}
bool scr_map_delete_bool(ScrMap *m, bool key) {
return scr_map_delete_found(m, scr_map_find_bool(m, key));
}
void scr_map_set_bool_f64(ScrMap *m, bool key, double value) {
scr_map_set(m, scr_map_hash_word(key ? 1 : 0), key ? 1 : 0, scr_map_slot_from_f64(value));
}
void scr_map_set_bool_bool(ScrMap *m, bool key, bool value) {
scr_map_set(m, scr_map_hash_word(key ? 1 : 0), key ? 1 : 0, value ? 1 : 0);
}
void scr_map_set_bool_ref(ScrMap *m, bool key, void *value) {
scr_map_set(m, scr_map_hash_word(key ? 1 : 0), key ? 1 : 0, scr_map_slot_from_ptr(value));
}
bool scr_map_get_bool_f64(const ScrMap *m, bool key, double *out) {
size_t entry = scr_map_find_bool(m, key);
if (entry == SCR_MAP_EMPTY) return false;
*out = scr_map_slot_to_f64(m->entries[entry].val);
return true;
}
bool scr_map_get_bool_bool(const ScrMap *m, bool key, bool *out) {
size_t entry = scr_map_find_bool(m, key);
if (entry == SCR_MAP_EMPTY) return false;
*out = m->entries[entry].val != 0;
return true;
}
void *scr_map_get_bool_ref(const ScrMap *m, bool key) {
size_t entry = scr_map_find_bool(m, key);
if (entry == SCR_MAP_EMPTY) return NULL;
return m->ops->val_retain(scr_map_slot_to_ptr(m->entries[entry].val));
}
/* ── iteration primitives (the forEach desugar) ────────────────────────── */
double scr_map_iter_count(const ScrMap *m) { return (double)m->nentries; }
bool scr_map_iter_live(const ScrMap *m, double i) {
if (!(i >= 0) || i >= (double)m->nentries) return false;
return m->entries[(size_t)i].hash;
}
/* A walk that starts at or below head moves head to the live entry it
* finds, so the deleted prefix is walked once however many iterations
* start there. A published map is read by other threads and keeps its
* header unchanged. */
double scr_map_iter_skip(ScrMap *m, double i) {
if (!(i < (double)m->nentries)) return i;
bool from_head = !(i > (double)m->head);
size_t e = from_head ? m->head : (size_t)i;
while (e < m->nentries && !m->entries[e].hash) e++;
if (from_head && m->rc != SIZE_MAX) m->head = (uint32_t)e;
return e == (size_t)i ? i : (double)e;
}
double scr_map_iter_start(ScrMap *m) {
return scr_map_iter_skip(m, 0);
}
static const ScrMapEntry *scr_map_iter_at(const ScrMap *m, double i) {
if (!(i >= 0) || i >= (double)m->nentries || !m->entries[(size_t)i].hash) {
scr_trap("scriptc: internal error: map iteration index out of range\n");
}
return &m->entries[(size_t)i];
}
double scr_map_iter_key_f64(const ScrMap *m, double i) {
return scr_map_slot_to_f64(scr_map_iter_at(m, i)->key);
}
bool scr_map_iter_key_bool(const ScrMap *m, double i) {
return scr_map_iter_at(m, i)->key != 0;
}
ScrStr *scr_map_iter_key_str(const ScrMap *m, double i) {
return scr_str_retain((ScrStr *)scr_map_slot_to_ptr(scr_map_iter_at(m, i)->key));
}
void *scr_map_iter_key_ref(const ScrMap *m, double i) {
return m->ops->key_retain(scr_map_slot_to_ptr(scr_map_iter_at(m, i)->key)); /* +1 */
}
double scr_map_iter_val_f64(const ScrMap *m, double i) {
return scr_map_slot_to_f64(scr_map_iter_at(m, i)->val);
}
bool scr_map_iter_val_bool(const ScrMap *m, double i) {
return scr_map_iter_at(m, i)->val != 0;
}
void scr_map_dyn_attach(ScrMap *map, const ScrMapDynOps *ops) {
map->dyn_ops = ops;
}
ScrDyn *scr_map_dyn_key(const ScrMap *map, double index) {
uint64_t slot = scr_map_iter_at(map, index)->key;
return map->dyn_ops ? map->dyn_ops->key_box(slot) : scr_dyn_retain(scr_map_slot_to_ptr(slot));
}
ScrDyn *scr_map_dyn_value(const ScrMap *map, double index) {
uint64_t slot = scr_map_iter_at(map, index)->val;
return map->dyn_ops ? map->dyn_ops->val_box(slot) : scr_dyn_retain(scr_map_slot_to_ptr(slot));
}
static size_t scr_map_dyn_find(ScrMap *map, const ScrDyn *key) {
if (map->dyn_ops && !map->dyn_ops->key_matches(key)) return SCR_MAP_EMPTY;
uint64_t slot = map->dyn_ops ? map->dyn_ops->key_unbox(key) : scr_map_slot_from_ptr(scr_dyn_retain((ScrDyn *)key));
if (scr_exc_pending()) return SCR_MAP_EMPTY;
if (map->key_kind == SCR_MAP_KEY_F64) slot = scr_map_f64_bits(scr_map_slot_to_f64(slot));
size_t entry = scr_map_find(map, scr_map_hash_key(map, slot), slot);
scr_map_release_key(map, slot);
return entry;
}
ScrDyn *scr_map_dyn_get(ScrMap *map, const ScrDyn *key) {
size_t entry = scr_map_dyn_find(map, key);
if (scr_exc_pending()) return NULL;
return entry == SCR_MAP_EMPTY ? scr_dyn_retain(scr_dyn_undefined()) : scr_map_dyn_value(map, (double)entry);
}
bool scr_map_dyn_has(ScrMap *map, const ScrDyn *key, bool remove) {
size_t entry = scr_map_dyn_find(map, key);
return remove ? scr_map_delete_found(map, entry) : entry != SCR_MAP_EMPTY;
}
void scr_map_dyn_set(ScrMap *map, const ScrDyn *key, const ScrDyn *value, bool set) {
uint64_t key_slot = map->dyn_ops ? map->dyn_ops->key_unbox(key) : scr_map_slot_from_ptr(scr_dyn_retain((ScrDyn *)key));
if (scr_exc_pending()) return;
uint64_t value_slot = set ? 0 : map->dyn_ops ? map->dyn_ops->val_unbox(value) : scr_map_slot_from_ptr(scr_dyn_retain((ScrDyn *)value));
if (scr_exc_pending()) { scr_map_release_key(map, key_slot); return; }
if (map->key_kind == SCR_MAP_KEY_F64) key_slot = scr_map_f64_bits(scr_map_slot_to_f64(key_slot));
scr_map_set(map, scr_map_hash_key(map, key_slot), key_slot, value_slot);
scr_map_release_key(map, key_slot);
}
/* Copy only live entries. The source is borrowed, contains unique keys,
* and runs no user code while traversed, so cached hashes remain valid. */
ScrMap *scr_map_clone(const ScrMap *source, bool keys_only, const ScrMapOps *ops) {
ScrMap *out = scr_map_new((ScrMapKeyKind)source->key_kind,
keys_only ? SCR_MAP_VAL_F64 : (ScrMapValKind)source->val_kind,
ops ? ops : source->ops);
size_t count = source->nlive;
if (!count) return out;
if (count > SIZE_MAX / sizeof(ScrMapEntry) || count > SIZE_MAX / 2 / sizeof(size_t)) scr_map_oom();
out->entries = scr_mem_alloc(count * sizeof(ScrMapEntry));
if (!out->entries) scr_map_oom();
out->ecap = (uint32_t)count;
size_t top = 0;
for (size_t i = 0; i < source->nentries; i++) {
const ScrMapEntry *entry = &source->entries[i];
if (!entry->hash) continue;
if (source->ndense) {
size_t k = (size_t)scr_map_slot_to_f64(entry->key);
if (k > top) top = k;
}
ScrMapEntry *copy = &out->entries[out->nentries++];
*copy = *entry;
if (out->key_kind == SCR_MAP_KEY_STR) scr_str_retain(scr_map_slot_to_ptr(copy->key));
else if (scr_map_ref_key(out->key_kind)) out->ops->key_retain(scr_map_slot_to_ptr(copy->key));
if (keys_only) copy->val = 0;
else if (out->val_kind == SCR_MAP_VAL_REF) out->ops->val_retain(scr_map_slot_to_ptr(copy->val));
}
out->nlive = (uint32_t)count;
out->filter = source->filter;
/* Size the copy's direct index for its own keys, not for the source's
* peak; keys that are sparse for their count hash instead. */
if (source->ndense && top / 2 < scr_map_dense_limit(count)) {
scr_map_dense_alloc(out, scr_map_dense_size(top));
scr_map_fill_dense(out);
return out;
}
if (count <= SCR_MAP_LINEAR_LIMIT) return out;
size_t buckets = 16;
while (buckets < 2 * count) {
if (buckets > SIZE_MAX / 2 / sizeof(size_t)) scr_map_oom();
buckets *= 2;
}
scr_map_rebuild_buckets(out, buckets);
return out;
}
const ScrMapOps scr_map_dyn_set_ops = {
scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, NULL, NULL, NULL, NULL
};
static const ScrMapOps scr_map_dyn_map_ops = {
scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v,
scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, NULL
};
/* A generic view can be backed by typed slots. Its copy must accept new
* JavaScript values independently of that backing's narrower adapters. */
ScrMap *scr_map_clone_dyn(const ScrMap *source, bool keys_only) {
ScrMap *out = keys_only ? scr_map_new(SCR_MAP_KEY_DYN, SCR_MAP_VAL_F64, &scr_map_dyn_set_ops)
: scr_map_new(SCR_MAP_KEY_DYN, SCR_MAP_VAL_REF, &scr_map_dyn_map_ops);
for (size_t i = 0; i < source->nentries; i++) {
if (!source->entries[i].hash) continue;
ScrDyn *key = scr_map_dyn_key(source, (double)i);
uint64_t value = keys_only ? 0 : scr_map_slot_from_ptr(scr_map_dyn_value(source, (double)i));
scr_map_set(out, scr_map_hash_dyn(key), scr_map_slot_from_ptr(key), value);
scr_dyn_release(key);
}
return out;
}
void scr_map_values_into(ScrMap *set, const ScrMap *source, bool checked) {
for (size_t i = 0; i < source->nentries; i++) {
if (!source->entries[i].hash) continue;
uint64_t value = checked ? scr_map_slot_from_ptr(scr_map_dyn_value(source, (double)i)) : source->entries[i].val;
if (set->key_kind == SCR_MAP_KEY_F64) value = scr_map_f64_bits(scr_map_slot_to_f64(value));
scr_map_set(set, scr_map_hash_key(set, value), value, 0);
if (checked) scr_dyn_release(scr_map_slot_to_ptr(value));
}
}
ScrArr *scr_set_to_arr_dyn(const ScrMap *map) {
static const ScrArrOps dyn_ops = { scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v };
ScrArr *out = scr_arr_new_ops(&dyn_ops, map->nlive);
for (size_t i = 0; i < map->nentries; i++) {
if (scr_map_iter_live(map, (double)i)) scr_arr_push_ref(out, scr_map_dyn_key(map, (double)i));
}
return out;
}
void *scr_map_iter_val_ref(const ScrMap *m, double i) {
return m->ops->val_retain(scr_map_slot_to_ptr(scr_map_iter_at(m, i)->val)); /* +1 */
}
/* A published (immortal) map never changes, so iterating it needs no
* compaction guard, and it must not be written: other threads read it. */
void scr_map_iter_enter(ScrMap *m) {
if (m->rc != SIZE_MAX) m->iter_depth++;
}
void scr_map_iter_exit(ScrMap *m) {
if (m->rc == SIZE_MAX) return;
if (m->iter_depth > 0) m->iter_depth--;
/* A churny callback may have left many tombstones; with no iteration
* active they are safe to drop now (bounds memory under forEach-heavy
* add/delete workloads). */
if (m->iter_depth == 0 && m->nlive <= m->nentries / 2 && m->nentries >= 16) {
scr_map_compact(m, true);
}
}
/* ── seeded Set construction ───────────────────────────────────────────── */
/* `new Set(values)`: add() every element of one borrowed T[] in order —
* duplicates overwrite the unit value in place, so first insertion
* position wins (SameValueZero, exactly JS). The map is a set (value kind
* pinned to f64, every stored value 0); elements are the set's key kind —
* f64, string, or an identity reference matching the array's element kind. */
void scr_set_add_all(ScrMap *set, ScrArr *values) {
size_t n = values->len;
for (size_t i = 0; i < n; i++) {
/* Packed values stay owned by the seed array throughout construction.
* Avoid taking a temporary owner only to release it after insertion.
* Sparse/missing positions retain the existing accessor semantics. */
if (i < values->cap && values->present[i] == SCR_ARR_VALUE) {
uint64_t key = scr_arr_dense_get(values, i);
if (set->key_kind == SCR_MAP_KEY_F64) key = scr_map_f64_bits(scr_map_slot_to_f64(key));
scr_map_set(set, scr_map_hash_key(set, key), key, 0);
} else if (values->elem == SCR_ELEM_STR) {
ScrStr *s = (ScrStr *)scr_arr_get_ref(values, (double)i); /* +1 */
scr_map_set_str_f64(set, s, 0); /* borrows; retains stored copy */
scr_str_release(s);
} else if (scr_map_ref_key(set->key_kind)) {
void *p = scr_arr_get_ref(values, (double)i); /* +1 */
scr_map_set_ref_f64(set, p, 0); /* borrows; retains stored copy */
set->ops->key_release(p);
} else if (set->key_kind == SCR_MAP_KEY_BOOL) {
scr_map_set_bool_f64(set, scr_arr_get_bool(values, (double)i), 0);
} else {
scr_map_set_f64_f64(set, scr_arr_get_f64(values, (double)i), 0);
}
}
}
/* ── JS own-key ordering over the overflow map ─────────────────────────── */
/* Canonical array index test: "0".."4294967294" — digits only, no leading
* zero (except "0" itself), value <= 2^32 - 2. JS orders these OWN keys
* first, ascending numerically, before every other string key. */
static bool scr_map_key_array_index(const ScrStr *key, uint32_t *out) {
return scr_key_array_index(key->data, key->len, out);
}
/* Enumerate numeric keys in ascending order, then other keys in insertion
* order. Scratch entries refer to stable map indices, not copied strings. */
ScrArr *scr_map_keys_js_order(const ScrMap *m) {
ScrArr *out = scr_arr_new(SCR_ELEM_STR, m->nlive);
size_t live = m->nlive;
if (live > SIZE_MAX / sizeof(ScrKeyIndex)) scr_map_oom();
ScrKeyIndex *indices = NULL;
size_t count = 0;
for (size_t i = 0; i < m->nentries; i++) {
if (!m->entries[i].hash) continue;
ScrStr *key = scr_map_slot_to_ptr(m->entries[i].key);
uint32_t index;
if (!scr_map_key_array_index(key, &index)) continue;
if (!indices) {
indices = malloc(m->nlive * sizeof *indices);
if (!indices) scr_map_oom();
}
indices[count++] = (ScrKeyIndex){index, i};
}
scr_key_index_sort(indices, count);
for (size_t i = 0; i < count; i++) {
ScrStr *key = scr_map_slot_to_ptr(m->entries[indices[i].entry].key);
scr_arr_push_ref(out, scr_str_retain(key));
}
free(indices);
for (size_t i = 0; i < m->nentries; i++) {
if (!m->entries[i].hash) continue;
ScrStr *key = scr_map_slot_to_ptr(m->entries[i].key);
uint32_t index;
if (!scr_map_key_array_index(key, &index)) scr_arr_push_ref(out, scr_str_retain(key));
}
return out;
}
/* Publication (scr_message.c): cache a heap string's key hash before it
* becomes immortal, after which no thread may write its header. */
void scr_str_hash_prime(ScrStr *s) {
if (s && s->rc != SIZE_MAX) (void)scr_map_hash_str(s);
}