Files
scriptc/packages/runtime/src/scr_key.h
T
Malte Ubl f7ac13e87f Cache string Map key hashes in the string header
On 64-bit little-endian targets ScrStr's capacity word is split into a
32-bit cap and a 32-bit cached Map key hash (header stays 24 bytes).
Heap strings fill the hash on first lookup; every in-place mutation path
(unique concat, split scratch reuse, regrow, spare reuse) clears it.
Immortal strings are never written: compiler literals and the runtime's
one-character/empty statics carry the hash precomputed (string-key-hash.ts
mirrors scr_key_hash; both sides pin the same vectors).

scr_key_hash seeds by length multiplicatively: XORing the length into the
low bits cancelled against the last byte ("ab" and "abc" collided).

Map string lookups inline the probe into each getter and reject linear
entries by hash; set specializes its probe/overwrite by lookup kind with a
shared append path.

Tests: runtime map.test.ts (pinned vectors, statics, in-place append
invalidation), string-key-hash.test.ts, corpus 3141-map-string-key-hash
(plain + sanitized; mutation-checked against both invalidation paths).
2026-10-08 15:40:53 -05:00

109 lines
4.4 KiB
C

/* Internal key hashing and canonical array-index ordering shared by native
* maps and checked objects. Hashes never escape into JavaScript values. */
#ifndef SCR_KEY_H
#define SCR_KEY_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
static inline uint64_t scr_key_mix(uint64_t value) {
value ^= value >> 30;
value *= UINT64_C(0xbf58476d1ce4e5b9);
value ^= value >> 27;
value *= UINT64_C(0x94d049bb133111eb);
return value ^ (value >> 31);
}
/* Pack at most eight bytes with fixed-size, unaligned loads. The overlapping
* ends cover every byte at lengths four through eight; the three selected
* bytes cover lengths one through three. No terminator or padding is read. */
static inline uint64_t scr_key_short_word(const char *bytes, size_t length) {
if (length >= 4) {
uint32_t first, last;
memcpy(&first, bytes, sizeof first);
memcpy(&last, bytes + length - sizeof last, sizeof last);
return ((uint64_t)first << 32) | last;
}
if (length == 0) return 0;
return ((uint64_t)(unsigned char)bytes[0] << 16) |
((uint64_t)(unsigned char)bytes[length / 2] << 8) |
(unsigned char)bytes[length - 1];
}
/* Callers check equal lengths first. Short keys need no out-of-line memcmp;
* longer keys keep the platform's bulk comparison. */
static inline bool scr_key_equal(const char *a, const char *b, size_t length) {
if (a == b) return true;
if (length <= 8) return scr_key_short_word(a, length) == scr_key_short_word(b, length);
return memcmp(a, b, length) == 0;
}
/* Unaligned loads stay inside the input. Hashing is process-local, so host
* byte order is immaterial; equal byte strings always follow the same path.
* Short keys need one avalanche and no variable-size tail copy. The length
* selects a full-width seed: XORing it into the low bits would cancel
* against the last byte ("ab" and "abc" used to collide). The compiler
* mirrors this function for literals (string-key-hash.ts). */
static inline uint64_t scr_key_hash(const char *bytes, size_t length) {
uint64_t hash = UINT64_C(0x9e3779b97f4a7c15) * ((uint64_t)length + 1);
if (length <= 8) return scr_key_mix(hash ^ scr_key_short_word(bytes, length));
size_t i = 0;
while (length - i >= sizeof(uint64_t)) {
uint64_t word;
memcpy(&word, bytes + i, sizeof word);
hash = scr_key_mix(hash ^ word);
i += sizeof word;
}
return scr_key_mix(hash ^ scr_key_short_word(bytes + i, length - i));
}
/* Integer-constant forms of the string key hash for static strings of at
* most three bytes (word = scr_key_short_word of the bytes), folded to the
* 32-bit value native maps cache (scr_map_hash_fold). Static initializers
* cannot call functions; these expand to plain arithmetic. */
#define SCR_KEY_MIX1_(v) (((v) ^ ((v) >> 30)) * UINT64_C(0xbf58476d1ce4e5b9))
#define SCR_KEY_MIX2_(v) (((v) ^ ((v) >> 27)) * UINT64_C(0x94d049bb133111eb))
#define SCR_KEY_MIX3_(v) ((v) ^ ((v) >> 31))
#define SCR_KEY_MIX_CONST(v) SCR_KEY_MIX3_(SCR_KEY_MIX2_(SCR_KEY_MIX1_(v)))
#define SCR_KEY_FOLD_(h) ((uint32_t)((h) ^ ((h) >> 32)))
#define SCR_KEY_HASH32_(h) (SCR_KEY_FOLD_(h) ? SCR_KEY_FOLD_(h) : UINT32_C(1))
#define SCR_KEY_HASH32_SHORT(length, word) \
SCR_KEY_HASH32_(SCR_KEY_MIX_CONST((UINT64_C(0x9e3779b97f4a7c15) * ((uint64_t)(length) + 1)) ^ (uint64_t)(word)))
static inline bool scr_key_array_index(const char *key, size_t length, uint32_t *out) {
if (length == 0 || length > 10 || (length > 1 && key[0] == '0')) return false;
uint64_t value = 0;
for (size_t i = 0; i < length; i++) {
if (key[i] < '0' || key[i] > '9') return false;
value = value * 10 + (unsigned)(key[i] - '0');
}
if (value >= UINT32_MAX) return false;
*out = (uint32_t)value;
return true;
}
typedef struct {
uint32_t index;
size_t entry;
} ScrKeyIndex;
static inline int scr_key_index_compare(const void *left, const void *right) {
uint32_t a = ((const ScrKeyIndex *)left)->index;
uint32_t b = ((const ScrKeyIndex *)right)->index;
return (a > b) - (a < b);
}
/* Keys are unique. Already ordered inputs need no sort, while arbitrary
* numeric property insertion must not require quadratic enumeration. */
static inline void scr_key_index_sort(ScrKeyIndex *keys, size_t count) {
for (size_t i = 1; i < count; i++) {
if (keys[i - 1].index > keys[i].index) {
qsort(keys, count, sizeof *keys, scr_key_index_compare);
return;
}
}
}
#endif