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.<method>.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.
This commit is contained in:
Chris Tate
2026-10-26 10:19:57 -05:00
parent a59df23c71
commit 928c30e908
2 changed files with 164 additions and 101 deletions
+121 -101
View File
@@ -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) {
@@ -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));