diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index ca3e0719f..783f88c62 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -14354,6 +14354,18 @@ "/tests/corpus/text-encode-into-boundaries.ts" ], "diags": [] + }, + "/tests/corpus/object-key-indexes.js": { + "order": [ + "/tests/corpus/object-key-indexes.js" + ], + "diags": [] + }, + "/tests/corpus/record-key-order.ts": { + "order": [ + "/tests/corpus/record-key-order.ts" + ], + "diags": [] } } } diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 03a71ca41..c12d0a825 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -22,6 +22,7 @@ * serializers (and the error messages here). */ #include "scr_runtime.h" +#include "scr_key.h" static SCR_TL ScrDyn *scr_builtin_object_prototype; @@ -709,6 +710,10 @@ static void scr_dyn_handle_release(void *h, ScrDynHandleTag tag); static bool scr_dyn_to_primitive_result_is_object(const ScrDyn *d); static void scr_dyn_dispose(ScrDyn *d, bool collected) { + if (d->kind == SCR_DYN_OBJ) { + free(d->v.obj.index); + d->v.obj.index = NULL; + } if (!collected) scr_dyn_release(d->prototype); if (!collected) scr_dyn_release(d->symbol_properties); if (!collected) scr_dyn_release(d->symbol_keys); @@ -829,6 +834,42 @@ void scr_dyn_release(ScrDyn *d) { scr_dyn_dispose(d, false); } +/* Small objects retain their compact linear representation. A wide object + * owns a bucket index of entry positions; entries remain insertion ordered. + * No key/value ownership lives here, and callbacks never retain its slots. */ +#define SCR_DYN_OBJECT_INDEX_MIN 32 +typedef struct ScrDynObjectIndex { + size_t buckets; + size_t slots[]; /* zero is empty; occupied slots store entry + 1 */ +} ScrDynObjectIndex; + +static void scr_dyn_index_insert(ScrDynObjectIndex *index, const ScrDynEntry *entry, size_t position) { + size_t slot = scr_key_hash(entry->key, entry->key_len) & (index->buckets - 1); + while (index->slots[slot]) slot = (slot + 1) & (index->buckets - 1); + index->slots[slot] = position + 1; +} + +/* Keep index allocation/probing outside compact object insertion and reads. */ +static __attribute__((noinline)) void scr_dyn_index_append(ScrDyn *object) { + size_t count = object->v.obj.len; + ScrDynObjectIndex *index = object->v.obj.index; + if (!index || count > index->buckets / 2) { + size_t buckets = index ? index->buckets : 64; + while (count > buckets / 2) { + if (buckets > (SIZE_MAX - sizeof *index) / sizeof(size_t) / 2) scr_json_oom(); + buckets *= 2; + } + free(index); + index = calloc(1, sizeof *index + buckets * sizeof(size_t)); + if (!index) scr_json_oom(); + index->buckets = buckets; + object->v.obj.index = index; + for (size_t i = 0; i < count; i++) scr_dyn_index_insert(index, &object->v.obj.entries[i], i); + } else { + scr_dyn_index_insert(index, &object->v.obj.entries[count - 1], count - 1); + } +} + /* A checked hint avoids repeating a linear walk for recurring property * names. Slots store indices only, never object/key pointers. Every hit * checks the current entry, so different shapes, deletion and reordering @@ -836,22 +877,38 @@ void scr_dyn_release(ScrDyn *d) { #define SCR_DYN_PROPERTY_HINTS 64 static SCR_TL size_t scr_dyn_property_hints[SCR_DYN_PROPERTY_HINTS]; +static ScrDynEntry *scr_dyn_find_linear_entry(const ScrDyn *object, const char *key, size_t length) { + for (size_t i = 0; i < object->v.obj.len; i++) { + ScrDynEntry *entry = &object->v.obj.entries[i]; + if (entry->key_len == length && !memcmp(entry->key, key, length)) return entry; + } + return NULL; +} + +static __attribute__((noinline)) ScrDynEntry *scr_dyn_find_indexed_entry( + const ScrDyn *object, const char *key, size_t length) { + const ScrDynObjectIndex *index = object->v.obj.index; + size_t slot = scr_key_hash(key, length) & (index->buckets - 1); + while (index->slots[slot]) { + ScrDynEntry *entry = &object->v.obj.entries[index->slots[slot] - 1]; + if (entry->key_len == length && !memcmp(entry->key, key, length)) return entry; + slot = (slot + 1) & (index->buckets - 1); + } + return NULL; +} + static ScrDynEntry *scr_dyn_find_entry(const ScrDyn *object, const char *key, size_t length) { if (object->kind != SCR_DYN_OBJ) return NULL; + if (object->v.obj.index) return scr_dyn_find_indexed_entry(object, key, length); size_t slot = (length + (length ? (unsigned char)key[0] * 7u : 0)) % SCR_DYN_PROPERTY_HINTS; size_t hint = scr_dyn_property_hints[slot]; if (hint < object->v.obj.len) { ScrDynEntry *entry = &object->v.obj.entries[hint]; if (entry->key_len == length && !memcmp(entry->key, key, length)) return entry; } - for (size_t i = 0; i < object->v.obj.len; i++) { - ScrDynEntry *entry = &object->v.obj.entries[i]; - if (entry->key_len == length && !memcmp(entry->key, key, length)) { - scr_dyn_property_hints[slot] = i; - return entry; - } - } - return NULL; + ScrDynEntry *entry = scr_dyn_find_linear_entry(object, key, length); + if (entry) scr_dyn_property_hints[slot] = (size_t)(entry - object->v.obj.entries); + return entry; } ScrDyn *scr_dyn_obj_get(const ScrDyn *d, const char *key, size_t key_len) { @@ -1156,20 +1213,21 @@ ScrDyn *scr_dyn_arr_named_get(const ScrDyn *d, const ScrStr *key) { * original spelling/storage and insertion position; the later value wins. * A recycled entry can reuse equal key bytes without a malloc/free pair. */ static void scr_dyn_obj_put(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value) { - for (size_t i = 0; i < obj->v.obj.len; i++) { - ScrDynEntry *e = &obj->v.obj.entries[i]; - if (e->key_len == key_len && memcmp(e->key, key, key_len) == 0) { - if (e->accessor || !e->writable) { - scr_dyn_release(value); - static const char message[] = "Cannot assign to read only property"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); - return; - } - ScrDyn *old = e->value; - e->value = value; - scr_dyn_release(old); + /* Construction usually appends an absent name. Avoid a read hint that + * repeats comparisons on compact objects; wide objects use their index. */ + ScrDynEntry *existing = obj->v.obj.index ? scr_dyn_find_entry(obj, key, key_len) + : scr_dyn_find_linear_entry(obj, key, key_len); + if (existing) { + if (existing->accessor || !existing->writable) { + scr_dyn_release(value); + static const char message[] = "Cannot assign to read only property"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return; } + ScrDyn *old = existing->value; + existing->value = value; + scr_dyn_release(old); + return; } if (obj->non_extensible) { scr_dyn_release(value); @@ -1199,6 +1257,7 @@ static void scr_dyn_obj_put(ScrDyn *obj, const char *key, size_t key_len, ScrDyn e->writable = true; e->enumerable = true; e->configurable = true; + if (obj->v.obj.len >= SCR_DYN_OBJECT_INDEX_MIN) scr_dyn_index_append(obj); } /* ── dyn construction (compiler-emitted converters & overflow reads) ───── */ @@ -6332,6 +6391,8 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) { ScrDynEntry removed = *entry; memmove(entry, entry + 1, (object->v.obj.len - i - 1) * sizeof *entry); object->v.obj.len--; + free(object->v.obj.index); + object->v.obj.index = NULL; /* The moved tail aliases a live entry; spare slots own only their own * retained key bytes, never a second pointer to a surviving property. */ memset(&object->v.obj.entries[object->v.obj.len], 0, sizeof *entry); @@ -7371,15 +7432,9 @@ bool scr_dyn_reflect_set(ScrDyn *target, ScrDyn *raw_key, ScrDyn *value, ScrDyn /* The array-index test (ECMA: a canonical numeric string < 2^32-1). */ static bool scr_dyn_key_is_index(const char *key, size_t len, double *out) { - if (len == 0 || len > 10) return false; - if (key[0] == '0' && len > 1) return false; - double v = 0; - for (size_t i = 0; i < len; i++) { - if (key[i] < '0' || key[i] > '9') return false; - v = v * 10 + (key[i] - '0'); - } - if (v > 4294967294.0) return false; - *out = v; + uint32_t index; + if (!scr_key_array_index(key, len, &index)) return false; + *out = index; return true; } @@ -7397,34 +7452,31 @@ static ScrDyn *scr_dyn_objwalk_key(const char *key, size_t key_len) { ScrDyn *scr_dyn_obj_own_keys(const ScrDyn *v) { size_t n = v->v.obj.len; ScrDyn *keys = scr_dyn_new_arr(); - bool *is_index = malloc(n ? n * sizeof *is_index : 1); - double *idx = malloc(n ? n * sizeof *idx : 1); - if (!is_index || !idx) scr_trap("scriptc: out of memory\n"); - size_t index_count = 0; + ScrKeyIndex *indices = NULL; + size_t count = 0; for (size_t i = 0; i < n; i++) { - const ScrDynEntry *e = &v->v.obj.entries[i]; - is_index[i] = scr_dyn_key_is_index(e->key, e->key_len, &idx[i]); - if (is_index[i]) index_count++; - } - double last = -1; - for (size_t done = 0; done < index_count; done++) { - size_t best = (size_t)-1; - for (size_t i = 0; i < n; i++) { - if (!is_index[i] || idx[i] <= last) continue; - if (best == (size_t)-1 || idx[i] < idx[best]) best = i; + const ScrDynEntry *entry = &v->v.obj.entries[i]; + uint32_t index; + if (!scr_key_array_index(entry->key, entry->key_len, &index)) continue; + if (!indices) { + if (n > SIZE_MAX / sizeof *indices) scr_json_oom(); + indices = malloc(n * sizeof *indices); + if (!indices) scr_json_oom(); } - if (best == (size_t)-1) break; - last = idx[best]; - scr_dyn_arr_push(keys, scr_dyn_objwalk_key(v->v.obj.entries[best].key, - v->v.obj.entries[best].key_len)); + indices[count++] = (ScrKeyIndex){index, i}; } + scr_key_index_sort(indices, count); + for (size_t i = 0; i < count; i++) { + const ScrDynEntry *entry = &v->v.obj.entries[indices[i].entry]; + scr_dyn_arr_push(keys, scr_dyn_objwalk_key(entry->key, entry->key_len)); + } + free(indices); for (size_t i = 0; i < n; i++) { - if (is_index[i]) continue; - scr_dyn_arr_push(keys, scr_dyn_objwalk_key(v->v.obj.entries[i].key, - v->v.obj.entries[i].key_len)); + const ScrDynEntry *entry = &v->v.obj.entries[i]; + uint32_t index; + if (!scr_key_array_index(entry->key, entry->key_len, &index)) + scr_dyn_arr_push(keys, scr_dyn_objwalk_key(entry->key, entry->key_len)); } - free(is_index); - free(idx); return keys; } diff --git a/packages/runtime/src/scr_json.test.c b/packages/runtime/src/scr_json.test.c index 0fcfc6c39..bc97dbc76 100644 --- a/packages/runtime/src/scr_json.test.c +++ b/packages/runtime/src/scr_json.test.c @@ -114,6 +114,41 @@ static void checked_storage(void) { } scr_dyn_release(object); } + /* Cross index growth boundaries, invalidate moved entry positions, then + * recreate the index from survivors. Repeat after pool reuse. */ + for (int round = 0; round < 4; round++) { + ScrDyn *object = scr_dyn_new_obj(); + assert(!object->v.obj.index); + for (int i = 0; i < 129; i++) { + char key[16]; + int length = snprintf(key, sizeof key, "key-%d", i); + scr_dyn_obj_set(object, key, (size_t)length, scr_dyn_new_num(i)); + assert((object->v.obj.index != NULL) == (i >= 31)); + } + for (int i = 0; i < 129; i++) { + char key[16]; + int length = snprintf(key, sizeof key, "key-%d", i); + assert(scr_dyn_obj_get(object, key, (size_t)length)->v.num == i); + if (i % 3 == 0) { + ScrStr *name = scr_str_new(key, (size_t)length); + scr_dyn_key_delete(object, name, true); + scr_str_release(name); + assert(!scr_dyn_obj_get(object, key, (size_t)length)); + } + } + scr_dyn_obj_set(object, "last", 4, scr_dyn_new_num(129)); + assert(object->v.obj.index); + for (int i = 0; i < 129; i++) { + char key[16]; + int length = snprintf(key, sizeof key, "key-%d", i); + ScrDyn *value = scr_dyn_obj_get(object, key, (size_t)length); + assert(i % 3 == 0 ? value == NULL : value && value->v.num == i); + } + scr_dyn_obj_set(object, "key-128", 7, scr_dyn_new_num(900)); + assert(object->v.obj.len == 87); + assert(scr_dyn_obj_get(object, "key-128", 7)->v.num == 900); + scr_dyn_release(object); + } /* A direct step is authorized only by the captured builtin method. */ ScrDyn *source = scr_dyn_new_arr(); scr_dyn_arr_push(source, scr_dyn_new_num(7)); diff --git a/packages/runtime/src/scr_key.h b/packages/runtime/src/scr_key.h new file mode 100644 index 000000000..63c01337b --- /dev/null +++ b/packages/runtime/src/scr_key.h @@ -0,0 +1,68 @@ +/* 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 +#include +#include +#include +#include + +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); +} + +/* Unaligned loads stay inside the input. Hashing is process-local, so host + * byte order is immaterial; equal byte strings always follow the same path. */ +static inline uint64_t scr_key_hash(const char *bytes, size_t length) { + uint64_t hash = UINT64_C(0x9e3779b97f4a7c15) ^ 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; + } + uint64_t tail = 0; + if (i < length) memcpy(&tail, bytes + i, length - i); + return scr_key_mix(hash ^ tail); +} + +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 diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index 75c0b4084..3c71336f9 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -15,6 +15,7 @@ * keep the lean 1-word header and never touch the collector. */ #include "scr_runtime.h" +#include "scr_key.h" #include #include @@ -46,27 +47,14 @@ static uint64_t scr_map_f64_bits(double k) { return bits; } -static uint64_t scr_map_fnv1a(const unsigned char *bytes, size_t n) { - uint64_t h = UINT64_C(0xcbf29ce484222325); - for (size_t i = 0; i < n; i++) { - h ^= bytes[i]; - h *= UINT64_C(0x100000001b3); - } - return h; -} - static uint64_t scr_map_hash_str(const ScrStr *k) { - return scr_map_fnv1a((const unsigned char *)k->data, k->len); + return scr_key_hash(k->data, k->len); } /* 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) { - value ^= value >> 30; - value *= UINT64_C(0xbf58476d1ce4e5b9); - value ^= value >> 27; - value *= UINT64_C(0x94d049bb133111eb); - return value ^ (value >> 31); + return scr_key_mix(value); } /* ── slot packing (8-byte slots, like ScrArr) ──────────────────────────── */ @@ -906,59 +894,39 @@ void scr_set_add_all(ScrMap *set, ScrArr *values) { /* 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 *k, uint32_t *out) { - size_t n = k->len; - if (n == 0 || n > 10) return false; - const char *s = k->data; - if (s[0] == '0' && n > 1) return false; - uint64_t v = 0; - for (size_t i = 0; i < n; i++) { - if (s[i] < '0' || s[i] > '9') return false; - v = v * 10 + (uint64_t)(s[i] - '0'); - } - if (v > UINT64_C(4294967294)) return false; - *out = (uint32_t)v; - return true; +static bool scr_map_key_array_index(const ScrStr *key, uint32_t *out) { + return scr_key_array_index(key->data, key->len, out); } -/* The live STRING keys of `m` in JS OWN-KEY ORDER (Object.keys over the - * index-signature overflow): integer-like keys (canonical array indices) - * first in ascending numeric order, then the rest in insertion order. - * Returns a fresh ScrStr* array (+1); the map is borrowed. Insertion sort - * over the integer-like subset — overflow maps are small, and ties are - * impossible (keys are unique). */ +/* 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) { - size_t n = m->nentries; ScrArr *out = scr_arr_new(SCR_ELEM_STR, m->nlive); - size_t nidx = 0; - struct { uint32_t v; ScrStr *k; } *idx = NULL; - for (size_t i = 0; i < n; i++) { + if (m->nlive > 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 *k = (ScrStr *)scr_map_slot_to_ptr(m->entries[i].key); - uint32_t v; - if (!scr_map_key_array_index(k, &v)) continue; - if (nidx % 16 == 0) { - idx = realloc(idx, (nidx + 16) * sizeof *idx); - if (!idx) scr_map_oom(); + 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(); } - size_t j = nidx++; - while (j > 0 && idx[j - 1].v > v) { - idx[j] = idx[j - 1]; - j--; - } - idx[j].v = v; - idx[j].k = k; + indices[count++] = (ScrKeyIndex){index, i}; } - for (size_t i = 0; i < nidx; i++) { - scr_arr_push_ref(out, scr_str_retain(idx[i].k)); + 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(idx); - for (size_t i = 0; i < n; i++) { + free(indices); + for (size_t i = 0; i < m->nentries; i++) { if (!m->entries[i].hash) continue; - ScrStr *k = (ScrStr *)scr_map_slot_to_ptr(m->entries[i].key); - uint32_t v; - if (scr_map_key_array_index(k, &v)) continue; - scr_arr_push_ref(out, scr_str_retain(k)); + 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; } diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 855724f35..0faa828a2 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -3759,6 +3759,7 @@ struct ScrDyn { * interfaces may request a fresh snapshot with the true arm. */ void *source_identity; ScrDyn *(*source_access)(void *, bool materialize); + struct ScrDynObjectIndex *index; /* optional wide-object lookup; owns no values */ } obj; /* owned */ /* SCR_DYN_FUNC: the boxed closure (owned) + its call descriptor. `sig` * and `name` are static compiler-emitted literals (never freed); name diff --git a/tests/corpus/object-key-indexes.js b/tests/corpus/object-key-indexes.js new file mode 100644 index 000000000..36fbe1af8 --- /dev/null +++ b/tests/corpus/object-key-indexes.js @@ -0,0 +1,30 @@ +// Wide checked objects preserve insertion order, descriptor behavior, and +// lookups when entries move after deletion or are replaced by accessors. +const object = JSON.parse("{}"); +for (let i = 95; i >= 0; i--) object[String(i)] = i * 3; +for (const key of ["tail", "01", "4294967295", "4294967294", "-0", "", "a\0b", "é😀"]) object[key] = key; +console.log(Object.keys(object).join("|")); +console.log(Object.values(object).join("|")); +for (let i = 0; i < 96; i += 3) delete object[String(i)]; +for (let i = 0; i < 96; i += 3) object[String(i)] = -i; +object.tail = "updated"; +Object.defineProperty(object, "hidden", { value: 42, enumerable: false, configurable: true }); +Object.defineProperty(object, "31", { get() { delete object["32"]; object.added = 123; return "getter"; }, enumerable: true, configurable: true }); +console.log(Object.entries(object).map(([key, value]) => key + ":" + value).join("|")); +console.log(Reflect.ownKeys(object).join("|")); +console.log(object.hidden, object.missing, object["a\0b"], object["é😀"]); +for (let i = 0; i < 96; i++) delete object[String(i)]; +console.log(Object.keys(object).join("|")); +for (let i = 0; i < 100; i++) object["key-" + i] = i; +Object.freeze(object); +try { object["key-50"] = 0; } catch (error) { console.log(error.name); } +console.log(object["key-50"], Object.keys(object).length); +const duplicates = JSON.parse('{"32":1,"1":2,"32":3,"x":4,"x":5}'); +console.log(JSON.stringify(duplicates)); +// Recycling and cyclic destruction must discard all index storage. +for (let round = 0; round < 40; round++) { + const cycle = JSON.parse("{}"); + for (let i = 0; i < 80; i++) cycle["property-" + i] = round + i; + cycle.self = cycle; + console.log(cycle["property-79"], Object.keys(cycle).length); +} diff --git a/tests/corpus/record-key-order.ts b/tests/corpus/record-key-order.ts new file mode 100644 index 000000000..38f587c34 --- /dev/null +++ b/tests/corpus/record-key-order.ts @@ -0,0 +1,24 @@ +const record: Record = {}; +for (let i = 96; i >= 0; i--) record[String(i)] = i; +for (const key of ["word", "01", "4294967295", "4294967294", "-0", "", "a\0b", "é😀"]) record[key] = 100; +console.log(Object.keys(record).join("|")); +console.log(Object.values(record).join("|")); +console.log(Object.entries(record).map(([key, value]) => key + ":" + value).join("|")); +const map = new Map(); +for (let length = 0; length <= 40; length++) { + const prefix = "same-prefix-".repeat(length); + for (const tail of ["a", "b", "\0", "é", "😀"]) map.set(prefix + tail, length); +} +let sum = 0; +for (const [key, value] of map) { + const copy = ("!" + key).slice(1); + console.log(map.has(copy), map.get(copy), value); + sum += map.get(copy)!; +} +console.log(map.size, sum); +for (let i = 0; i < 200; i++) { + const key = "churn-" + i; + map.set(key, i); + if (i >= 7) map.delete("churn-" + (i - 7)); +} +console.log(Array.from(map.keys()).slice(-7).join("|")); diff --git a/tests/harness/wasi-differential.test.ts b/tests/harness/wasi-differential.test.ts index 4532baf3c..5c766419b 100644 --- a/tests/harness/wasi-differential.test.ts +++ b/tests/harness/wasi-differential.test.ts @@ -146,6 +146,8 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => { "uint8array-sort-domains.ts", "buffer-codec-boundaries.ts", "text-encode-into-boundaries.ts", + "object-key-indexes.js", + "record-key-order.ts", "1612-cjs-module-globals.cjs", "992-fs-roundtrip.ts", "751-cycle-records-mutual.ts",