diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index f4bc8db680..2509dde13e 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -1526,6 +1526,14 @@ void scr_class_throw_read_only(ScrStr *field, ScrStr *owner); void scr_class_throw_not_extensible(ScrStr *field); ScrDyn *scr_dyn_get_own_property_descriptor(ScrDyn *target, ScrDyn *key); ScrDyn *scr_dyn_array_proto_call(ScrDyn *target, ScrStr *method, ScrDyn *args); +/* Array.prototype's generic methods over an array-like receiver + * (scr_dyn_array_like.c, linked only into programs that call them from a + * JavaScript source): +1, or NULL with the exception pending. */ +ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args); +/* Number.prototype or Boolean.prototype as a value, one object per thread + * (scr_dyn_builtin_prototype.c, linked only into programs that read them + * from a JavaScript source); `kind` is "Number" or "Boolean". +1. */ +ScrDyn *scr_dyn_builtin_prototype(const ScrStr *kind); ScrDyn *scr_dyn_array_prototype(void); /* +1, installs native method adapters */ ScrDyn *scr_dyn_array_prototype_base(void); /* +1, shared native array identity */ /* Names the prototype object of a compiled class `name` for util.inspect diff --git a/packages/runtime/src/scr_dyn_array_like.c b/packages/runtime/src/scr_dyn_array_like.c index 5d341816d9..a238239ce3 100644 --- a/packages/runtime/src/scr_dyn_array_like.c +++ b/packages/runtime/src/scr_dyn_array_like.c @@ -485,17 +485,18 @@ ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) { ? scr_dyn_string_coerce_js(argv[0]) : scr_str_new(",", 1); if (!sep) return NULL; + /* Get alone at each index: a missing index reads undefined, which + * joins as the empty string. */ ScrJsonBuf buffer; scr_jb_init(&buffer); for (size_t i = 0; i < len; i++) { if (i) scr_jb_write(&buffer, sep->data, sep->len); - if (len > 1000000 && !al_iteration(i)) { scr_str_release(sep); return NULL; } - if (!al_present(recv, i)) { - if (scr_exc_pending()) { scr_str_release(sep); return NULL; } - continue; + ScrDyn *item = len > 1000000 && !al_iteration(i) ? NULL : al_item(recv, i); + if (!item) { + scr_str_release(scr_jb_finish(&buffer)); + scr_str_release(sep); + return NULL; } - ScrDyn *item = al_item(recv, i); - if (!item) { scr_str_release(sep); return NULL; } if (item->kind != SCR_DYN_UNDEF && item->kind != SCR_DYN_NULL) { ScrStr *text = scr_dyn_string_coerce_js(item); if (text) { @@ -504,7 +505,11 @@ ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) { } } scr_dyn_release(item); - if (scr_exc_pending()) { scr_str_release(sep); return NULL; } + if (scr_exc_pending()) { + scr_str_release(scr_jb_finish(&buffer)); + scr_str_release(sep); + return NULL; + } } scr_str_release(sep); ScrStr *text = scr_jb_finish(&buffer); @@ -710,22 +715,30 @@ ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) { return out; } if (al_is(method, "flat") || al_is(method, "flatMap")) { - /* Snapshot the receiver into a native array the array dispatch - * flattens: the flattening rules (depth counting, nested arrays and - * wrapped array views) live in one implementation. flatMap maps first, - * then flattens the mapped array at depth 1. */ + /* FlattenIntoArray: after the length, flat converts its depth and + * flatMap checks its callback. Then each present index is read and, + * for flatMap, mapped, and its value is flattened into the result + * before the next index is read. The array dispatch flattens each + * value as `[value].flat(depth)`, so the flattening rules (nested + * arrays and wrapped array views) keep one implementation. */ bool mapping = al_is(method, "flatMap"); - if (mapping && !al_cb_check(argv, argc)) return NULL; - ScrDyn *mapped = scr_dyn_new_arr(); + double depth = 1; + if (mapping) { + if (!al_cb_check(argv, argc)) return NULL; + } else if (argc > 0 && argv[0]->kind != SCR_DYN_UNDEF) { + if (!al_number(argv[0], &depth)) return NULL; + if (depth < 0) depth = 0; + } + ScrDyn *depth_arg = scr_dyn_new_num(depth); + ScrDyn *out = scr_dyn_new_arr(); for (size_t i = 0; i < len; i++) { - if (len > 1000000 && !al_iteration(i)) { scr_dyn_release(mapped); return NULL; } + if (len > 1000000 && !al_iteration(i)) goto flat_failed; if (!al_present(recv, i)) { - if (scr_exc_pending()) { scr_dyn_release(mapped); return NULL; } - scr_dyn_arr_push_hole(mapped); + if (scr_exc_pending()) goto flat_failed; continue; } ScrDyn *item = al_item(recv, i); - if (!item) { scr_dyn_release(mapped); return NULL; } + if (!item) goto flat_failed; if (mapping) { ScrDyn *index = scr_dyn_new_num((double)i); ScrDyn *cbargs[3] = { item, index, recv }; @@ -734,25 +747,24 @@ ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) { scr_dyn_this_pop(); scr_dyn_release(index); scr_dyn_release(item); - if (!result) { scr_dyn_release(mapped); return NULL; } + if (!result) goto flat_failed; item = result; } - scr_dyn_arr_push(mapped, item); + ScrDyn *one = scr_dyn_new_arr(); + scr_dyn_arr_push(one, item); + ScrDyn *flattened = scr_dyn_invoke(one, "flat", &depth_arg, 1, mapping ? "flatMap" : "flat"); + scr_dyn_release(one); + if (!flattened) goto flat_failed; + for (size_t j = 0; j < flattened->v.arr.len; j++) + scr_dyn_arr_push(out, scr_dyn_retain(flattened->v.arr.items[j])); + scr_dyn_release(flattened); } - ScrDyn *one = scr_dyn_new_num(1); - ScrDyn *flatargs[1] = { one }; - ScrDyn *out; - if (mapping) { - out = scr_dyn_invoke(mapped, "flat", flatargs, 1, "flatMap"); - } else { - /* flat's own depth argument: absent, undefined or a number. */ - ScrDyn *given[1] = { argc > 0 ? argv[0] : scr_dyn_undefined() }; - size_t count = argc > 0 ? 1 : 0; - out = scr_dyn_invoke(mapped, "flat", given, count, "flat"); - } - scr_dyn_release(one); - scr_dyn_release(mapped); + scr_dyn_release(depth_arg); return out; + flat_failed: + scr_dyn_release(depth_arg); + scr_dyn_release(out); + return NULL; } /* Every other name keeps the one implementation in the invoke unit * (at, slice, indexOf, the higher-order methods, and the "not supported diff --git a/tests/corpus/js-array-proto-array-likes.js b/tests/corpus/js-array-proto-array-likes.js index b31be0df86..79a008b494 100644 --- a/tests/corpus/js-array-proto-array-likes.js +++ b/tests/corpus/js-array-proto-array-likes.js @@ -79,7 +79,7 @@ const traced = (target) => { }, has(object, key) { log.push('has:' + String(key)); - return Reflect.has(object, key); + return key in object; }, }); return { proxy, log }; @@ -149,4 +149,39 @@ report('array-concat', Array.prototype.concat.call(list, ['s'], 't').join('')); report('array-toSpliced', Array.prototype.toSpliced.call(list, 0, 1).join('')); report('array-with', Array.prototype.with.call(list, 0, 'z').join('')); +// join reads every index with Get alone; a has trap never runs. +const joinedLog = traced({ length: 3, 0: 'a', 2: 'c' }); +report('join', Array.prototype.join.call(joinedLog.proxy, '-')); +report('join-reads', joinedLog.log.join(',')); +const hidden = new Proxy({ length: 2, 0: 'x', 1: 'y' }, { has() { throw new Error('has trap'); } }); +report('join-has-trap', Array.prototype.join.call(hidden, '')); + +// flat converts its depth before it reads any index. +const order = []; +const depthSource = { length: 2, get 0() { order.push('read 0'); return [1, [2]]; }, 1: 3 }; +const depth = { valueOf() { order.push('depth'); return 2; } }; +report('flat-depth', JSON.stringify(Array.prototype.flat.call(depthSource, depth))); +report('flat-order', order.join(',')); +const failing = { length: 1, get 0() { order.push('never'); return 1; } }; +try { + Array.prototype.flat.call(failing, { valueOf() { throw new Error('depth failed'); } }); +} catch (error) { + report('flat-depth-throws', error.message + ' ' + order.includes('never')); +} + +// flatMap flattens each callback's result before the next callback runs. +const shared = [1, 2]; +const mapped = Array.prototype.flatMap.call({ length: 2, 0: 'a', 1: 'b' }, (value) => { + if (value === 'a') return shared; + shared[0] = 9; + return []; +}); +report('flatMap-order', JSON.stringify(mapped)); +// flat also flattens a nested array when its index is read. +const nestedArray = [1, 2]; +const nestedSource = { length: 2, 0: nestedArray, get 1() { nestedArray[0] = 9; return 3; } }; +report('flat-nested-order', JSON.stringify(Array.prototype.flat.call(nestedSource))); +report('flat-negative', JSON.stringify(Array.prototype.flat.call({ length: 1, 0: [1, [2]] }, -1))); +report('flat-infinity', JSON.stringify(Array.prototype.flat.call({ length: 1, 0: [1, [2, [3]]] }, Infinity))); + console.log(resolved.join('\n'));