From f3a693902396583f0616cbadc2acba2b7346af12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Mon, 20 Apr 2026 11:52:01 +0200 Subject: [PATCH] Port js_allocate_fast_array helper for non-mutating array methods Ported from bellard/quickjs: - https://github.com/bellard/quickjs/commit/feefdb1742ed2de8ecd09fd66a848d1695294b3a - https://github.com/bellard/quickjs/commit/4c722cea4e709682003eb0da67b393ea38c56e5e --- quickjs.c | 112 +++++++++++++------------------------- tests/test_array_to_gc.js | 69 +++++++++++++++++++++++ 2 files changed, 108 insertions(+), 73 deletions(-) create mode 100644 tests/test_array_to_gc.js diff --git a/quickjs.c b/quickjs.c index 3bf342e31..a3728722d 100644 --- a/quickjs.c +++ b/quickjs.c @@ -10013,6 +10013,35 @@ static int add_fast_array_element(JSContext *ctx, JSObject *p, return true; } +/* Allocate a new fast array initialized to JS_UNDEFINED. Its maximum + size is 2^31-1 elements. For convenience, 'len' is a 64 bit + integer. */ +static JSValue js_allocate_fast_array(JSContext *ctx, int64_t len) +{ + JSValue arr; + JSObject *p; + int i; + + if (len > INT32_MAX) + return JS_ThrowRangeError(ctx, "invalid array length"); + arr = JS_NewArray(ctx); + if (JS_IsException(arr)) + return arr; + if (len > 0) { + p = JS_VALUE_GET_OBJ(arr); + if (expand_fast_array(ctx, p, len) < 0) { + JS_FreeValue(ctx, arr); + return JS_EXCEPTION; + } + p->u.array.count = len; + for(i = 0; i < len; i++) + p->u.array.u.values[i] = JS_UNDEFINED; + /* update the 'length' field */ + set_value(ctx, &p->prop[0].u.value, js_int32(len)); + } + return arr; +} + static void js_free_desc(JSContext *ctx, JSPropertyDescriptor *desc) { JS_FreeValue(ctx, desc->getter); @@ -41608,11 +41637,6 @@ static JSValue js_array_with(JSContext *ctx, JSValueConst this_val, if (js_get_length64(ctx, &len, obj)) goto exception; - if (len > UINT32_MAX) { - JS_ThrowRangeError(ctx, "invalid array length"); - goto exception; - } - if (JS_ToInt64Sat(ctx, &idx, argv[0])) goto exception; @@ -41624,15 +41648,11 @@ static JSValue js_array_with(JSContext *ctx, JSValueConst this_val, goto exception; } - arr = JS_NewArray(ctx); + arr = js_allocate_fast_array(ctx, len); if (JS_IsException(arr)) goto exception; p = JS_VALUE_GET_OBJ(arr); - if (expand_fast_array(ctx, p, len) < 0) - goto exception; - p->u.array.count = len; - i = 0; pval = p->u.array.u.values; if (js_get_fast_array(ctx, obj, &arrp, &count32) && count32 == len) { @@ -41644,21 +41664,14 @@ static JSValue js_array_with(JSContext *ctx, JSValueConst this_val, } else { for (; i < idx; i++, pval++) if (-1 == JS_TryGetPropertyInt64(ctx, obj, i, pval)) - goto fill_and_fail; + goto exception; *pval = js_dup(argv[1]); for (i++, pval++; i < len; i++, pval++) { - if (-1 == JS_TryGetPropertyInt64(ctx, obj, i, pval)) { - fill_and_fail: - for (; i < len; i++, pval++) - *pval = JS_UNDEFINED; + if (-1 == JS_TryGetPropertyInt64(ctx, obj, i, pval)) goto exception; - } } } - if (JS_SetProperty(ctx, arr, JS_ATOM_length, js_int64(len)) < 0) - goto exception; - ret = arr; arr = JS_UNDEFINED; @@ -42568,20 +42581,12 @@ static JSValue js_array_toReversed(JSContext *ctx, JSValueConst this_val, if (js_get_length64(ctx, &len, obj)) goto exception; - if (len > UINT32_MAX) { - JS_ThrowRangeError(ctx, "invalid array length"); - goto exception; - } - - arr = JS_NewArray(ctx); + arr = js_allocate_fast_array(ctx, len); if (JS_IsException(arr)) goto exception; if (len > 0) { p = JS_VALUE_GET_OBJ(arr); - if (expand_fast_array(ctx, p, len) < 0) - goto exception; - p->u.array.count = len; i = len - 1; pval = p->u.array.u.values; @@ -42591,17 +42596,10 @@ static JSValue js_array_toReversed(JSContext *ctx, JSValueConst this_val, } else { // Query order is observable; test262 expects descending order. for (; i >= 0; i--, pval++) { - if (-1 == JS_TryGetPropertyInt64(ctx, obj, i, pval)) { - // Exception; initialize remaining elements. - for (; i >= 0; i--, pval++) - *pval = JS_UNDEFINED; + if (-1 == JS_TryGetPropertyInt64(ctx, obj, i, pval)) goto exception; - } } } - - if (JS_SetProperty(ctx, arr, JS_ATOM_length, js_int64(len)) < 0) - goto exception; } ret = arr; @@ -42727,8 +42725,6 @@ static JSValue js_array_toSpliced(JSContext *ctx, JSValueConst this_val, int64_t i, j, len, newlen, start, add, del; uint32_t count32; - pval = NULL; - last = NULL; ret = JS_EXCEPTION; arr = JS_UNDEFINED; @@ -42753,17 +42749,12 @@ static JSValue js_array_toSpliced(JSContext *ctx, JSValueConst this_val, add = argc - 2; newlen = len + add - del; - if (newlen > UINT32_MAX) { - // Per spec: TypeError if newlen >= 2**53, RangeError below - if (newlen > MAX_SAFE_INTEGER) { - JS_ThrowTypeError(ctx, "invalid array length"); - } else { - JS_ThrowRangeError(ctx, "invalid array length"); - } + if (newlen > MAX_SAFE_INTEGER) { + JS_ThrowTypeError(ctx, "invalid array length"); goto exception; } - arr = JS_NewArray(ctx); + arr = js_allocate_fast_array(ctx, newlen); if (JS_IsException(arr)) goto exception; @@ -42771,10 +42762,6 @@ static JSValue js_array_toSpliced(JSContext *ctx, JSValueConst this_val, goto done; p = JS_VALUE_GET_OBJ(arr); - if (expand_fast_array(ctx, p, newlen) < 0) - goto exception; - - p->u.array.count = newlen; pval = &p->u.array.u.values[0]; last = &p->u.array.u.values[newlen]; @@ -42798,17 +42785,11 @@ static JSValue js_array_toSpliced(JSContext *ctx, JSValueConst this_val, assert(pval == last); - if (JS_SetProperty(ctx, arr, JS_ATOM_length, js_int64(newlen)) < 0) - goto exception; - done: ret = arr; arr = JS_UNDEFINED; exception: - while (pval != last) - *pval++ = JS_UNDEFINED; - JS_FreeValue(ctx, arr); JS_FreeValue(ctx, obj); return ret; @@ -43141,21 +43122,12 @@ static JSValue js_array_toSorted(JSContext *ctx, JSValueConst this_val, if (js_get_length64(ctx, &len, obj)) goto exception; - if (len > UINT32_MAX) { - JS_ThrowRangeError(ctx, "invalid array length"); - goto exception; - } - - arr = JS_NewArray(ctx); + arr = js_allocate_fast_array(ctx, len); if (JS_IsException(arr)) goto exception; if (len > 0) { p = JS_VALUE_GET_OBJ(arr); - if (expand_fast_array(ctx, p, len) < 0) - goto exception; - p->u.array.count = len; - i = 0; pval = p->u.array.u.values; if (js_get_fast_array(ctx, obj, &arrp, &count32) && count32 == len) { @@ -43163,16 +43135,10 @@ static JSValue js_array_toSorted(JSContext *ctx, JSValueConst this_val, *pval = js_dup(arrp[i]); } else { for (; i < len; i++, pval++) { - if (-1 == JS_TryGetPropertyInt64(ctx, obj, i, pval)) { - for (; i < len; i++, pval++) - *pval = JS_UNDEFINED; + if (-1 == JS_TryGetPropertyInt64(ctx, obj, i, pval)) goto exception; - } } } - - if (JS_SetProperty(ctx, arr, JS_ATOM_length, js_int64(len)) < 0) - goto exception; } ret = js_array_sort(ctx, arr, argc, argv); diff --git a/tests/test_array_to_gc.js b/tests/test_array_to_gc.js new file mode 100644 index 000000000..6351f2901 --- /dev/null +++ b/tests/test_array_to_gc.js @@ -0,0 +1,69 @@ +import * as std from "qjs:std"; +import { assert } from "./assert.js"; + +// The non-mutating array methods (with, toReversed, toSpliced, toSorted) +// allocate a fast array and then populate it by pulling values from the +// source via JS_TryGetPropertyInt64. When the source is an array-like with +// a property getter that triggers GC, the GC must see fully initialized +// slots — otherwise it would walk uninitialized JSValues in the newly +// allocated array. Run under ASAN to catch regressions. + +function makeArrayLike(length, getterIndex) { + const obj = { length }; + Object.defineProperty(obj, getterIndex, { + configurable: true, + get() { + std.gc(); + return 1; + }, + }); + return obj; +} + +// with +{ + const obj = makeArrayLike(256, 0); + Object.defineProperty(obj, 1, { + value: 2, + writable: true, + configurable: true, + }); + const res = Array.prototype.with.call(obj, 1, 9); + assert(res.length, 256); + assert(res[0], 1); + assert(res[1], 9); + assert(res[2], undefined); + assert(res[255], undefined); +} + +// toReversed +{ + const obj = makeArrayLike(256, 255); + const res = Array.prototype.toReversed.call(obj); + assert(res.length, 256); + assert(res[0], 1); + assert(res[1], undefined); + assert(res[255], undefined); +} + +// toSpliced +{ + const obj = makeArrayLike(256, 0); + const res = Array.prototype.toSpliced.call(obj, 1, 0, 7); + assert(res.length, 257); + assert(res[0], 1); + assert(res[1], 7); + assert(res[2], undefined); + assert(res[256], undefined); +} + +// toSorted +{ + const obj = makeArrayLike(256, 0); + const res = Array.prototype.toSorted.call(obj); + assert(res.length, 256); + // Sort places `undefined` at the end, so the single defined value (1) is first. + assert(res[0], 1); + assert(res[1], undefined); + assert(res[255], undefined); +}