mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-10 01:56:15 +08:00
Follow the specification's reads and writes for array-like receivers
concat read a receiver's length before appending it, flatMap called its callback without thisArg, and with, toReversed, toSorted and toSpliced checked indexes with HasProperty, which a proxy's has trap or a getter observes. Writes to a string receiver were discarded, though a String wrapper's indexes below its length and its length are read-only. - concat appends a receiver that is not an array as its first element without reading `length`; the array dispatch's concat spreads the arguments. - flatMap calls the callback with thisArg as `this`. - with, toReversed, toSorted and toSpliced read every index with Get alone and define it on the result, so a missing index becomes undefined. toReversed reads from the last index down, with never reads the index it replaces, and toSpliced without arguments returns a copy. - On a string receiver, a write to an index below the string's length or to `length` throws "Cannot assign to read only property '<key>' of object '[object String]'", and deleting such an index throws "Cannot delete property '<index>' of [object String]", as in Node. Writes to number, boolean, bigint and symbol receivers are still discarded with their wrapper. - fill, copyWithin and reverse return their receiver, which for a primitive is its wrapper object, and concat's result starts with it. A compiled program has no wrapper objects, so these calls on a primitive throw the "not supported yet" Error. - An array receiver held as an untyped value ran these methods through scr_dyn_array_proto_call, which implements none of them, and threw "not supported yet". It now runs them through the array dispatch.
This commit is contained in:
@@ -171,8 +171,8 @@ static ScrDyn *al_item(ScrDyn *recv, size_t index) {
|
||||
}
|
||||
|
||||
/* A primitive receiver's ToObject wrapper is fresh and unobservable: the
|
||||
* generic array algorithms read through it and discard their writes, which
|
||||
* is exactly what a wrapper nobody holds observes. */
|
||||
* generic array algorithms read through it, and a write the wrapper accepts
|
||||
* is discarded with it. */
|
||||
static bool al_primitive(const ScrDyn *recv) {
|
||||
switch (recv->kind) {
|
||||
case SCR_DYN_BOOL:
|
||||
@@ -186,10 +186,49 @@ static bool al_primitive(const ScrDyn *recv) {
|
||||
}
|
||||
}
|
||||
|
||||
/* The "not supported yet" Error for a method whose result would be a
|
||||
* primitive receiver's wrapper object. */
|
||||
static void al_refuse_primitive(const ScrStr *method) {
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
scr_jb_puts(&b, "Array.prototype.");
|
||||
scr_jb_write(&b, method->data, method->len);
|
||||
scr_jb_puts(&b, " on a primitive receiver is not supported yet");
|
||||
scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b));
|
||||
}
|
||||
|
||||
/* A String wrapper's indexes below its UTF-16 length are read-only and
|
||||
* non-configurable, so Set throws on them and DeletePropertyOrThrow does
|
||||
* too; an index at or past the length is an ordinary property of the
|
||||
* wrapper. Number, boolean, bigint and symbol wrappers carry no index. */
|
||||
static bool al_string_index(const ScrDyn *recv, size_t index) {
|
||||
return recv->kind == SCR_DYN_STR && (double)index < scr_str_utf16_len(recv->v.str);
|
||||
}
|
||||
|
||||
/* V8's TypeError for a write or delete the String wrapper refuses; `key`
|
||||
* is the property's name. */
|
||||
static void al_string_refuse(const char *key, bool deleting) {
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
scr_jb_puts(&b, deleting ? "Cannot delete property '" : "Cannot assign to read only property '");
|
||||
scr_jb_puts(&b, key);
|
||||
scr_jb_puts(&b, deleting ? "' of [object String]" : "' of object '[object String]'");
|
||||
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));
|
||||
}
|
||||
|
||||
static void al_string_refuse_index(size_t index, bool deleting) {
|
||||
char text[32];
|
||||
snprintf(text, sizeof text, "%zu", index);
|
||||
al_string_refuse(text, deleting);
|
||||
}
|
||||
|
||||
/* Set(O, index, value, true): a failing write throws from scr_dyn_key_set
|
||||
* and the caller checks scr_exc_pending(). */
|
||||
static void al_set(ScrDyn *recv, size_t index, ScrDyn *value) {
|
||||
if (al_primitive(recv)) return;
|
||||
if (al_primitive(recv)) {
|
||||
if (al_string_index(recv, index)) al_string_refuse_index(index, false);
|
||||
return;
|
||||
}
|
||||
ScrStr *key = al_key(index);
|
||||
scr_dyn_key_set(recv, key, value);
|
||||
scr_str_release(key);
|
||||
@@ -197,15 +236,22 @@ static void al_set(ScrDyn *recv, size_t index, ScrDyn *value) {
|
||||
|
||||
/* DeletePropertyOrThrow(O, index). */
|
||||
static void al_delete(ScrDyn *recv, size_t index) {
|
||||
if (al_primitive(recv)) return;
|
||||
if (al_primitive(recv)) {
|
||||
if (al_string_index(recv, index)) al_string_refuse_index(index, true);
|
||||
return;
|
||||
}
|
||||
ScrStr *key = al_key(index);
|
||||
scr_dyn_key_delete(recv, key, true);
|
||||
scr_str_release(key);
|
||||
}
|
||||
|
||||
/* Set(O, "length", length, true). */
|
||||
/* Set(O, "length", length, true). A String wrapper's length is read-only
|
||||
* whatever the value. */
|
||||
static void al_set_length(ScrDyn *recv, size_t length) {
|
||||
if (al_primitive(recv)) return;
|
||||
if (al_primitive(recv)) {
|
||||
if (recv->kind == SCR_DYN_STR) al_string_refuse("length", false);
|
||||
return;
|
||||
}
|
||||
ScrStr *key = scr_str_new("length", 6);
|
||||
ScrDyn *value = scr_dyn_new_num((double)length);
|
||||
scr_dyn_key_set(recv, key, value);
|
||||
@@ -213,20 +259,25 @@ static void al_set_length(ScrDyn *recv, size_t length) {
|
||||
scr_str_release(key);
|
||||
}
|
||||
|
||||
/* The receiver's elements in index order as a fresh dyn array. Every index
|
||||
* the receiver does not carry becomes undefined, as toReversed, toSorted
|
||||
* and with create their result with CreateDataProperty for every index.
|
||||
* Owned (+1), NULL with the exception pending. */
|
||||
static ScrDyn *al_snapshot(ScrDyn *recv, size_t len) {
|
||||
/* fill, copyWithin and reverse return O, which for a primitive receiver is
|
||||
* its ToObject wrapper. A compiled program has no wrapper object to return,
|
||||
* so for a primitive the method throws the "not supported yet" Error. */
|
||||
static ScrDyn *al_receiver_result(ScrDyn *recv, const ScrStr *method) {
|
||||
if (!al_primitive(recv)) return scr_dyn_retain(recv);
|
||||
al_refuse_primitive(method);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* The receiver's elements in index order as a fresh dyn array, each read
|
||||
* with Get alone (an index the receiver does not carry reads undefined), as
|
||||
* toSorted and with read through holes; the element at `skip` (SIZE_MAX:
|
||||
* none) is `replacement` and is never read. Owned (+1), NULL with the
|
||||
* exception pending. */
|
||||
static ScrDyn *al_snapshot(ScrDyn *recv, size_t len, size_t skip, ScrDyn *replacement) {
|
||||
ScrDyn *out = scr_dyn_new_arr();
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (len > 1000000 && !al_iteration(i)) { scr_dyn_release(out); return NULL; }
|
||||
if (!al_present(recv, i)) {
|
||||
if (scr_exc_pending()) { scr_dyn_release(out); return NULL; }
|
||||
scr_dyn_arr_push(out, scr_dyn_retain(scr_dyn_undefined()));
|
||||
continue;
|
||||
}
|
||||
ScrDyn *item = al_item(recv, i);
|
||||
ScrDyn *item = i == skip ? scr_dyn_retain(replacement) : al_item(recv, i);
|
||||
if (!item) { scr_dyn_release(out); return NULL; }
|
||||
scr_dyn_arr_push(out, item);
|
||||
}
|
||||
@@ -272,12 +323,32 @@ static bool al_cb_check(ScrDyn *const *args, size_t argc) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* The methods this unit runs; every other name is the invoke unit's. */
|
||||
static bool al_owns(const ScrStr *method) {
|
||||
static const char *const names[] = {
|
||||
"concat", "copyWithin", "fill", "flat", "flatMap", "join", "pop", "push", "reverse",
|
||||
"shift", "splice", "toReversed", "toSorted", "toSpliced", "unshift", "with",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof names / sizeof names[0]; i++)
|
||||
if (al_is(method, names[i])) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* One packed argument list, borrowed: the array dispatch hands the
|
||||
* algorithm its arguments as a dyn array. */
|
||||
ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
|
||||
if (recv->kind == SCR_DYN_ARR ||
|
||||
(recv->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(recv)))
|
||||
(recv->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_array_ops(recv))) {
|
||||
/* An array receiver runs this unit's methods through the array
|
||||
* dispatch, which implements each of them for arrays. */
|
||||
char name[16];
|
||||
if (al_owns(method) && args && args->kind == SCR_DYN_ARR) {
|
||||
memcpy(name, method->data, method->len);
|
||||
name[method->len] = '\0';
|
||||
return scr_dyn_invoke(recv, name, args->v.arr.items, args->v.arr.len, name);
|
||||
}
|
||||
return scr_dyn_array_proto_call(recv, method, args);
|
||||
}
|
||||
/* Step 1 of each of these algorithms: `? ToObject(this value)`. The
|
||||
* array dispatch answers it for its own receivers; a nullish one never
|
||||
* reaches a length read. */
|
||||
@@ -289,6 +360,19 @@ ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
|
||||
if (!args || args->kind != SCR_DYN_ARR) scr_trap("scriptc: invalid Array.prototype arguments\n");
|
||||
ScrDyn *const *argv = args->v.arr.items;
|
||||
size_t argc = args->v.arr.len;
|
||||
if (al_is(method, "concat")) {
|
||||
/* A receiver that is not an array is not spreadable: it is the result's
|
||||
* first element, and its length is never read. The arguments then
|
||||
* spread as they do for an array receiver, so the array dispatch
|
||||
* answers `[receiver].concat(...args)`. A primitive receiver's first
|
||||
* element would be its wrapper object. */
|
||||
if (al_primitive(recv)) { al_refuse_primitive(method); return NULL; }
|
||||
ScrDyn *head = scr_dyn_new_arr();
|
||||
scr_dyn_arr_push(head, scr_dyn_retain(recv));
|
||||
ScrDyn *out = scr_dyn_invoke(head, "concat", argv, argc, "concat");
|
||||
scr_dyn_release(head);
|
||||
return out;
|
||||
}
|
||||
size_t len;
|
||||
if (!al_length(recv, &len)) return NULL;
|
||||
|
||||
@@ -394,7 +478,7 @@ ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
|
||||
scr_dyn_release(upper_value);
|
||||
if (scr_exc_pending()) return NULL;
|
||||
}
|
||||
return scr_dyn_retain(recv);
|
||||
return al_receiver_result(recv, method);
|
||||
}
|
||||
if (al_is(method, "join")) {
|
||||
ScrStr *sep = argc > 0 && argv[0]->kind != SCR_DYN_UNDEF
|
||||
@@ -441,7 +525,7 @@ ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
|
||||
al_set(recv, i, value);
|
||||
if (scr_exc_pending()) return NULL;
|
||||
}
|
||||
return scr_dyn_retain(recv);
|
||||
return al_receiver_result(recv, method);
|
||||
}
|
||||
if (al_is(method, "copyWithin")) {
|
||||
double target_d = 0;
|
||||
@@ -453,7 +537,7 @@ ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
|
||||
size_t to = al_rel_index(trunc(target_d), len);
|
||||
size_t from = al_rel_index(trunc(start_d), len);
|
||||
size_t end = al_rel_index(trunc(end_d), len);
|
||||
if (from >= end || to >= len) return scr_dyn_retain(recv);
|
||||
if (from >= end || to >= len) return al_receiver_result(recv, method);
|
||||
size_t count = end - from < len - to ? end - from : len - to;
|
||||
bool backwards = from < to && to < from + count;
|
||||
for (size_t step = 0; step < count; step++) {
|
||||
@@ -470,31 +554,7 @@ ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
|
||||
}
|
||||
if (scr_exc_pending()) return NULL;
|
||||
}
|
||||
return scr_dyn_retain(recv);
|
||||
}
|
||||
if (al_is(method, "concat")) {
|
||||
/* The receiver's own elements spread one level when it is a native
|
||||
* array; an array-like receiver is appended as a single value, as
|
||||
* Array.prototype.concat treats a non-array `this`. */
|
||||
ScrDyn *out = scr_dyn_new_arr();
|
||||
if (recv->kind == SCR_DYN_ARR) {
|
||||
for (size_t i = 0; i < len; i++)
|
||||
if (!al_copy_item(out, recv, i)) { scr_dyn_release(out); return NULL; }
|
||||
} else {
|
||||
scr_dyn_arr_push(out, scr_dyn_retain(recv));
|
||||
}
|
||||
/* An array argument spreads one level (holes included); every other
|
||||
* value is appended as itself. */
|
||||
for (size_t a = 0; a < argc; a++) {
|
||||
ScrDyn *arg = argv[a];
|
||||
if (arg->kind != SCR_DYN_ARR) {
|
||||
scr_dyn_arr_push(out, scr_dyn_retain(arg));
|
||||
continue;
|
||||
}
|
||||
for (size_t i = 0; i < arg->v.arr.len; i++)
|
||||
if (!al_copy_item(out, arg, i)) { scr_dyn_release(out); return NULL; }
|
||||
}
|
||||
return out;
|
||||
return al_receiver_result(recv, method);
|
||||
}
|
||||
if (al_is(method, "splice")) {
|
||||
/* No arguments is the one form whose actualStart is len, not 0: it
|
||||
@@ -576,18 +636,19 @@ ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
|
||||
scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1);
|
||||
return NULL;
|
||||
}
|
||||
ScrDyn *out = al_snapshot(recv, len);
|
||||
if (!out) return NULL;
|
||||
al_set(out, (size_t)index, argc > 1 ? argv[1] : scr_dyn_undefined());
|
||||
if (scr_exc_pending()) { scr_dyn_release(out); return NULL; }
|
||||
return out;
|
||||
return al_snapshot(recv, len, (size_t)index, argc > 1 ? argv[1] : scr_dyn_undefined());
|
||||
}
|
||||
if (al_is(method, "toReversed")) {
|
||||
ScrDyn *out = al_snapshot(recv, len);
|
||||
if (!out) return NULL;
|
||||
ScrDyn *reversed = scr_dyn_invoke(out, "reverse", NULL, 0, "toReversed");
|
||||
scr_dyn_release(out);
|
||||
return reversed;
|
||||
/* Get(O, len - k - 1) for k ascending: the reads go from the last
|
||||
* index down. */
|
||||
ScrDyn *out = scr_dyn_new_arr();
|
||||
for (size_t k = 0; k < len; k++) {
|
||||
if (len > 1000000 && !al_iteration(k)) { scr_dyn_release(out); return NULL; }
|
||||
ScrDyn *item = al_item(recv, len - k - 1);
|
||||
if (!item) { scr_dyn_release(out); return NULL; }
|
||||
scr_dyn_arr_push(out, item);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if (al_is(method, "toSorted")) {
|
||||
ScrDyn *cmp = argc > 0 ? argv[0] : scr_dyn_undefined();
|
||||
@@ -596,7 +657,7 @@ ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
|
||||
return NULL;
|
||||
}
|
||||
ScrDyn *out = al_snapshot(recv, len);
|
||||
ScrDyn *out = al_snapshot(recv, len, SIZE_MAX, NULL);
|
||||
if (!out) return NULL;
|
||||
if (out->v.arr.len < 2) return out;
|
||||
ScrDyn *sorted = scr_dyn_invoke(out, "sort", argv, argc, "toSorted");
|
||||
@@ -607,7 +668,8 @@ ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
|
||||
double start_d = 0;
|
||||
if (argc > 0 && !al_number(argv[0], &start_d)) return NULL;
|
||||
size_t start = al_rel_index(trunc(start_d), len);
|
||||
size_t removed = len - start;
|
||||
/* Without `start` nothing is skipped: the result is a copy. */
|
||||
size_t removed = argc > 0 ? len - start : 0;
|
||||
if (argc > 1) {
|
||||
double count_d = 0;
|
||||
if (!al_number(argv[1], &count_d)) return NULL;
|
||||
@@ -629,15 +691,21 @@ ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
|
||||
scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1);
|
||||
return NULL;
|
||||
}
|
||||
/* Every copied index is read with Get and defined on the result, so a
|
||||
* missing one becomes an own undefined, not a hole. */
|
||||
ScrDyn *out = scr_dyn_new_arr();
|
||||
for (size_t i = 0; i < start; i++) {
|
||||
if (len > 1000000 && !al_iteration(i)) { scr_dyn_release(out); return NULL; }
|
||||
if (!al_copy_item(out, recv, i)) { scr_dyn_release(out); return NULL; }
|
||||
ScrDyn *item = al_item(recv, i);
|
||||
if (!item) { scr_dyn_release(out); return NULL; }
|
||||
scr_dyn_arr_push(out, item);
|
||||
}
|
||||
for (size_t j = 2; j < argc; j++) scr_dyn_arr_push(out, scr_dyn_retain(argv[j]));
|
||||
for (size_t i = start + removed; i < len; i++) {
|
||||
if (len > 1000000 && !al_iteration(i)) { scr_dyn_release(out); return NULL; }
|
||||
if (!al_copy_item(out, recv, i)) { scr_dyn_release(out); return NULL; }
|
||||
ScrDyn *item = al_item(recv, i);
|
||||
if (!item) { scr_dyn_release(out); return NULL; }
|
||||
scr_dyn_arr_push(out, item);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -661,7 +729,9 @@ ScrDyn *scr_dyn_array_like_call(ScrDyn *recv, ScrStr *method, ScrDyn *args) {
|
||||
if (mapping) {
|
||||
ScrDyn *index = scr_dyn_new_num((double)i);
|
||||
ScrDyn *cbargs[3] = { item, index, recv };
|
||||
scr_dyn_this_push_dyn(argc > 1 ? argv[1] : scr_dyn_undefined());
|
||||
ScrDyn *result = scr_dyn_call(argv[0], cbargs, 3, "callback");
|
||||
scr_dyn_this_pop();
|
||||
scr_dyn_release(index);
|
||||
scr_dyn_release(item);
|
||||
if (!result) { scr_dyn_release(mapped); return NULL; }
|
||||
|
||||
@@ -40,7 +40,7 @@ report('flat', Array.prototype.flat.call(nested, 1).join(''));
|
||||
report('flat-deep', Array.prototype.flat.call(nested, 2).join(''));
|
||||
report('flatMap', Array.prototype.flatMap.call(nested, (value) => [value, value]).length);
|
||||
|
||||
// Holes and absent indexes stay holes.
|
||||
// toReversed defines every index on its result, so a hole becomes undefined.
|
||||
const sparse = { length: 3, 0: 'a', 2: 'c' };
|
||||
const copied = Array.prototype.toReversed.call(sparse);
|
||||
report('holes', '1' in copied ? 'present' : 'absent');
|
||||
@@ -69,4 +69,84 @@ attempt('null-toReversed', () => Array.prototype.toReversed.call(null));
|
||||
attempt('undefined-with', () => Array.prototype.with.call(undefined, 0, 1));
|
||||
attempt('null-splice', () => Array.prototype.splice.call(null, 0, 1));
|
||||
|
||||
// A proxy over `target` that logs its string-keyed reads and its has checks.
|
||||
const traced = (target) => {
|
||||
const log = [];
|
||||
const proxy = new Proxy(target, {
|
||||
get(object, key, receiver) {
|
||||
if (typeof key === 'string') log.push('get:' + key);
|
||||
return Reflect.get(object, key, receiver);
|
||||
},
|
||||
has(object, key) {
|
||||
log.push('has:' + String(key));
|
||||
return Reflect.has(object, key);
|
||||
},
|
||||
});
|
||||
return { proxy, log };
|
||||
};
|
||||
// concat appends a receiver that is not spreadable as one value, without
|
||||
// reading its length.
|
||||
const unread = traced({ length: 2, 0: 'a', 1: 'b' });
|
||||
const joined = Array.prototype.concat.call(unread.proxy, [1]);
|
||||
report('concat-length', joined.length);
|
||||
report('concat-reads', unread.log.join(','));
|
||||
|
||||
// flatMap calls the callback with thisArg as `this`.
|
||||
const tagged = { length: 2, 0: 'a', 1: 'b' };
|
||||
report('flatMap-this', Array.prototype.flatMap.call(tagged, function (value) {
|
||||
return [value, this.tag];
|
||||
}, { tag: 't' }).join(''));
|
||||
|
||||
// A string's ToObject wrapper has read-only indexes below its length and a
|
||||
// read-only length, so the mutating methods throw on them.
|
||||
attempt('string-push', () => Array.prototype.push.call('ab', 'c'));
|
||||
attempt('string-pop', () => Array.prototype.pop.call('ab'));
|
||||
attempt('string-pop-empty', () => Array.prototype.pop.call(''));
|
||||
attempt('string-shift', () => Array.prototype.shift.call('ab'));
|
||||
attempt('string-unshift', () => Array.prototype.unshift.call('ab', 'x'));
|
||||
attempt('string-unshift-none', () => Array.prototype.unshift.call('ab'));
|
||||
attempt('string-reverse', () => Array.prototype.reverse.call('ab'));
|
||||
attempt('string-fill', () => Array.prototype.fill.call('ab', 'x'));
|
||||
attempt('string-copyWithin', () => Array.prototype.copyWithin.call('abc', 0, 1));
|
||||
attempt('string-splice', () => Array.prototype.splice.call('abc', 1, 1));
|
||||
attempt('string-splice-insert', () => Array.prototype.splice.call('', 0, 0, 'x'));
|
||||
attempt('string-splice-none', () => Array.prototype.splice.call('abc'));
|
||||
// Other primitives' wrappers carry no indexes and a writable length.
|
||||
attempt('number-push', () => Array.prototype.push.call(5, 'x'));
|
||||
attempt('boolean-pop', () => Array.prototype.pop.call(true));
|
||||
|
||||
// toReversed reads with Get alone, from the last index down.
|
||||
const backwards = traced({ length: 3, 0: 'a', 2: 'c' });
|
||||
report('toReversed', JSON.stringify(Array.prototype.toReversed.call(backwards.proxy)));
|
||||
report('toReversed-reads', backwards.log.join(','));
|
||||
|
||||
// with never reads the replaced index, and reads every other one with Get.
|
||||
const replaced = traced({ length: 3, 0: 'a', 2: 'c' });
|
||||
report('with', JSON.stringify(Array.prototype.with.call(replaced.proxy, 0, 'z')));
|
||||
report('with-reads', replaced.log.join(','));
|
||||
|
||||
// toSorted reads every index with Get, holes included.
|
||||
const sorted = traced({ length: 3, 0: 'b', 2: 'a' });
|
||||
report('toSorted', JSON.stringify(Array.prototype.toSorted.call(sorted.proxy)));
|
||||
report('toSorted-reads', sorted.log.join(','));
|
||||
|
||||
// toSpliced without arguments copies the receiver; a missing index becomes
|
||||
// an own undefined, read with Get alone.
|
||||
const copiedOut = traced({ length: 3, 0: 'a', 2: 'c' });
|
||||
const copy = Array.prototype.toSpliced.call(copiedOut.proxy);
|
||||
report('toSpliced-none', JSON.stringify(copy) + ' ' + ('1' in copy));
|
||||
report('toSpliced-none-reads', copiedOut.log.join(','));
|
||||
const skipped = traced({ length: 4, 0: 'a', 3: 'd' });
|
||||
report('toSpliced-holes', JSON.stringify(Array.prototype.toSpliced.call(skipped.proxy, 1, 1, 'X')));
|
||||
report('toSpliced-reads', skipped.log.join(','));
|
||||
|
||||
// An array receiver held as an untyped value runs the array methods.
|
||||
const either = (useArray) => (useArray ? ['p', 'q'] : { length: 0 });
|
||||
const list = either(resolved.length > 0);
|
||||
report('array-push', Array.prototype.push.call(list, 'r'));
|
||||
report('array-reverse', Array.prototype.join.call(Array.prototype.reverse.call(list), ''));
|
||||
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(''));
|
||||
|
||||
console.log(resolved.join('\n'));
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile } from "@scriptc/compiler";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
// fill, copyWithin and reverse return their receiver, and concat's result
|
||||
// starts with it: for a primitive receiver that is its wrapper object, which
|
||||
// a compiled program has no value for, so these calls refuse with a catchable
|
||||
// error. A write the String wrapper refuses still throws its TypeError first.
|
||||
test("Array.prototype methods whose result is a primitive's wrapper refuse", async () => {
|
||||
const outDir = mkdtempSync(join(tmpdir(), "scriptc-array-like-refusals-"));
|
||||
const entry = join(outDir, "main.js");
|
||||
writeFileSync(
|
||||
entry,
|
||||
`
|
||||
function attempt(run) {
|
||||
try { run(); console.log("returned"); } catch (error) { console.log(error.name, error.message); }
|
||||
}
|
||||
attempt(() => Array.prototype.reverse.call("a"));
|
||||
attempt(() => Array.prototype.fill.call(5, 0));
|
||||
attempt(() => Array.prototype.fill.call("ab", "x", 2));
|
||||
attempt(() => Array.prototype.copyWithin.call(true, 0, 0));
|
||||
attempt(() => Array.prototype.concat.call("ab", [1]));
|
||||
attempt(() => Array.prototype.reverse.call("ab"));
|
||||
attempt(() => Array.prototype.join.call("ab", "-"));
|
||||
`,
|
||||
);
|
||||
const result = await compile(entry, {
|
||||
backend: "llvm",
|
||||
sanitize: process.env["SCRIPTC_SAN"] === "1",
|
||||
outDir,
|
||||
outPath: join(outDir, "program"),
|
||||
});
|
||||
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
|
||||
if (!result.ok) return;
|
||||
const { stdout, stderr } = await execFileAsync(result.binaryPath, [], { encoding: "utf8" });
|
||||
const refused = (method: string): string =>
|
||||
`Error Array.prototype.${method} on a primitive receiver is not supported yet`;
|
||||
expect(stdout).toBe(
|
||||
[
|
||||
refused("reverse"),
|
||||
refused("fill"),
|
||||
refused("fill"),
|
||||
refused("copyWithin"),
|
||||
refused("concat"),
|
||||
"TypeError Cannot assign to read only property '0' of object '[object String]'",
|
||||
"returned",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
expect(stderr).toBe("");
|
||||
});
|
||||
@@ -1 +1 @@
|
||||
{ "plain": 3300, "san": 5400 }
|
||||
{ "plain": 3700, "san": 6200 }
|
||||
|
||||
Reference in New Issue
Block a user