From 928c30e908fc2faf0caece6967dcf44500974d34 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 26 Oct 2026 10:19:57 -0500 Subject: [PATCH] Bind thisArg in the array callback methods of dynamic arrays forEach, map, filter, some, every, find, findIndex, findLast, findLastIndex and flatMap on a dynamic array, and through Array.prototype..call on a native array, called the callback without its thisArg, so `this` in the callback was not the second argument. These methods now bind `this` to the thisArg, or undefined when there is none, once per call, around all of the method's callbacks. --- packages/runtime/src/scr_dyn_invoke.c | 222 ++++++++++-------- .../array-prototype-call-native-arrays.ts | 43 ++++ 2 files changed, 164 insertions(+), 101 deletions(-) diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 6deca008c..7a9b1bc35 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -916,6 +916,117 @@ void scr_dyn_invoke_discard_id(ScrDyn *recv, ScrDyn *callee, const char *method, scr_dyn_release(scr_dyn_invoke_prepared_id(recv, callee, method, mid_slot, args, argc, what)); } +/* The callback methods of a dense dyn array that take a thisArg: + * forEach, map, filter, some, every, find, findIndex, findLast, + * findLastIndex and flatMap. The caller binds `this` for the callbacks. + * `len` is the length before the first callback. */ +static ScrDyn *dyn_arr_callback_method(ScrDyn *recv, int mid, ScrDyn *const *args, size_t argc, + size_t len, ScrDyn *callback_recv) { + if ((mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex)) { + if (!dyn_cb_check(args, argc)) return NULL; + ScrDyn *visible_recv = callback_recv ? callback_recv : recv; + for (size_t i = len; i > 0; i--) { + size_t index = i - 1; + ScrDyn *item = index < recv->v.arr.len + ? scr_dyn_retain(recv->v.arr.items[index]) + : scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *result = dyn_call_cb(args[0], item, index, visible_recv); + if (!result) { scr_dyn_release(item); return NULL; } + bool matched = scr_dyn_truthy(result); + scr_dyn_release(result); + if (matched) { + if ((mid == DYN_M_findLast)) return item; + scr_dyn_release(item); + return scr_dyn_new_num((double)index); + } + scr_dyn_release(item); + } + return (mid == DYN_M_findLast) + ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(-1); + } + if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || + (mid == DYN_M_filter) || (mid == DYN_M_some) || + (mid == DYN_M_every) || (mid == DYN_M_find) || + (mid == DYN_M_findIndex)) { + if (!dyn_cb_check(args, argc)) return NULL; + ScrDyn *cb = args[0]; + ScrDyn *out = ((mid == DYN_M_map) || (mid == DYN_M_filter)) ? scr_dyn_new_arr() : NULL; + /* Array iteration methods snapshot length before the first callback. + * A shrinking callback can remove a later dense entry; an expanding + * callback must not make the new tail observable to this pass. Live + * Web-boundary capsules are the externally visible receiver, so pass + * that capsule as callback arg 3: mutations through the callback's + * array argument then commit directly to the original static array. */ + size_t n = len; + ScrDyn *visible_recv = callback_recv ? callback_recv : recv; + for (size_t step = 0; step < n; step++) { + size_t i = step; + if (i >= recv->v.arr.len) continue; + ScrDyn *item = scr_dyn_retain(recv->v.arr.items[i]); + ScrDyn *r = dyn_call_cb(cb, item, i, visible_recv); + if (!r) { scr_dyn_release(item); scr_dyn_release(out); return NULL; } + if ((mid == DYN_M_map)) { + scr_dyn_arr_push(out, r); /* ownership moves in */ + r = NULL; + } else { + bool truthy = scr_dyn_truthy(r); + scr_dyn_release(r); + if ((mid == DYN_M_filter) && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); + if ((mid == DYN_M_some) && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } + if ((mid == DYN_M_every) && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } + if ((mid == DYN_M_find) && truthy) return item; /* +1 moves out */ + if ((mid == DYN_M_findIndex) && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } + } + scr_dyn_release(item); + } + if (out) return out; + if ((mid == DYN_M_some)) return scr_dyn_new_bool(false); + if ((mid == DYN_M_every)) return scr_dyn_new_bool(true); + if ((mid == DYN_M_find)) return scr_dyn_retain(scr_dyn_undefined()); + if ((mid == DYN_M_findIndex)) return scr_dyn_new_num(-1); + return scr_dyn_retain(scr_dyn_undefined()); /* forEach */ + } + if ((mid == DYN_M_flatMap)) { + /* JS Array.prototype.flatMap over a dyn array: map + a depth-1 + * flatten. Native dyn-array results flatten element-by-element; a + * WRAPPED engine array flattens through the routed keyed reads + * (elements wrap back scalar-normalized); everything else pushes + * as a single element (JS keeps non-array callback results whole). + * JS's spec snapshots the length up front (elements appended by + * the callback are not visited). */ + if (!dyn_cb_check(args, argc)) return NULL; + ScrDyn *cb = args[0]; + ScrDyn *out = scr_dyn_new_arr(); + size_t n = recv->v.arr.len; + for (size_t i = 0; i < n && i < recv->v.arr.len; i++) { + if (!dyn_borrow_present(recv, i)) { if (scr_exc_pending()) { scr_dyn_release(out); return NULL; } continue; } + ScrDyn *item = dyn_borrow_item(recv, i); + if (!item) { scr_dyn_release(out); return NULL; } + ScrDyn *r = dyn_call_cb( + cb, item, i, callback_recv ? callback_recv : recv); + scr_dyn_release(item); + if (!r) { scr_dyn_release(out); return NULL; } + if (r->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(r)) { + ScrDyn *view = scr_dyn_typed_ref_materialize(r); + scr_dyn_release(r); + r = view; + if (!r) { scr_dyn_release(out); return NULL; } + } + if (r->kind == SCR_DYN_ARR) { + for (size_t j = 0; j < r->v.arr.len; j++) { + if (dyn_borrow_present(r, j)) scr_dyn_arr_push(out, dyn_borrow_item(r, j)); + if (scr_exc_pending()) { scr_dyn_release(r); scr_dyn_release(out); return NULL; } + } + scr_dyn_release(r); + } else { + scr_dyn_arr_push(out, r); /* ownership moves in */ + } + } + return out; + } + return NULL; +} + static ScrDyn *scr_dyn_invoke_impl( ScrDyn *recv, const char *method, int mid, ScrDyn *const *args, size_t argc, const char *what, ScrDyn *callback_recv) { @@ -1367,27 +1478,16 @@ static ScrDyn *scr_dyn_invoke_impl( } return (mid == DYN_M_includes) ? scr_dyn_new_bool(false) : scr_dyn_new_num(-1); } - if ((mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex)) { - if (!dyn_cb_check(args, argc)) return NULL; - ScrDyn *visible_recv = callback_recv ? callback_recv : recv; - for (size_t i = len; i > 0; i--) { - size_t index = i - 1; - ScrDyn *item = index < recv->v.arr.len - ? scr_dyn_retain(recv->v.arr.items[index]) - : scr_dyn_retain(scr_dyn_undefined()); - ScrDyn *result = dyn_call_cb(args[0], item, index, visible_recv); - if (!result) { scr_dyn_release(item); return NULL; } - bool matched = scr_dyn_truthy(result); - scr_dyn_release(result); - if (matched) { - if ((mid == DYN_M_findLast)) return item; - scr_dyn_release(item); - return scr_dyn_new_num((double)index); - } - scr_dyn_release(item); - } - return (mid == DYN_M_findLast) - ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(-1); + if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || (mid == DYN_M_filter) || + (mid == DYN_M_some) || (mid == DYN_M_every) || (mid == DYN_M_find) || + (mid == DYN_M_findIndex) || (mid == DYN_M_findLast) || (mid == DYN_M_findLastIndex) || + (mid == DYN_M_flatMap)) { + /* Every callback runs with `this` bound to the thisArg, or undefined + * when there is none. */ + scr_dyn_this_push_dyn(argc > 1 ? args[1] : scr_dyn_undefined()); + ScrDyn *result = dyn_arr_callback_method(recv, mid, args, argc, len, callback_recv); + scr_dyn_this_pop(); + return result; } if ((mid == DYN_M_join)) { ScrJsonBuf b; @@ -1550,48 +1650,6 @@ static ScrDyn *scr_dyn_invoke_impl( } return scr_dyn_retain(recv); } - if ((mid == DYN_M_forEach) || (mid == DYN_M_map) || - (mid == DYN_M_filter) || (mid == DYN_M_some) || - (mid == DYN_M_every) || (mid == DYN_M_find) || - (mid == DYN_M_findIndex)) { - if (!dyn_cb_check(args, argc)) return NULL; - ScrDyn *cb = args[0]; - ScrDyn *out = ((mid == DYN_M_map) || (mid == DYN_M_filter)) ? scr_dyn_new_arr() : NULL; - /* Array iteration methods snapshot length before the first callback. - * A shrinking callback can remove a later dense entry; an expanding - * callback must not make the new tail observable to this pass. Live - * Web-boundary capsules are the externally visible receiver, so pass - * that capsule as callback arg 3: mutations through the callback's - * array argument then commit directly to the original static array. */ - size_t n = len; - ScrDyn *visible_recv = callback_recv ? callback_recv : recv; - for (size_t step = 0; step < n; step++) { - size_t i = step; - if (i >= recv->v.arr.len) continue; - ScrDyn *item = scr_dyn_retain(recv->v.arr.items[i]); - ScrDyn *r = dyn_call_cb(cb, item, i, visible_recv); - if (!r) { scr_dyn_release(item); scr_dyn_release(out); return NULL; } - if ((mid == DYN_M_map)) { - scr_dyn_arr_push(out, r); /* ownership moves in */ - r = NULL; - } else { - bool truthy = scr_dyn_truthy(r); - scr_dyn_release(r); - if ((mid == DYN_M_filter) && truthy) scr_dyn_arr_push(out, scr_dyn_retain(item)); - if ((mid == DYN_M_some) && truthy) { scr_dyn_release(item); return scr_dyn_new_bool(true); } - if ((mid == DYN_M_every) && !truthy) { scr_dyn_release(item); return scr_dyn_new_bool(false); } - if ((mid == DYN_M_find) && truthy) return item; /* +1 moves out */ - if ((mid == DYN_M_findIndex) && truthy) { scr_dyn_release(item); return scr_dyn_new_num((double)i); } - } - scr_dyn_release(item); - } - if (out) return out; - if ((mid == DYN_M_some)) return scr_dyn_new_bool(false); - if ((mid == DYN_M_every)) return scr_dyn_new_bool(true); - if ((mid == DYN_M_find)) return scr_dyn_retain(scr_dyn_undefined()); - if ((mid == DYN_M_findIndex)) return scr_dyn_new_num(-1); - return scr_dyn_retain(scr_dyn_undefined()); /* forEach */ - } if ((mid == DYN_M_reduce) || (mid == DYN_M_reduceRight)) { if (!dyn_cb_check(args, argc)) return NULL; bool backward = (mid == DYN_M_reduceRight); @@ -1616,44 +1674,6 @@ static ScrDyn *scr_dyn_invoke_impl( scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } - if ((mid == DYN_M_flatMap)) { - /* JS Array.prototype.flatMap over a dyn array: map + a depth-1 - * flatten. Native dyn-array results flatten element-by-element; a - * WRAPPED engine array flattens through the routed keyed reads - * (elements wrap back scalar-normalized); everything else pushes - * as a single element (JS keeps non-array callback results whole). - * JS's spec snapshots the length up front (elements appended by - * the callback are not visited). */ - if (!dyn_cb_check(args, argc)) return NULL; - ScrDyn *cb = args[0]; - ScrDyn *out = scr_dyn_new_arr(); - size_t n = recv->v.arr.len; - for (size_t i = 0; i < n && i < recv->v.arr.len; i++) { - if (!dyn_borrow_present(recv, i)) { if (scr_exc_pending()) { scr_dyn_release(out); return NULL; } continue; } - ScrDyn *item = dyn_borrow_item(recv, i); - if (!item) { scr_dyn_release(out); return NULL; } - ScrDyn *r = dyn_call_cb( - cb, item, i, callback_recv ? callback_recv : recv); - scr_dyn_release(item); - if (!r) { scr_dyn_release(out); return NULL; } - if (r->kind == SCR_DYN_TYPED_REF && scr_dyn_ref_is_array(r)) { - ScrDyn *view = scr_dyn_typed_ref_materialize(r); - scr_dyn_release(r); - r = view; - if (!r) { scr_dyn_release(out); return NULL; } - } - if (r->kind == SCR_DYN_ARR) { - for (size_t j = 0; j < r->v.arr.len; j++) { - if (dyn_borrow_present(r, j)) scr_dyn_arr_push(out, dyn_borrow_item(r, j)); - if (scr_exc_pending()) { scr_dyn_release(r); scr_dyn_release(out); return NULL; } - } - scr_dyn_release(r); - } else { - scr_dyn_arr_push(out, r); /* ownership moves in */ - } - } - return out; - } if ((mid == DYN_M_sort)) { ScrDyn *cmp = argc > 0 ? args[0] : scr_dyn_undefined(); if (cmp->kind != SCR_DYN_UNDEF && cmp->kind != SCR_DYN_FUNC) { diff --git a/tests/corpus/array-prototype-call-native-arrays.ts b/tests/corpus/array-prototype-call-native-arrays.ts index ce198ff73..d5bfec507 100644 --- a/tests/corpus/array-prototype-call-native-arrays.ts +++ b/tests/corpus/array-prototype-call-native-arrays.ts @@ -20,3 +20,46 @@ console.log(words.join(","), Array.prototype.includes.call(words, "a")); const records: { id: number }[] = [{ id: 1 }, { id: 2 }, { id: 3 }]; Array.prototype.copyWithin.call(records, 1, 0, 1); console.log(records.map((r) => r.id).join(","), records[0] === records[1]); + +// The callback methods bind `this` to their thisArg, for native arrays +// through .call and for dynamic arrays called directly. +type Scale = { k: number; seen: number[] }; +const scale: Scale = { k: 10, seen: [] }; +const values: number[] = [1, 2, 3, 4]; +Array.prototype.forEach.call(values, function (this: Scale, v: number) { + this.seen.push(v); +}, scale); +console.log(scale.seen.join(",")); +console.log(Array.prototype.map.call(values, function (this: Scale, v: number) { + return v * this.k; +}, scale).join(",")); +console.log(Array.prototype.filter.call(values, function (this: Scale, v: number) { + return v * this.k > 15; +}, scale).join(",")); +console.log( + Array.prototype.some.call(values, function (this: Scale, v: number) { return v === this.k / 5; }, scale), + Array.prototype.every.call(values, function (this: Scale, v: number) { return v < this.k; }, scale), + Array.prototype.find.call(values, function (this: Scale, v: number) { return v * this.k === 30; }, scale), + Array.prototype.findIndex.call(values, function (this: Scale, v: number) { return v * this.k === 30; }, scale), + Array.prototype.findLast.call(values, function (this: Scale, v: number) { return v * this.k < 30; }, scale), + Array.prototype.findLastIndex.call(values, function (this: Scale, v: number) { return v * this.k < 30; }, scale), +); +console.log(Array.prototype.flatMap.call(values, function (this: unknown, v: number) { + return [v, v * (this as Scale).k]; +}, scale).join(",")); +const parsed: any = JSON.parse("[1, 2, 3]"); +const sum = { total: 0 }; +parsed.forEach(function (this: { total: number }, v: number) { + this.total += v; +}, sum); +console.log(sum.total, parsed.map(function (this: Scale, v: number) { return v * this.k; }, scale).join(",")); +class Counter { + total = 0; + addAll(items: number[]): number { + Array.prototype.forEach.call(items, (v: number) => { + this.total += v; + }); + return this.total; + } +} +console.log(new Counter().addAll(values));