Read array-like elements for join, flat and flatMap in specification order

join checked each index with HasProperty before reading it, so a proxy's
has trap ran, and could hide a value or throw. flat converted its depth
only after it had read every index, and flatMap ran every callback before
it flattened any result, so a later read or callback could change an
array that should already have been copied.

- join reads every index with Get alone; a missing index joins as the
  empty string.
- flat converts its depth after the length and before the first index,
  and flatMap checks its callback there.
- Each present value, mapped first for flatMap, is flattened into the
  result before the next index is read, through the array dispatch's
  flat over `[value]`.

scr_dyn.h declares scr_dyn_array_like_call and scr_dyn_builtin_prototype,
the entry points of the two gated units, so every scr_* name the backend
can emit has a prototype in scr_runtime.h. The array-like corpus program's
logging proxy answers its has trap with `in` instead of Reflect.has, which
has no static lowering, so the program stays fully static.
This commit is contained in:
Chris Tate
2026-10-29 18:32:55 -05:00
parent 3cafa64fe7
commit 6c3affab90
3 changed files with 88 additions and 33 deletions
+8
View File
@@ -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
+44 -32
View File
@@ -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
+36 -1
View File
@@ -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'));