From c01a887d18a0973a50496b2e1fc9f10d64533c81 Mon Sep 17 00:00:00 2001 From: theMackabu Date: Tue, 5 May 2026 12:22:52 -0700 Subject: [PATCH] fix own-key ordering regressions and Object.assign cleanup --- examples/results.txt | 10 +- examples/spec/json.js | 20 + examples/spec/objects.js | 68 +++ examples/spec/reflect.js | 45 ++ include/internal.h | 1 + src/ant.c | 668 +++++++++++++++++++++++++++--- src/errors.c | 12 +- src/modules/reflect.c | 13 +- tests/bench_forin_object_keys.js | 201 +++++++++ tests/bench_own_property_order.js | 228 ++++++++++ 10 files changed, 1186 insertions(+), 80 deletions(-) create mode 100644 tests/bench_forin_object_keys.js create mode 100644 tests/bench_own_property_order.js diff --git a/examples/results.txt b/examples/results.txt index 4f5d828..957b1bd 100644 --- a/examples/results.txt +++ b/examples/results.txt @@ -1030,12 +1030,12 @@ compat-table/es6/misc.identifier-syntax.no-escaped-reserved.js: OK compat-table/es6/misc.identifier-syntax.var-1.js: OK compat-table/es6/misc.identifier-syntax.var-2.js: OK compat-table/es6/misc.property-order.JSON.parse.js: OK -compat-table/es6/misc.property-order.JSON.stringify.js: failed -compat-table/es6/misc.property-order.Object.assign.js: failed -compat-table/es6/misc.property-order.Object.getOwnPropertyNames.js: failed +compat-table/es6/misc.property-order.JSON.stringify.js: OK +compat-table/es6/misc.property-order.Object.assign.js: OK +compat-table/es6/misc.property-order.Object.getOwnPropertyNames.js: OK compat-table/es6/misc.property-order.Object.keys.js: OK -compat-table/es6/misc.property-order.Reflect.ownKeys.string.js: failed -compat-table/es6/misc.property-order.Reflect.ownKeys.symbol.js: failed +compat-table/es6/misc.property-order.Reflect.ownKeys.string.js: OK +compat-table/es6/misc.property-order.Reflect.ownKeys.symbol.js: OK compat-table/es6/misc.prototypes-not-instances.js: OK compat-table/es6/new-target.assignment-error.js: OK compat-table/es6/new-target.constructor.js: OK diff --git a/examples/spec/json.js b/examples/spec/json.js index 64b68a2..c124396 100644 --- a/examples/spec/json.js +++ b/examples/spec/json.js @@ -50,6 +50,26 @@ test('stringify bool true', withTypes.includes('true'), true); test('stringify bool false', withTypes.includes('false'), true); test('stringify null', withTypes.includes('null'), true); +const ordered = { + 2: true, + 0: true, + 1: true, + ' ': true, + 9: true, + D: true, + B: true, + '-1': true +}; +ordered.A = true; +ordered[3] = true; +'EFGHIJKLMNOPQRSTUVWXYZ'.split('').forEach(key => ordered[key] = true); +Object.defineProperty(ordered, 'C', { value: true, enumerable: true }); +Object.defineProperty(ordered, '4', { value: true, enumerable: true }); +delete ordered[2]; +ordered[2] = true; +test('stringify object property order', JSON.stringify(ordered), + '{"0":true,"1":true,"2":true,"3":true,"4":true,"9":true," ":true,"D":true,"B":true,"-1":true,"A":true,"E":true,"F":true,"G":true,"H":true,"I":true,"J":true,"K":true,"L":true,"M":true,"N":true,"O":true,"P":true,"Q":true,"R":true,"S":true,"T":true,"U":true,"V":true,"W":true,"X":true,"Y":true,"Z":true,"C":true}'); + let original = '{"count":5}'; let parsed = JSON.parse(original); parsed.count = 10; diff --git a/examples/spec/objects.js b/examples/spec/objects.js index 98c5d5d..b2d4041 100644 --- a/examples/spec/objects.js +++ b/examples/spec/objects.js @@ -27,6 +27,74 @@ let merged = Object.assign({}, { a: 1 }, { b: 2 }); test('Object.assign a', merged.a, 1); test('Object.assign b', merged.b, 2); +const ordered = { + 2: true, + 0: true, + 1: true, + ' ': true, + 9: true, + D: true, + B: true, + '-1': true +}; +ordered.A = true; +ordered[3] = true; +'EFGHIJKLMNOPQRSTUVWXYZ'.split('').forEach(key => ordered[key] = true); +Object.defineProperty(ordered, 'C', { value: true, enumerable: true }); +Object.defineProperty(ordered, '4', { value: true, enumerable: true }); +delete ordered[2]; +ordered[2] = true; + +test('Object.getOwnPropertyNames order', Object.getOwnPropertyNames(ordered).join(''), + '012349 DB-1AEFGHIJKLMNOPQRSTUVWXYZC'); + +const overflowNameOrder = { a: 1 }; +overflowNameOrder['18446744073709551616'] = 2; +test('Object.getOwnPropertyNames overflow-like key order', + Object.getOwnPropertyNames(overflowNameOrder).join('|'), 'a|18446744073709551616'); + +let assignOrder = ''; +const assignTarget = {}; +'012349 DBACEFGHIJKLMNOPQRST'.split('').concat(-1).forEach(key => { + Object.defineProperty(assignTarget, key, { + set() { + assignOrder += key; + } + }); +}); +const assignSource = { 2: 2, 0: 0, 1: 1, ' ': ' ', 9: 9, D: 'D', B: 'B', '-1': '-1' }; +Object.defineProperty(assignSource, 'A', { value: 'A', enumerable: true }); +Object.defineProperty(assignSource, '3', { value: '3', enumerable: true }); +Object.defineProperty(assignSource, 'C', { value: 'C', enumerable: true }); +Object.defineProperty(assignSource, '4', { value: '4', enumerable: true }); +delete assignSource[2]; +assignSource[2] = true; +'EFGHIJKLMNOPQRST'.split('').forEach(key => assignSource[key] = key); +Object.assign(assignTarget, assignSource); +test('Object.assign property order', assignOrder, '012349 DB-1ACEFGHIJKLMNOPQRST'); + +const assignedArray = Object.assign({}, [10, 20]); +test('Object.assign dense array source 0', assignedArray[0], 10); +test('Object.assign dense array source 1', assignedArray[1], 20); + +const sparseArraySource = []; +Object.defineProperty(sparseArraySource, '2', { value: 2, enumerable: true }); +const assignedSparseArray = Object.assign({}, sparseArraySource); +test('Object.assign sparse array source index', assignedSparseArray[2], 2); + +let overflowAssignOrder = ''; +const overflowAssignTarget = {}; +Object.defineProperty(overflowAssignTarget, 'a', { set() { overflowAssignOrder += 'a'; } }); +Object.defineProperty(overflowAssignTarget, '18446744073709551616', { + set() { + overflowAssignOrder += 'h'; + } +}); +const overflowAssignSource = { a: 1 }; +overflowAssignSource['18446744073709551616'] = 2; +Object.assign(overflowAssignTarget, overflowAssignSource); +test('Object.assign overflow-like key order', overflowAssignOrder, 'ah'); + let nested = { a: { b: { c: 1 } } }; test('nested access', nested.a.b.c, 1); diff --git a/examples/spec/reflect.js b/examples/spec/reflect.js index 77a921c..1c2dd53 100644 --- a/examples/spec/reflect.js +++ b/examples/spec/reflect.js @@ -16,6 +16,51 @@ Reflect.deleteProperty(obj, 'c'); test('Reflect.deleteProperty', obj.c, undefined); testDeep('Reflect.ownKeys', Reflect.ownKeys({ x: 1, y: 2 }), ['x', 'y']); +testDeep('Reflect.ownKeys array', Reflect.ownKeys([1]), ['0', 'length']); + +const ordered = { + 2: true, + 0: true, + 1: true, + ' ': true, + 9: true, + D: true, + B: true, + '-1': true +}; +ordered.A = true; +ordered[3] = true; +'EFGHIJKLMNOPQRSTUVWXYZ'.split('').forEach(key => ordered[key] = true); +Object.defineProperty(ordered, 'C', { value: true, enumerable: true }); +Object.defineProperty(ordered, '4', { value: true, enumerable: true }); +delete ordered[2]; +ordered[2] = true; +test('Reflect.ownKeys string order', Reflect.ownKeys(ordered).join(''), '012349 DB-1AEFGHIJKLMNOPQRSTUVWXYZC'); + +const sparseArray = []; +Object.defineProperty(sparseArray, '2', { value: 2, enumerable: true }); +testDeep('Reflect.ownKeys sparse array', Reflect.ownKeys(sparseArray), ['2', 'length']); + +const overflowKeyOrder = { a: 1 }; +overflowKeyOrder['18446744073709551616'] = 2; +test('Reflect.ownKeys overflow-like key order', + Reflect.ownKeys(overflowKeyOrder).join('|'), 'a|18446744073709551616'); + +const sym1 = Symbol(); +const sym2 = Symbol(); +const sym3 = Symbol(); +const symbolOrder = { 1: true, A: true }; +symbolOrder.B = true; +symbolOrder[sym1] = true; +symbolOrder[2] = true; +symbolOrder[sym2] = true; +Object.defineProperty(symbolOrder, 'C', { value: true, enumerable: true }); +Object.defineProperty(symbolOrder, sym3, { value: true, enumerable: true }); +Object.defineProperty(symbolOrder, 'D', { value: true, enumerable: true }); +const symbolKeys = Reflect.ownKeys(symbolOrder); +test('Reflect.ownKeys symbol order 1', symbolKeys[symbolKeys.length - 3], sym1); +test('Reflect.ownKeys symbol order 2', symbolKeys[symbolKeys.length - 2], sym2); +test('Reflect.ownKeys symbol order 3', symbolKeys[symbolKeys.length - 1], sym3); function Point(x, y) { this.x = x; diff --git a/include/internal.h b/include/internal.h index 6d2d5d2..7a7f3d0 100644 --- a/include/internal.h +++ b/include/internal.h @@ -448,6 +448,7 @@ ant_value_t rope_flatten(ant_t *js, ant_value_t rope); ant_value_t str_materialize(ant_t *js, ant_value_t value); ant_value_t js_for_in_keys(ant_t *js, ant_value_t obj); +ant_value_t js_own_property_keys(ant_t *js, ant_value_t obj, bool include_symbols, bool enumerable_only); ant_value_t js_delete_prop(ant_t *js, ant_value_t obj, const char *key, size_t len); ant_value_t js_delete_sym_prop(ant_t *js, ant_value_t obj, ant_value_t sym); diff --git a/src/ant.c b/src/ant.c index 349ca2f..a1498a8 100644 --- a/src/ant.c +++ b/src/ant.c @@ -6505,6 +6505,253 @@ static ant_value_t object_enum(ant_t *js, ant_value_t obj, enum obj_enum_mode mo return mkval(T_ARR, vdata(arr)); } +typedef struct { + uint32_t slot; + unsigned long index; +} own_index_key_t; + +static bool own_slot_list_push(uint32_t **slots, uint32_t *count, uint32_t *cap, uint32_t *stack, uint32_t slot) { + if (*count == *cap) { + uint32_t next_cap = *cap * 2; + uint32_t *next = NULL; + if (*slots == stack) { + next = calloc(next_cap, sizeof(*next)); + if (next) memcpy(next, *slots, *count * sizeof(*next)); + } else { + next = realloc(*slots, next_cap * sizeof(*next)); + } + if (!next) return false; + *slots = next; + *cap = next_cap; + } + (*slots)[(*count)++] = slot; + return true; +} + +static bool own_key_is_array_index(const char *key, size_t key_len, unsigned long *out_idx) { + if (key_len == 0) return false; + if (key[0] < '0' || key[0] > '9') return false; + if (key_len > 1 && key[0] == '0') return false; + if (key_len == 1) { + if (out_idx) *out_idx = (unsigned long)(key[0] - '0'); + return true; + } + if (key_len > 10) return false; + + unsigned long parsed = (unsigned long)(key[0] - '0'); + for (size_t i = 1; i < key_len; i++) { + if (key[i] < '0' || key[i] > '9') return false; + unsigned digit = (unsigned)(key[i] - '0'); + if (parsed > ((unsigned long)UINT32_MAX - digit) / 10UL) return false; + parsed = parsed * 10UL + digit; + } + if (parsed >= (unsigned long)UINT32_MAX) return false; + if (out_idx) *out_idx = parsed; + return true; +} + +static bool own_key_is_dense_shadow(ant_t *js, ant_value_t obj, const char *key, size_t key_len) { + ant_offset_t doff = get_dense_buf(obj); + if (!doff) return false; + + unsigned long idx = 0; + if (!own_key_is_array_index(key, key_len, &idx)) return false; + if (idx >= (unsigned long)dense_iterable_length(js, obj)) return false; + return !is_empty_slot(dense_get(doff, (ant_offset_t)idx)); +} + +static bool own_key_is_enumerable(ant_t *js, ant_value_t obj, ant_object_t *ptr, uint32_t slot) { + if (!ptr || !ptr->shape) return false; + bool enumerable = (ant_shape_get_attrs(ptr->shape, slot) & ANT_PROP_ATTR_ENUMERABLE) != 0; + const ant_shape_prop_t *prop = ant_shape_prop_at(ptr->shape, slot); + if (!prop) return false; + + if (prop->type == ANT_SHAPE_KEY_SYMBOL) { + prop_meta_t meta; + if (lookup_symbol_prop_meta(obj, prop->key.sym_off, &meta)) enumerable = meta.enumerable; + return enumerable; + } + + if (ptr->is_exotic) { + descriptor_entry_t *desc = lookup_descriptor(js_as_obj(obj), prop->key.interned, strlen(prop->key.interned)); + if (desc) enumerable = desc->enumerable; + } + return enumerable; +} + +ant_value_t js_own_property_keys(ant_t *js, ant_value_t obj, bool include_symbols, bool enumerable_only) { + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, obj); + + ant_value_t arr = mkarr(js); + GC_ROOT_PIN(js, arr); + ant_offset_t out_idx = 0; + + if (vtype(obj) == T_CFUNC) { + ant_value_t promoted = js_cfunc_lookup_promoted(js, obj); + if (vtype(promoted) == T_FUNC) obj = promoted; + } + if (vtype(obj) == T_FUNC) obj = js_func_obj(obj); + if (vtype(obj) != T_OBJ && vtype(obj) != T_ARR) goto done; + + ant_object_t *ptr = js_obj_ptr(obj); + if (!ptr || !ptr->shape) goto done; + + if (ptr->is_exotic && ptr->exotic_keys && !include_symbols) { + ant_value_t keys = ptr->exotic_keys(js, obj); + GC_ROOT_RESTORE(js, root_mark); + return keys; + } + + bool is_arr = (vtype(obj) == T_ARR); + uint32_t count = ant_shape_count(ptr->shape); + + if (!is_arr) { + bool has_index_key = false; + for (uint32_t i = 0; i < count; i++) { + const ant_shape_prop_t *prop = ant_shape_prop_at(ptr->shape, i); + if (!prop || prop->type != ANT_SHAPE_KEY_STRING) continue; + if (i >= ptr->prop_count) continue; + if (enumerable_only && !own_key_is_enumerable(js, obj, ptr, i)) continue; + + const char *key = prop->key.interned; + size_t key_len = strlen(key); + if (own_key_is_array_index(key, key_len, NULL)) { + has_index_key = true; + break; + } + arr_set(js, arr, out_idx++, js_mkstr(js, key, key_len)); + } + + if (!has_index_key) { + if (include_symbols) { + for (uint32_t i = 0; i < count; i++) { + const ant_shape_prop_t *prop = ant_shape_prop_at(ptr->shape, i); + if (!prop || prop->type != ANT_SHAPE_KEY_SYMBOL) continue; + if (i >= ptr->prop_count) continue; + if (enumerable_only && !own_key_is_enumerable(js, obj, ptr, i)) continue; + arr_set(js, arr, out_idx++, mkval(T_SYMBOL, prop->key.sym_off)); + } + } + goto done; + } + + arr = mkarr(js); + GC_ROOT_PIN(js, arr); + out_idx = 0; + } + + if (is_arr) { + ant_offset_t doff = get_dense_buf(obj); + ant_offset_t dense_len = doff ? dense_iterable_length(js, obj) : 0; + for (ant_offset_t i = 0; i < dense_len; i++) { + if (is_empty_slot(dense_get(doff, i))) continue; + char idxstr[16]; + size_t idxlen = uint_to_str(idxstr, sizeof(idxstr), (unsigned)i); + arr_set(js, arr, out_idx++, js_mkstr(js, idxstr, idxlen)); + } + } + + own_index_key_t stack_indices[32]; + own_index_key_t *indices = stack_indices; + uint32_t index_count = 0; + uint32_t index_cap = (uint32_t)(sizeof(stack_indices) / sizeof(stack_indices[0])); + uint32_t stack_string_slots[64]; + uint32_t *string_slots = stack_string_slots; + uint32_t string_count = 0; + uint32_t string_cap = (uint32_t)(sizeof(stack_string_slots) / sizeof(stack_string_slots[0])); + ant_offset_t dense_shadow_len = is_arr && get_dense_buf(obj) ? dense_iterable_length(js, obj) : 0; + ant_offset_t dense_shadow_buf = is_arr ? get_dense_buf(obj) : 0; + + for (uint32_t i = 0; i < count; i++) { + const ant_shape_prop_t *prop = ant_shape_prop_at(ptr->shape, i); + if (!prop || prop->type != ANT_SHAPE_KEY_STRING) continue; + if (i >= ptr->prop_count) continue; + if (enumerable_only && !own_key_is_enumerable(js, obj, ptr, i)) continue; + + const char *key = prop->key.interned; + size_t key_len = strlen(key); + unsigned long parsed = 0; + if (!own_key_is_array_index(key, key_len, &parsed)) { + if (!own_slot_list_push(&string_slots, &string_count, &string_cap, stack_string_slots, i)) { + if (indices != stack_indices) free(indices); + if (string_slots != stack_string_slots) free(string_slots); + arr = js_mkerr(js, "oom"); + goto done; + } + continue; + } + + if ( + dense_shadow_buf && + parsed < (unsigned long)dense_shadow_len && + !is_empty_slot(dense_get(dense_shadow_buf, (ant_offset_t)parsed)) + ) continue; + + if (index_count == index_cap) { + uint32_t next_cap = index_cap * 2; + own_index_key_t *next = NULL; + if (indices == stack_indices) { + next = calloc(next_cap, sizeof(*next)); + if (next) memcpy(next, indices, index_count * sizeof(*next)); + } else { + next = realloc(indices, next_cap * sizeof(*next)); + } + if (!next) { + if (indices != stack_indices) free(indices); + if (string_slots != stack_string_slots) free(string_slots); + arr = js_mkerr(js, "oom"); + goto done; + } + indices = next; + index_cap = next_cap; + } + + uint32_t pos = index_count++; + while (pos > 0 && indices[pos - 1].index > parsed) { + indices[pos] = indices[pos - 1]; + pos--; + } + indices[pos] = (own_index_key_t){ .slot = i, .index = parsed }; + } + + for (uint32_t i = 0; i < index_count; i++) { + const ant_shape_prop_t *prop = ant_shape_prop_at(ptr->shape, indices[i].slot); + if (!prop) continue; + const char *key = prop->key.interned; + arr_set(js, arr, out_idx++, js_mkstr(js, key, strlen(key))); + } + + if (is_arr && !enumerable_only) { + arr_set(js, arr, out_idx++, js->length_str); + } + + for (uint32_t i = 0; i < string_count; i++) { + const ant_shape_prop_t *prop = ant_shape_prop_at(ptr->shape, string_slots[i]); + if (!prop) continue; + const char *key = prop->key.interned; + size_t key_len = strlen(key); + arr_set(js, arr, out_idx++, js_mkstr(js, key, key_len)); + } + + if (include_symbols) { + for (uint32_t i = 0; i < count; i++) { + const ant_shape_prop_t *prop = ant_shape_prop_at(ptr->shape, i); + if (!prop || prop->type != ANT_SHAPE_KEY_SYMBOL) continue; + if (i >= ptr->prop_count) continue; + if (enumerable_only && !own_key_is_enumerable(js, obj, ptr, i)) continue; + arr_set(js, arr, out_idx++, mkval(T_SYMBOL, prop->key.sym_off)); + } + } + + if (indices != stack_indices) free(indices); + if (string_slots != stack_string_slots) free(string_slots); + +done: + GC_ROOT_RESTORE(js, root_mark); + return arr; +} + // TODO: reduce nesting static ant_value_t proxy_enum(ant_t *js, ant_value_t obj, enum obj_enum_mode mode) { GC_ROOT_SAVE(root_mark, js); @@ -6656,7 +6903,7 @@ static ant_value_t builtin_object_keys(ant_t *js, ant_value_t *args, int nargs) if (vtype(obj) != T_OBJ && vtype(obj) != T_ARR && vtype(obj) != T_FUNC) return mkarr(js); if (is_proxy(obj)) return proxy_enum(js, obj, OBJ_ENUM_KEYS); - return object_enum(js, obj, OBJ_ENUM_KEYS); + return js_own_property_keys(js, obj, false, true); } static ant_value_t for_in_keys_add(ant_t *js, ant_value_t out, ant_value_t seen, ant_value_t key) { @@ -6685,6 +6932,38 @@ done: return js_mkundef(); } +static ant_value_t for_in_keys_add_cstr( + ant_t *js, ant_value_t out, ant_value_t seen, const char *key, size_t key_len, bool enumerable +) { + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, out); + GC_ROOT_PIN(js, seen); + + const char *interned = intern_string(key, key_len); + if (!interned) { + GC_ROOT_RESTORE(js, root_mark); + return js_mkerr(js, "oom"); + } + + if (lkp_interned(js, seen, interned, key_len) != 0) goto done; + + ant_value_t mark = mkprop_interned(js, seen, interned, js_true, 0); + if (is_err(mark)) { + GC_ROOT_RESTORE(js, root_mark); + return mark; + } + + if (enumerable && vtype(out) == T_ARR) { + ant_value_t key_val = js_mkstr(js, key, key_len); + GC_ROOT_PIN(js, key_val); + js_arr_push(js, out, key_val); + } + +done: + GC_ROOT_RESTORE(js, root_mark); + return js_mkundef(); +} + static ant_value_t for_in_keys_add_string_indices(ant_t *js, ant_value_t out, ant_value_t seen, ant_value_t str) { GC_ROOT_SAVE(root_mark, js); GC_ROOT_PIN(js, out); @@ -6707,6 +6986,130 @@ static ant_value_t for_in_keys_add_string_indices(ant_t *js, ant_value_t out, an return js_mkundef(); } +static ant_value_t for_in_keys_collect_ordinary_own( + ant_t *js, ant_value_t out, ant_value_t seen, ant_value_t obj, ant_object_t *ptr +) { + bool is_arr = (vtype(obj) == T_ARR); + uint32_t count = ant_shape_count(ptr->shape); + ant_value_t r = js_mkundef(); + + if (!is_arr) { + bool has_index_key = false; + for (uint32_t i = 0; i < count; i++) { + const ant_shape_prop_t *prop = ant_shape_prop_at(ptr->shape, i); + if (!prop || prop->type != ANT_SHAPE_KEY_STRING) continue; + if (i >= ptr->prop_count) continue; + const char *key = prop->key.interned; + if (own_key_is_array_index(key, strlen(key), NULL)) { + has_index_key = true; + break; + } + } + + if (!has_index_key) { + for (uint32_t i = 0; i < count; i++) { + const ant_shape_prop_t *prop = ant_shape_prop_at(ptr->shape, i); + if (!prop || prop->type != ANT_SHAPE_KEY_STRING) continue; + if (i >= ptr->prop_count) continue; + const char *key = prop->key.interned; + bool enumerable = own_key_is_enumerable(js, obj, ptr, i); + r = for_in_keys_add_cstr(js, out, seen, key, strlen(key), enumerable); + if (is_err(r)) return r; + } + return js_mkundef(); + } + } + + if (is_arr) { + ant_offset_t doff = get_dense_buf(obj); + ant_offset_t dense_len = doff ? dense_iterable_length(js, obj) : 0; + for (ant_offset_t i = 0; i < dense_len; i++) { + if (is_empty_slot(dense_get(doff, i))) continue; + char idxstr[16]; + size_t idxlen = uint_to_str(idxstr, sizeof(idxstr), (unsigned)i); + r = for_in_keys_add_cstr(js, out, seen, idxstr, idxlen, true); + if (is_err(r)) return r; + } + } + + own_index_key_t stack_indices[32]; + own_index_key_t *indices = stack_indices; + uint32_t index_count = 0; + uint32_t index_cap = (uint32_t)(sizeof(stack_indices) / sizeof(stack_indices[0])); + + for (uint32_t i = 0; i < count; i++) { + const ant_shape_prop_t *prop = ant_shape_prop_at(ptr->shape, i); + if (!prop || prop->type != ANT_SHAPE_KEY_STRING) continue; + if (i >= ptr->prop_count) continue; + + const char *key = prop->key.interned; + size_t key_len = strlen(key); + if (is_arr && own_key_is_dense_shadow(js, obj, key, key_len)) continue; + + unsigned long parsed = 0; + if (!own_key_is_array_index(key, key_len, &parsed)) continue; + + if (index_count == index_cap) { + uint32_t next_cap = index_cap * 2; + own_index_key_t *next = NULL; + if (indices == stack_indices) { + next = calloc(next_cap, sizeof(*next)); + if (next) memcpy(next, indices, index_count * sizeof(*next)); + } else { + next = realloc(indices, next_cap * sizeof(*next)); + } + if (!next) { + if (indices != stack_indices) free(indices); + return js_mkerr(js, "oom"); + } + indices = next; + index_cap = next_cap; + } + + uint32_t pos = index_count++; + while (pos > 0 && indices[pos - 1].index > parsed) { + indices[pos] = indices[pos - 1]; + pos--; + } + indices[pos] = (own_index_key_t){ .slot = i, .index = parsed }; + } + + for (uint32_t i = 0; i < index_count; i++) { + const ant_shape_prop_t *prop = ant_shape_prop_at(ptr->shape, indices[i].slot); + if (!prop) continue; + const char *key = prop->key.interned; + bool enumerable = own_key_is_enumerable(js, obj, ptr, indices[i].slot); + r = for_in_keys_add_cstr(js, out, seen, key, strlen(key), enumerable); + if (is_err(r)) { + if (indices != stack_indices) free(indices); + return r; + } + } + + if (indices != stack_indices) free(indices); + + if (is_arr) { + r = for_in_keys_add_cstr(js, out, seen, "length", 6, false); + if (is_err(r)) return r; + } + + for (uint32_t i = 0; i < count; i++) { + const ant_shape_prop_t *prop = ant_shape_prop_at(ptr->shape, i); + if (!prop || prop->type != ANT_SHAPE_KEY_STRING) continue; + if (i >= ptr->prop_count) continue; + + const char *key = prop->key.interned; + size_t key_len = strlen(key); + if (own_key_is_array_index(key, key_len, NULL)) continue; + + bool enumerable = own_key_is_enumerable(js, obj, ptr, i); + r = for_in_keys_add_cstr(js, out, seen, key, key_len, enumerable); + if (is_err(r)) return r; + } + + return js_mkundef(); +} + static inline ant_value_t for_in_keys_collect_chain( ant_t *js, ant_value_t out, ant_value_t seen, ant_value_t obj ) { @@ -6718,9 +7121,7 @@ static inline ant_value_t for_in_keys_collect_chain( ant_value_t as_cur = (vtype(cur) == T_FUNC) ? js_func_obj(cur) : cur; ant_object_t *cur_ptr = js_obj_ptr(as_cur); ant_value_t key, r, proto; - ant_offset_t doff, dense_len, klen; - bool is_arr; - + if (!cur_ptr) goto next_proto; if (!cur_ptr->is_exotic || !cur_ptr->exotic_keys) goto shape_props; @@ -6738,58 +7139,19 @@ static inline ant_value_t for_in_keys_collect_chain( } goto next_proto; -shape_props: + shape_props: if (!cur_ptr->shape) goto next_proto; - is_arr = (vtype(as_cur) == T_ARR); - if (!is_arr) goto shape_iter; - - doff = get_dense_buf(as_cur); - dense_len = doff ? dense_iterable_length(js, as_cur) : 0; - for (ant_offset_t i = 0; i < dense_len; i++) { - if (is_empty_slot(dense_get(doff, i))) continue; - char idxstr[16]; - size_t idxlen = uint_to_str(idxstr, sizeof(idxstr), (unsigned)i); - key = js_mkstr(js, idxstr, idxlen); - GC_ROOT_PIN(js, key); - r = for_in_keys_add(js, out, seen, key); - if (is_err(r)) goto err; - } - -shape_iter: - for (uint32_t i = 0; i < ant_shape_count(cur_ptr->shape); i++) { - const ant_shape_prop_t *prop = ant_shape_prop_at(cur_ptr->shape, i); - if (!prop || prop->type == ANT_SHAPE_KEY_SYMBOL) continue; - if (i >= cur_ptr->prop_count) continue; - - const char *kstr = prop->key.interned; - klen = (ant_offset_t)strlen(kstr); - if (is_arr && is_array_index(kstr, klen)) { - doff = get_dense_buf(as_cur); - if (doff) { - unsigned long pidx = 0; - for (ant_offset_t ci = 0; ci < klen; ci++) pidx = pidx * 10 + (kstr[ci] - '0'); - if (pidx < (unsigned long)dense_iterable_length(js, as_cur)) continue; - }} - - bool enumerable = (ant_shape_get_attrs(cur_ptr->shape, i) & ANT_PROP_ATTR_ENUMERABLE) != 0; - if (cur_ptr->is_exotic) { - descriptor_entry_t *desc = lookup_descriptor(js_as_obj(as_cur), kstr, (size_t)klen); - if (desc) enumerable = desc->enumerable; - } - - key = js_mkstr(js, kstr, (size_t)klen); - GC_ROOT_PIN(js, key); - r = for_in_keys_add(js, enumerable ? out : js_mkundef(), seen, key); - if (is_err(r)) goto err; - } + r = for_in_keys_collect_ordinary_own(js, out, seen, as_cur, cur_ptr); + if (is_err(r)) goto err; + goto next_proto; -next_proto: + next_proto: GC_ROOT_RESTORE(js, iter_mark); proto = js_get_proto(js, cur); if (!is_object_type(proto)) break; cur = proto; continue; -err: + err: GC_ROOT_RESTORE(js, iter_mark); return r; } @@ -7694,6 +8056,154 @@ bool js_is_own_enumerable_prop( return (ant_shape_get_attrs(source_ptr->shape, key->slot) & ANT_PROP_ATTR_ENUMERABLE) != 0; } +static ant_value_t object_assign_copy_slot( + ant_t *js, ant_value_t target, ant_object_t *source_ptr, uint32_t slot +) { + const ant_shape_prop_t *prop = ant_shape_prop_at(source_ptr->shape, slot); + if (!prop) return js_mkundef(); + if ((ant_shape_get_attrs(source_ptr->shape, slot) & ANT_PROP_ATTR_ENUMERABLE) == 0) + return js_mkundef(); + + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, target); + ant_value_t val = ant_object_prop_get_unchecked(source_ptr, slot); + GC_ROOT_PIN(js, val); + ant_value_t prop_key = js_mkundef(); + + if (prop->type == ANT_SHAPE_KEY_SYMBOL) { + prop_key = mkval(T_SYMBOL, prop->key.sym_off); + } else { + const char *key = prop->key.interned; + size_t key_len = strlen(key); + ant_value_t setter = js_mkundef(); + bool has_setter = false; + uintptr_t existing = lkp_with_setter(js, target, key, key_len, &setter, &has_setter); + ant_object_t *target_ptr = js_obj_ptr(js_as_obj(target)); + if ( + existing == 0 && !has_setter && target_ptr && + target_ptr->extensible && !target_ptr->frozen && !target_ptr->sealed && !target_ptr->is_exotic + ) { + const char *interned = intern_string(key, key_len); + ant_value_t result = interned + ? mkprop_interned_exact(js, target, interned, val, 0) + : js_mkerr(js, "oom"); + GC_ROOT_RESTORE(js, root_mark); + return result; + } + prop_key = js_mkstr(js, key, key_len); + } + GC_ROOT_PIN(js, prop_key); + + ant_value_t result = js_setprop(js, target, prop_key, val); + GC_ROOT_RESTORE(js, root_mark); + return result; +} + +static ant_value_t object_assign_fast_ordinary_source( + ant_t *js, ant_value_t target, ant_value_t source, bool *handled +) { + *handled = false; + if (vtype(source) != T_OBJ) return js_mkundef(); + + ant_value_t source_obj = js_as_obj(source); + ant_object_t *source_ptr = js_obj_ptr(source_obj); + if (!source_ptr || !source_ptr->shape) { + *handled = true; + return js_mkundef(); + } + if (source_ptr->is_exotic) return js_mkundef(); + + uint32_t count = ant_shape_count(source_ptr->shape); + own_index_key_t stack_indices[32]; + own_index_key_t *indices = stack_indices; + uint32_t index_count = 0; + uint32_t index_cap = (uint32_t)(sizeof(stack_indices) / sizeof(stack_indices[0])); + + for (uint32_t i = 0; i < count; i++) { + const ant_shape_prop_t *prop = ant_shape_prop_at(source_ptr->shape, i); + if (!prop) continue; + if (i >= source_ptr->prop_count) continue; + if (prop->has_getter || prop->has_setter) { + if (indices != stack_indices) free(indices); + return js_mkundef(); + } + if (prop->type == ANT_SHAPE_KEY_STRING) { + unsigned long parsed = 0; + const char *key = prop->key.interned; + if (!own_key_is_array_index(key, strlen(key), &parsed)) continue; + + if (index_count == index_cap) { + uint32_t next_cap = index_cap * 2; + own_index_key_t *next = NULL; + if (indices == stack_indices) { + next = calloc(next_cap, sizeof(*next)); + if (next) memcpy(next, indices, index_count * sizeof(*next)); + } else { + next = realloc(indices, next_cap * sizeof(*next)); + } + if (!next) { + if (indices != stack_indices) free(indices); + *handled = true; + return js_mkerr(js, "oom"); + } + indices = next; + index_cap = next_cap; + } + + uint32_t pos = index_count++; + while (pos > 0 && indices[pos - 1].index > parsed) { + indices[pos] = indices[pos - 1]; + pos--; + } + indices[pos] = (own_index_key_t){ .slot = i, .index = parsed }; + } + } + + *handled = true; + GC_ROOT_SAVE(root_mark, js); + GC_ROOT_PIN(js, target); + GC_ROOT_PIN(js, source_obj); + + if (index_count > 0) for (uint32_t i = 0; i < index_count; i++) { + ant_value_t result = object_assign_copy_slot(js, target, source_ptr, indices[i].slot); + if (is_err(result)) { + if (indices != stack_indices) free(indices); + GC_ROOT_RESTORE(js, root_mark); + return result; + } + } + + for (uint32_t i = 0; i < count; i++) { + const ant_shape_prop_t *prop = ant_shape_prop_at(source_ptr->shape, i); + if (!prop || prop->type != ANT_SHAPE_KEY_STRING) continue; + if (i >= source_ptr->prop_count) continue; + const char *key = prop->key.interned; + if (own_key_is_array_index(key, strlen(key), NULL)) continue; + ant_value_t result = object_assign_copy_slot(js, target, source_ptr, i); + if (is_err(result)) { + if (indices != stack_indices) free(indices); + GC_ROOT_RESTORE(js, root_mark); + return result; + } + } + + for (uint32_t i = 0; i < count; i++) { + const ant_shape_prop_t *prop = ant_shape_prop_at(source_ptr->shape, i); + if (!prop || prop->type != ANT_SHAPE_KEY_SYMBOL) continue; + if (i >= source_ptr->prop_count) continue; + ant_value_t result = object_assign_copy_slot(js, target, source_ptr, i); + if (is_err(result)) { + if (indices != stack_indices) free(indices); + GC_ROOT_RESTORE(js, root_mark); + return result; + } + } + + if (indices != stack_indices) free(indices); + GC_ROOT_RESTORE(js, root_mark); + return js_mkundef(); +} + static ant_value_t builtin_object_assign(ant_t *js, ant_value_t *args, int nargs) { if (nargs == 0) return js_mkerr(js, "Object.assign requires at least 1 argument"); @@ -7716,20 +8226,57 @@ static ant_value_t builtin_object_assign(ant_t *js, ant_value_t *args, int nargs if (st == T_NULL || st == T_UNDEF) continue; if (st != T_OBJ && st != T_ARR && st != T_FUNC) continue; + + bool fast_handled = false; + ant_value_t fast_result = object_assign_fast_ordinary_source(js, as_obj, source, &fast_handled); + if (is_err(fast_result)) return fast_result; + if (fast_handled) continue; - ant_iter_t iter = js_prop_iter_begin(js, source); - ant_object_t *source_ptr = js_obj_ptr(js_as_obj(source)); - - ant_iter_key_t key = {0}; - ant_value_t val = js_mkundef(); - - while (js_prop_iter_next_key(&iter, &key, &val)) { - if (!js_is_own_enumerable_prop(js, source, source_ptr, &key)) continue; - ant_value_t prop_key = key.is_symbol ? mkval(T_SYMBOL, key.sym_off) : js_mkstr(js, key.str, key.key_len); + GC_ROOT_SAVE(source_mark, js); + GC_ROOT_PIN(js, target); + GC_ROOT_PIN(js, as_obj); + GC_ROOT_PIN(js, source); + + ant_value_t keys = js_own_property_keys(js, source, true, true); + GC_ROOT_PIN(js, keys); + if (is_err(keys)) { + GC_ROOT_RESTORE(js, source_mark); + return keys; + } + + ant_offset_t key_count = js_arr_len(js, keys); + for (ant_offset_t k = 0; k < key_count; k++) { + GC_ROOT_SAVE(key_mark, js); + ant_value_t prop_key = js_arr_get(js, keys, k); + GC_ROOT_PIN(js, prop_key); + + ant_value_t val = js_mkundef(); + if (vtype(prop_key) == T_SYMBOL) { + val = js_get_sym(js, source, prop_key); + } else { + ant_offset_t key_len = 0; + ant_offset_t key_off = vstr(js, prop_key, &key_len); + const char *key_str = (const char *)(uintptr_t)key_off; + unsigned long idx = 0; + if ( + array_obj_ptr(source) && + own_key_is_array_index(key_str, (size_t)key_len, &idx) && + idx < (unsigned long)get_array_length(js, source) && + arr_has(js, source, (ant_offset_t)idx) + ) { + val = arr_get(js, source, (ant_offset_t)idx); + } else { + ant_offset_t prop_off = lkp(js, source, key_str, (size_t)key_len); + val = prop_off ? propref_load(js, prop_off) : js_mkundef(); + } + } + GC_ROOT_PIN(js, val); + js_setprop(js, as_obj, prop_key, val); + GC_ROOT_RESTORE(js, key_mark); } - - js_prop_iter_end(&iter); + + GC_ROOT_RESTORE(js, source_mark); } return target; @@ -8103,6 +8650,7 @@ static ant_value_t builtin_object_getOwnPropertyNames(ant_t *js, ant_value_t *ar ant_object_t *ptr = js_obj_ptr(obj); if (!ptr || !ptr->shape) return mkarr(js); + if (vtype(obj) != T_ARR) return js_own_property_keys(js, obj, false, false); bool is_arr_obj = (vtype(obj) == T_ARR); ant_value_t arr = mkarr(js); diff --git a/src/errors.c b/src/errors.c index 7437ebf..878b68c 100644 --- a/src/errors.c +++ b/src/errors.c @@ -497,7 +497,6 @@ static bool append_error_context( int error_line_no, int error_col, int error_span_cols ) { if (!src || src_len <= 0 || !n) return false; - if (src_pos < 0) src_pos = 0; if (src_pos > src_len) src_pos = src_len; ant_offset_t err_line_start = src_pos; @@ -508,12 +507,17 @@ static bool append_error_context( ant_offset_t ctx_start = err_line_start; int first_line_no = error_line_no; + for (int i = 0; i < 5 && ctx_start > 0; i++) { ant_offset_t prev = ctx_start - 1; - if (src[prev] == '\n') prev--; - while (prev >= 0 && src[prev] != '\n') prev--; + if (src[prev] == '\n') { + if (prev == 0) { ctx_start = 0; break; } + prev--; + } + + while (prev > 0 && src[prev] != '\n') prev--; + ctx_start = (src[prev] == '\n') ? prev + 1 : 0; - ctx_start = prev + 1; first_line_no--; if (first_line_no < 1) { first_line_no = 1; break; } } diff --git a/src/modules/reflect.c b/src/modules/reflect.c index c30abb8..b8d3aa7 100644 --- a/src/modules/reflect.c +++ b/src/modules/reflect.c @@ -97,23 +97,14 @@ static ant_value_t reflect_delete_property(ant_t *js, ant_value_t *args, int nar static ant_value_t reflect_own_keys(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1) return js_mkarr(js); - ant_value_t target = args[0]; int t = vtype(target); - if (t != T_OBJ && t != T_FUNC) { + if (t != T_OBJ && t != T_ARR && t != T_FUNC) { return js_mkerr(js, "Reflect.ownKeys called on non-object"); } - ant_value_t keys_arr = js_mkarr(js); - ant_iter_t iter = js_prop_iter_begin(js, target); - - ant_value_t key, value; - while (js_prop_iter_next_val(&iter, &key, &value)) - js_arr_push(js, keys_arr, key); - - js_prop_iter_end(&iter); - return keys_arr; + return js_own_property_keys(js, target, true, false); } static ant_value_t reflect_construct(ant_t *js, ant_value_t *args, int nargs) { diff --git a/tests/bench_forin_object_keys.js b/tests/bench_forin_object_keys.js new file mode 100644 index 0000000..c5fcfb0 --- /dev/null +++ b/tests/bench_forin_object_keys.js @@ -0,0 +1,201 @@ +function nowMs() { + if (typeof performance !== 'undefined' && performance && typeof performance.now === 'function') { + return performance.now(); + } + return Date.now(); +} + +function parseScale() { + if (typeof process === 'undefined' || !process || !process.argv) return 1; + var raw = Number(process.argv[2]); + return Number.isFinite(raw) && raw > 0 ? raw : 1; +} + +function percentile(sorted, p) { + if (sorted.length === 0) return 0; + if (sorted.length === 1) return sorted[0]; + var pos = (sorted.length - 1) * p; + var base = Math.floor(pos); + var frac = pos - base; + var next = sorted[base + 1]; + if (next === undefined) return sorted[base]; + return sorted[base] + (next - sorted[base]) * frac; +} + +function stableNumericSort(values) { + var out = values.slice(); + for (var i = 1; i < out.length; i++) { + var value = out[i]; + var j = i - 1; + while (j >= 0 && out[j] > value) { + out[j + 1] = out[j]; + j--; + } + out[j + 1] = value; + } + return out; +} + +var SCALE = parseScale(); +var WARMUP_RUNS = 2; +var SAMPLE_RUNS = 7; +var sink = 0; + +function scaledIters(base) { + return Math.max(1, Math.floor(base * SCALE)); +} + +function bench(label, fn, iters) { + var warmupIters = Math.max(1, Math.min(iters, Math.max(1000, (iters / 4) | 0))); + for (var i = 0; i < WARMUP_RUNS; i++) sink ^= fn(warmupIters) | 0; + + var result = 0; + var samples = []; + for (var j = 0; j < SAMPLE_RUNS; j++) { + var t0 = nowMs(); + var r = fn(iters); + var dt = nowMs() - t0; + if (j === 0) result = r; + samples.push(dt); + } + + var sorted = stableNumericSort(samples); + var min = sorted[0]; + var med = percentile(sorted, 0.5); + var p95 = percentile(sorted, 0.95); + var max = sorted[sorted.length - 1]; + var opsPerMs = med > 0 ? (iters / med).toFixed(2) : 'inf'; + + sink ^= result | 0; + console.log( + label + + ': median ' + med.toFixed(2) + 'ms (' + opsPerMs + ' ops/ms)' + + ', p95 ' + p95.toFixed(2) + 'ms' + + ', min ' + min.toFixed(2) + 'ms' + + ', max ' + max.toFixed(2) + 'ms' + + ' result=' + result + ); +} + +function makePlainObject() { + return { + alpha: 1, + beta: 2, + gamma: 3, + delta: 4, + epsilon: 5, + zeta: 6, + eta: 7, + theta: 8, + }; +} + +function makeMixedIndexObject() { + var obj = { + 2: true, + 0: true, + 1: true, + ' ': true, + 9: true, + D: true, + B: true, + '-1': true, + }; + obj.A = true; + obj[3] = true; + 'EFGHIJKLMNOPQRSTUVWXYZ'.split('').forEach(function (key) { + obj[key] = true; + }); + Object.defineProperty(obj, 'C', { value: true, enumerable: true }); + Object.defineProperty(obj, '4', { value: true, enumerable: true }); + delete obj[2]; + obj[2] = true; + return obj; +} + +function makeShadowObject() { + var proto = {}; + var names = 'ABCDEFGHIJKLMNOPQRST'.split(''); + for (var i = 0; i < names.length; i++) proto[names[i]] = i; + + var obj = Object.create(proto); + obj.keepA = 1; + obj.keepB = 2; + obj.keepC = 3; + for (var j = 0; j < names.length; j++) { + Object.defineProperty(obj, names[j], { + value: j, + enumerable: false, + configurable: true, + }); + } + return obj; +} + +var plainObj = makePlainObject(); +var mixedObj = makeMixedIndexObject(); +var shadowObj = makeShadowObject(); + +function consumeKeys(keys) { + var total = keys.length; + for (var i = 0; i < keys.length; i++) total += keys[i].length; + return total; +} + +function benchObjectKeysPlain(n) { + var total = 0; + for (var i = 0; i < n; i++) total += consumeKeys(Object.keys(plainObj)); + return total; +} + +function benchObjectKeysMixed(n) { + var total = 0; + for (var i = 0; i < n; i++) total += consumeKeys(Object.keys(mixedObj)); + return total; +} + +function benchForInPlain(n) { + var total = 0; + for (var i = 0; i < n; i++) { + for (var key in plainObj) total += key.length; + } + return total; +} + +function benchForInMixed(n) { + var total = 0; + for (var i = 0; i < n; i++) { + for (var key in mixedObj) total += key.length; + } + return total; +} + +function benchForInShadow(n) { + var total = 0; + for (var i = 0; i < n; i++) { + for (var key in shadowObj) total += key.length; + } + return total; +} + +function benchForInFunctionShadow(n) { + var total = 0; + Object.prototype.length = 42; + for (var i = 0; i < n; i++) { + for (var key in Function) total += key.length; + } + delete Object.prototype.length; + return total; +} + +console.log('for-in and Object.keys benchmark'); +console.log('scale=' + SCALE); + +bench('Object.keys plain', benchObjectKeysPlain, scaledIters(200000)); +bench('Object.keys mixed-index', benchObjectKeysMixed, scaledIters(100000)); +bench('for-in plain', benchForInPlain, scaledIters(100000)); +bench('for-in mixed-index', benchForInMixed, scaledIters(50000)); +bench('for-in nonenum-shadow', benchForInShadow, scaledIters(50000)); +bench('for-in Function shadow', benchForInFunctionShadow, scaledIters(50000)); + +console.log('sink=' + sink); diff --git a/tests/bench_own_property_order.js b/tests/bench_own_property_order.js new file mode 100644 index 0000000..753d419 --- /dev/null +++ b/tests/bench_own_property_order.js @@ -0,0 +1,228 @@ +function nowMs() { + if (typeof performance !== 'undefined' && performance && typeof performance.now === 'function') { + return performance.now(); + } + return Date.now(); +} + +function parseScale() { + if (typeof process === 'undefined' || !process || !process.argv) return 1; + const raw = Number(process.argv[2]); + return Number.isFinite(raw) && raw > 0 ? raw : 1; +} + +function percentile(sorted, p) { + if (sorted.length === 0) return 0; + if (sorted.length === 1) return sorted[0]; + const pos = (sorted.length - 1) * p; + const base = Math.floor(pos); + const frac = pos - base; + const next = sorted[base + 1]; + if (next === undefined) return sorted[base]; + return sorted[base] + (next - sorted[base]) * frac; +} + +function stableNumericSort(values) { + const out = values.slice(); + for (let i = 1; i < out.length; i++) { + const value = out[i]; + let j = i - 1; + while (j >= 0 && out[j] > value) { + out[j + 1] = out[j]; + j--; + } + out[j + 1] = value; + } + return out; +} + +const SCALE = parseScale(); +const WARMUP_RUNS = 2; +const SAMPLE_RUNS = 7; +let sink = 0; + +function scaledIters(base) { + return Math.max(1, Math.floor(base * SCALE)); +} + +function bench(label, fn, iters) { + const warmupIters = Math.max(1, Math.min(iters, Math.max(1000, (iters / 4) | 0))); + for (let i = 0; i < WARMUP_RUNS; i++) sink ^= fn(warmupIters) | 0; + + let result = 0; + const samples = []; + for (let i = 0; i < SAMPLE_RUNS; i++) { + const t0 = nowMs(); + const r = fn(iters); + const dt = nowMs() - t0; + if (i === 0) result = r; + samples.push(dt); + } + + const sorted = stableNumericSort(samples); + const min = sorted[0]; + const med = percentile(sorted, 0.5); + const p95 = percentile(sorted, 0.95); + const max = sorted[sorted.length - 1]; + const opsPerMs = med > 0 ? (iters / med).toFixed(2) : 'inf'; + + sink ^= result | 0; + console.log( + label + + ': median ' + med.toFixed(2) + 'ms (' + opsPerMs + ' ops/ms)' + + ', p95 ' + p95.toFixed(2) + 'ms' + + ', min ' + min.toFixed(2) + 'ms' + + ', max ' + max.toFixed(2) + 'ms' + + ' result=' + result + ); +} + +function makePlainStringObject() { + return { + alpha: 1, + beta: 2, + gamma: 3, + delta: 4, + epsilon: 5, + zeta: 6, + eta: 7, + theta: 8, + }; +} + +function makeMixedIndexObject() { + const obj = { + 2: true, + 0: true, + 1: true, + ' ': true, + 9: true, + D: true, + B: true, + '-1': true, + }; + obj.A = true; + obj[3] = true; + 'EFGHIJKLMNOPQRSTUVWXYZ'.split('').forEach(key => obj[key] = true); + Object.defineProperty(obj, 'C', { value: true, enumerable: true }); + Object.defineProperty(obj, '4', { value: true, enumerable: true }); + delete obj[2]; + obj[2] = true; + return obj; +} + +const sym1 = Symbol('one'); +const sym2 = Symbol('two'); +const sym3 = Symbol('three'); + +function makeSymbolObject() { + const obj = { 1: true, A: true }; + obj.B = true; + obj[sym1] = true; + obj[2] = true; + obj[sym2] = true; + Object.defineProperty(obj, 'C', { value: true, enumerable: true }); + Object.defineProperty(obj, sym3, { value: true, enumerable: true }); + Object.defineProperty(obj, 'D', { value: true, enumerable: true }); + return obj; +} + +const plainObj = makePlainStringObject(); +const mixedObj = makeMixedIndexObject(); +const symbolObj = makeSymbolObject(); + +function consumeKeys(keys) { + let total = keys.length; + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + total += typeof key === 'symbol' ? 17 : key.length; + } + return total; +} + +function benchGetOwnPropertyNamesPlain(n) { + let total = 0; + for (let i = 0; i < n; i++) total += consumeKeys(Object.getOwnPropertyNames(plainObj)); + return total; +} + +function benchGetOwnPropertyNamesMixed(n) { + let total = 0; + for (let i = 0; i < n; i++) total += consumeKeys(Object.getOwnPropertyNames(mixedObj)); + return total; +} + +function benchReflectOwnKeysPlain(n) { + let total = 0; + for (let i = 0; i < n; i++) total += consumeKeys(Reflect.ownKeys(plainObj)); + return total; +} + +function benchReflectOwnKeysMixed(n) { + let total = 0; + for (let i = 0; i < n; i++) total += consumeKeys(Reflect.ownKeys(symbolObj)); + return total; +} + +function benchObjectAssignPlain(n) { + let total = 0; + for (let i = 0; i < n; i++) { + const out = Object.assign({}, plainObj); + total += out.alpha + out.theta; + } + return total; +} + +function benchObjectAssignMixed(n) { + let total = 0; + for (let i = 0; i < n; i++) { + const out = Object.assign({}, mixedObj); + total += out[0] === true ? 1 : 0; + total += out.C === true ? 1 : 0; + } + return total; +} + +function benchForInPlain(n) { + let total = 0; + for (let i = 0; i < n; i++) { + for (const key in plainObj) total += key.length; + } + return total; +} + +function benchForInMixed(n) { + let total = 0; + for (let i = 0; i < n; i++) { + for (const key in mixedObj) total += key.length; + } + return total; +} + +function benchJsonStringifyPlain(n) { + let total = 0; + for (let i = 0; i < n; i++) total += JSON.stringify(plainObj).length; + return total; +} + +function benchJsonStringifyMixed(n) { + let total = 0; + for (let i = 0; i < n; i++) total += JSON.stringify(mixedObj).length; + return total; +} + +console.log('own-property order benchmark'); +console.log('scale=' + SCALE); + +bench('Object.getOwnPropertyNames plain', benchGetOwnPropertyNamesPlain, scaledIters(200000)); +bench('Object.getOwnPropertyNames mixed-index', benchGetOwnPropertyNamesMixed, scaledIters(100000)); +bench('Reflect.ownKeys plain', benchReflectOwnKeysPlain, scaledIters(200000)); +bench('Reflect.ownKeys mixed-symbol', benchReflectOwnKeysMixed, scaledIters(100000)); +bench('Object.assign plain', benchObjectAssignPlain, scaledIters(100000)); +bench('Object.assign mixed-index', benchObjectAssignMixed, scaledIters(50000)); +bench('for-in plain', benchForInPlain, scaledIters(100000)); +bench('for-in mixed-index', benchForInMixed, scaledIters(50000)); +bench('JSON.stringify plain', benchJsonStringifyPlain, scaledIters(50000)); +bench('JSON.stringify mixed-index', benchJsonStringifyMixed, scaledIters(20000)); + +console.log('sink=' + sink); -- 2.51.2