diff --git a/docs/content/docs/experimental.mdx b/docs/content/docs/experimental.mdx index 68026072b..64481634f 100644 --- a/docs/content/docs/experimental.mdx +++ b/docs/content/docs/experimental.mdx @@ -159,7 +159,7 @@ worker: square: 4 lines, 3 shapes
new InternTable<K, V>(name)
-
Opens the table called name. Within one thread every construction with the same name refers to the same table. In scriptc builds every thread of the process shares it; under Node.js each thread has its own. Keys are strings, numbers, or arrays of numbers or of strings. Strings and numbers compare like Map keys; arrays compare by their elements in the same way.
+
Opens the table called name. Within one thread every construction with the same name refers to the same table. In scriptc builds every thread of the process shares it; under Node.js each thread has its own. Keys are strings, numbers, or arrays of numbers or of strings. Strings and numbers compare like Map keys; arrays compare by their elements in the same way and can have at most 65536 elements.
table.get(key), table.has(key), table.size
Read the table.
table.getOrInsert(key, value)
diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index c1c9d1591..1b7fd93df 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -8650,7 +8650,8 @@ export class LlEmitter { /** InternTable's runtime calls (scr_itable.c). Table and entry handles * are f64 values carrying pointer bits; entry values are 64-bit slots * (double bits, a bool, or a pointer to an immortal value) that the - * table neither retains nor releases. */ + * table neither retains nor releases. Lookups and inserts of array keys + * throw a TypeError for keys longer than a table accepts. */ private emitInternTableIntrinsic(e: ExprOf<"intrinsic">): LlValue { const B = this.B; // The runtime copies keys and stores published values without counts, @@ -8692,6 +8693,8 @@ export class LlEmitter { const call = keyCall("find"); const entry = B.tmp(); B.line(`${entry} = call ptr ${call})`); + // Array keys longer than a table accepts throw a TypeError. + if (e.args[1]!.type.kind === "array") this.emitPendingCheck(); return toHandle(entry); } case "itable.insert": { @@ -8711,6 +8714,7 @@ export class LlEmitter { const call = keyCall("insert"); const entry = B.tmp(); B.line(`${entry} = call ptr ${call}, i64 ${slot})`); + if (e.args[1]!.type.kind === "array") this.emitPendingCheck(); return toHandle(entry); } case "itable.value": { diff --git a/packages/compiler/src/backend/may-throw.ts b/packages/compiler/src/backend/may-throw.ts index 9d692a15d..1c65da36f 100644 --- a/packages/compiler/src/backend/may-throw.ts +++ b/packages/compiler/src/backend/may-throw.ts @@ -196,6 +196,12 @@ export function computeMayThrow( if (rec.name === "module.tdzCheck") f.throws = true; // publish() throws on an unpublishable dyn value. if (rec.name === "threads.publish") f.throws = true; + // An InternTable array key longer than a table accepts. + if ( + (rec.name === "itable.find" || rec.name === "itable.insert") && + rec.args[1]?.type.kind === "array" + ) + f.throws = true; break; case "yieldExpr": // A consumer .throw() surfaces at the yield (and .return()'s diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index b45a36ed7..19b83560e 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -3482,9 +3482,11 @@ export type IrExpr = * key's entry, storing the already published value when the key has * none; itable.value (f64 entry) → V, the entry's immortal value; * itable.size (f64 table) → f64. K is string, f64, or an array of - * f64 or of strings (compared by elements). Handles carry - * raw pointer bits and only reach toBool and these intrinsics. - * None of them throws. */ + * f64 or of strings (compared by elements); find and insert throw + * a TypeError for array keys of more than 65536 elements or whose + * encoding exceeds 2^31 - 1 bytes; nothing else throws. Handles + * carry raw pointer bits and only reach toBool and these + * intrinsics. */ | "itable.open" | "itable.find" | "itable.insert" diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5726-intern-table-key-limits.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5726-intern-table-key-limits.ts.json new file mode 100644 index 000000000..5db8a9473 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5726-intern-table-key-limits.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5726-intern-table-key-limits.ts" + ], + "diags": [] +} diff --git a/packages/experimental/README.md b/packages/experimental/README.md index 5dc9775df..413deeff2 100644 --- a/packages/experimental/README.md +++ b/packages/experimental/README.md @@ -55,7 +55,7 @@ function unionOf(members: readonly Type[]): Type { ``` - `new InternTable(name)` opens the table called `name`. Within one thread, every construction with the same name refers to the same table. In scriptc builds the table is shared by every thread of the process; under Node.js each thread has its own. -- Keys are strings, numbers, or arrays of numbers or of strings. Strings and numbers compare like `Map` keys: strings by content, numbers by SameValueZero. Arrays compare by their elements in the same way, so a freshly built `[a.id, b.id]` finds the value stored under an equal array; holes are equal to each other and to no number or string. +- Keys are strings, numbers, or arrays of numbers or of strings. Strings and numbers compare like `Map` keys: strings by content, numbers by SameValueZero. Arrays compare by their elements in the same way, so a freshly built `[a.id, b.id]` finds the value stored under an equal array; holes are equal to each other and to no number or string. An array key can have at most 65536 elements; longer ones make every method throw a `TypeError`. - `get(key)`, `has(key)` and `size` read the table. - `getOrInsert(key, value)` returns the value stored under `key`. When there is none, it publishes `value`, stores it and returns it. - `getOrInsertComputed(key, callback)` returns the value stored under `key`. When there is none, it calls `callback(key)`, publishes the result and stores it, unless a value was stored under `key` meanwhile (by the callback, or in scriptc builds by another thread), and returns the stored value. diff --git a/packages/experimental/index.d.ts b/packages/experimental/index.d.ts index e5079112a..d831cbff9 100644 --- a/packages/experimental/index.d.ts +++ b/packages/experimental/index.d.ts @@ -35,7 +35,8 @@ export declare class InternTable< /** The value stored under `key`, or undefined. String and number keys * compare like Map keys: strings by content, numbers by SameValueZero. * Array keys compare by their elements in the same way; a hole and an - * undefined element are equal. */ + * undefined element are equal. Every method throws a TypeError for an + * array key of more than 65536 elements. */ get(key: K): V | undefined; /** Whether `key` has a value. */ has(key: K): boolean; diff --git a/packages/experimental/index.js b/packages/experimental/index.js index 6c4af68ba..8a1b28c26 100644 --- a/packages/experimental/index.js +++ b/packages/experimental/index.js @@ -24,9 +24,12 @@ const tables = new Map(); /** The Map key of a table key: strings and numbers as they are, arrays as a * string that encodes each element (numbers by SameValueZero, strings by - * content, holes and undefined elements alike). */ + * content, holes and undefined elements alike). Arrays of more than 65536 + * elements are refused, as in scriptc builds. */ function keyOf(key) { if (!Array.isArray(key)) return key; + if (key.length > 65536) + throw new TypeError("InternTable array keys can have at most 65536 elements"); let text = ""; for (let i = 0; i < key.length; i++) { const element = key[i]; diff --git a/packages/runtime/src/scr_itable.c b/packages/runtime/src/scr_itable.c index dcdcf481c..08db948b5 100644 --- a/packages/runtime/src/scr_itable.c +++ b/packages/runtime/src/scr_itable.c @@ -10,7 +10,8 @@ * tag of hash bits 50..57 in bits 48..55, so a probe reads an entry only * when the tags match. An array key is stored as one byte string that * encodes its elements (scr_itable_encode), so array keys compare by - * their elements. + * their elements; keys longer or larger than a table accepts are refused + * with a TypeError before anything is encoded. * * Readers load the segment's index pointer, then slots with acquire order, * and never write. Writers hold the segment's lock: they probe again, grow @@ -44,6 +45,12 @@ #define SCR_ITABLE_FIRST_SLOTS 8u #define SCR_ITABLE_FIRST_CHUNK ((size_t)512) #define SCR_ITABLE_MAX_CHUNK ((size_t)64 << 10) +/* Keys whose bytes (a string's, or an array key's encoding) exceed this + * are refused, which keeps every entry size representable on 32-bit + * targets. Array keys of more than SCR_ITABLE_MAX_KEY_ELEMENTS elements are + * refused too; the Node.js implementation applies the same limit. */ +#define SCR_ITABLE_MAX_KEY_BYTES ((size_t)SCR_STR_MAX_CAP) +#define SCR_ITABLE_MAX_KEY_ELEMENTS 65536u /* String slots: 0 is empty; otherwise the entry pointer with the tag in * bits 48..55, which no supported target uses for user-space addresses @@ -314,7 +321,10 @@ const ScrITableEntry *scr_itable_find_f64(ScrITable *t, double key) { /* ── inserts (the segment lock held) ─────────────────────────────────── */ static ScrITableIndex *scr_itable_index_new(uint64_t slots, bool numbers) { - size_t words = slots * (numbers ? 2 : 1) * sizeof(uint64_t); + /* Bytes per slot: the words and, for numbers, the tag byte. */ + size_t per = (numbers ? 2 : 1) * sizeof(uint64_t) + (numbers ? 1 : 0); + if (slots > (SIZE_MAX - sizeof(ScrITableIndex)) / per) scr_itable_oom(); + size_t words = (size_t)slots * (numbers ? 2 : 1) * sizeof(uint64_t); ScrITableIndex *index = calloc(1, sizeof(ScrITableIndex) + words + (numbers ? slots : 0)); if (!index) scr_itable_oom(); index->mask = slots - 1; @@ -394,7 +404,7 @@ static void *scr_itable_alloc(ScrITableSegment *seg, size_t size) { * unless the key has an entry. */ static const ScrITableEntry *scr_itable_insert_bytes(ScrITable *t, uint64_t hash, const char *bytes, size_t len, uint64_t value) { - if (len > UINT32_MAX) scr_trap("scriptc: InternTable key is too long\n"); + if (len > SCR_ITABLE_MAX_KEY_BYTES) scr_trap("scriptc: InternTable key is too long\n"); ScrITableSegment *seg = scr_itable_segment(t, hash); scr_itable_lock(&seg->lock); const ScrITableStrEntry *found = @@ -444,8 +454,16 @@ const ScrITableEntry *scr_itable_insert_f64(ScrITable *t, double key, uint64_t v /* ── array keys ──────────────────────────────────────────────────────── */ -/* The state and slot of element `i` of `a` (i < a->len). */ -static ScrArrState scr_itable_element(const ScrArr *a, size_t i, uint64_t *slot) { +/* A walk over the elements of an array in index order. Sparse entries + * are sorted by index (scr_array.h), so one pass visits each once. */ +typedef struct { + const ScrArr *a; + size_t sparse; +} ScrITableElements; + +/* The state and slot of element `i` (i < len, increasing across calls). */ +static ScrArrState scr_itable_element(ScrITableElements *w, size_t i, uint64_t *slot) { + const ScrArr *a = w->a; if (i < a->filled) { *slot = a->data[i]; return SCR_ARR_VALUE; @@ -454,28 +472,65 @@ static ScrArrState scr_itable_element(const ScrArr *a, size_t i, uint64_t *slot) *slot = a->data[i]; return (ScrArrState)a->present[i]; } - if (a->ext) - for (size_t j = 0; j < a->ext->sparse_len; j++) - if (a->ext->sparse[j].index == i) { - *slot = a->ext->sparse[j].slot; - return (ScrArrState)a->ext->sparse[j].state; - } + if (a->ext) { + while (w->sparse < a->ext->sparse_len && a->ext->sparse[w->sparse].index < i) w->sparse++; + if (w->sparse < a->ext->sparse_len && a->ext->sparse[w->sparse].index == i) { + *slot = a->ext->sparse[w->sparse].slot; + return (ScrArrState)a->ext->sparse[w->sparse].state; + } + } return SCR_ARR_HOLE; } /* An array key as bytes: per number element its normalized bits, per * string element a 32-bit length and the bytes; a hole or undefined * element is the bits of a NaN that normalization never produces, or the - * length 0xffffffff. Answers the encoded length; writes at most `cap` - * bytes into `out`. */ + * length 0xffffffff. */ #define SCR_ITABLE_ABSENT_F64 UINT64_C(0x7ff4000000000002) #define SCR_ITABLE_ABSENT_STR UINT32_C(0xffffffff) -static size_t scr_itable_encode(const ScrArr *key, int kind, char *out, size_t cap) { +static void scr_itable_refuse(const char *message) { + scr_throw_error_msg(SCR_ERR_TYPE, message, strlen(message)); +} + +/* The encoded size of `key`, or false (with a pending TypeError) when the + * key is longer or larger than a table accepts. Every sum stays at most + * SCR_ITABLE_MAX_KEY_BYTES, so none can wrap. */ +static bool scr_itable_encoded_size(const ScrArr *key, int kind, size_t *out) { + if (key->len > SCR_ITABLE_MAX_KEY_ELEMENTS) { + scr_itable_refuse("InternTable array keys can have at most 65536 elements"); + return false; + } size_t n = 0; + ScrITableElements walk = {key, 0}; + for (size_t i = 0; i < key->len; i++) { + size_t add = kind == SCR_ITABLE_KEY_F64S ? 8 : 4; + if (kind != SCR_ITABLE_KEY_F64S) { + uint64_t slot = 0; + if (scr_itable_element(&walk, i, &slot) == SCR_ARR_VALUE) { + const ScrStr *s = (const ScrStr *)(uintptr_t)slot; + if (s->len > SCR_ITABLE_MAX_KEY_BYTES - add) add = SCR_ITABLE_MAX_KEY_BYTES + 1; + else add += s->len; + } + } + if (add > SCR_ITABLE_MAX_KEY_BYTES - n) { + scr_itable_refuse("InternTable array keys can encode at most 2147483647 bytes"); + return false; + } + n += add; + } + *out = n; + return true; +} + +/* Writes the encoding of `key` into `out`, which holds exactly the size + * scr_itable_encoded_size answered. */ +static void scr_itable_encode(const ScrArr *key, int kind, char *out) { + size_t n = 0; + ScrITableElements walk = {key, 0}; for (size_t i = 0; i < key->len; i++) { uint64_t slot = 0; - bool present = scr_itable_element(key, i, &slot) == SCR_ARR_VALUE; + bool present = scr_itable_element(&walk, i, &slot) == SCR_ARR_VALUE; if (kind == SCR_ITABLE_KEY_F64S) { uint64_t bits = SCR_ITABLE_ABSENT_F64; if (present) { @@ -483,21 +538,19 @@ static size_t scr_itable_encode(const ScrArr *key, int kind, char *out, size_t c memcpy(&d, &slot, sizeof d); bits = scr_itable_f64_bits(d); } - if (n + 8 <= cap) memcpy(out + n, &bits, 8); + memcpy(out + n, &bits, 8); n += 8; } else { const ScrStr *s = present ? (const ScrStr *)(uintptr_t)slot : NULL; - if (s && s->len >= SCR_ITABLE_ABSENT_STR) scr_trap("scriptc: InternTable key is too long\n"); uint32_t len = s ? (uint32_t)s->len : SCR_ITABLE_ABSENT_STR; - if (n + 4 <= cap) memcpy(out + n, &len, 4); + memcpy(out + n, &len, 4); n += 4; if (s) { - if (n + s->len <= cap) memcpy(out + n, s->data, s->len); + memcpy(out + n, s->data, s->len); n += s->len; } } } - return n; } static inline uint64_t scr_itable_bytes_hash(const char *bytes, size_t len) { @@ -505,14 +558,16 @@ static inline uint64_t scr_itable_bytes_hash(const char *bytes, size_t len) { } /* Encodes `key` into `stack` when it fits, else into a heap buffer the - * caller frees. */ + * caller frees; NULL (with a pending TypeError) for a refused key. */ static char *scr_itable_encoded(const ScrITable *t, const ScrArr *key, char *stack, size_t cap, size_t *len) { - *len = scr_itable_encode(key, t->key_kind, stack, cap); - if (*len <= cap) return stack; - char *heap = malloc(*len); - if (!heap) scr_itable_oom(); - scr_itable_encode(key, t->key_kind, heap, *len); - return heap; + if (!scr_itable_encoded_size(key, t->key_kind, len)) return NULL; + char *bytes = stack; + if (*len > cap) { + bytes = malloc(*len); + if (!bytes) scr_itable_oom(); + } + scr_itable_encode(key, t->key_kind, bytes); + return bytes; } #define SCR_ITABLE_STACK_KEY 256 @@ -522,6 +577,7 @@ const ScrITableEntry *scr_itable_find_arr(ScrITable *t, const ScrArr *key) { char stack[SCR_ITABLE_STACK_KEY]; size_t len; char *bytes = scr_itable_encoded(t, key, stack, sizeof stack, &len); + if (!bytes) return NULL; uint64_t hash = scr_itable_bytes_hash(bytes, len); ScrITableSegment *seg = scr_itable_segment(t, hash); const ScrITableEntry *found = (const ScrITableEntry *)scr_itable_probe_bytes( @@ -535,6 +591,7 @@ const ScrITableEntry *scr_itable_insert_arr(ScrITable *t, const ScrArr *key, uin char stack[SCR_ITABLE_STACK_KEY]; size_t len; char *bytes = scr_itable_encoded(t, key, stack, sizeof stack, &len); + if (!bytes) return NULL; const ScrITableEntry *entry = scr_itable_insert_bytes(t, scr_itable_bytes_hash(bytes, len), bytes, len, value); if (bytes != stack) free(bytes); return entry; diff --git a/packages/runtime/src/scr_itable.h b/packages/runtime/src/scr_itable.h index 9ea3258dd..1c198182f 100644 --- a/packages/runtime/src/scr_itable.h +++ b/packages/runtime/src/scr_itable.h @@ -38,7 +38,9 @@ typedef struct ScrITableEntry { /* The table called `name`, created on first use. Traps when the name is * already open with another key kind. Never returns NULL. */ ScrITable *scr_itable_open(const ScrStr *name, int key_kind); -/* The entry of `key`, or NULL. */ +/* The entry of `key`, or NULL. The array variants refuse keys of more + * than 65536 elements, or whose encoding exceeds 2^31 - 1 bytes, with a + * pending TypeError (and answer NULL). */ const ScrITableEntry *scr_itable_find_str(ScrITable *t, const ScrStr *key); const ScrITableEntry *scr_itable_find_f64(ScrITable *t, double key); const ScrITableEntry *scr_itable_find_arr(ScrITable *t, const ScrArr *key); diff --git a/packages/runtime/src/scr_itable.test.c b/packages/runtime/src/scr_itable.test.c index 7781d4d01..0f218806b 100644 --- a/packages/runtime/src/scr_itable.test.c +++ b/packages/runtime/src/scr_itable.test.c @@ -8,12 +8,20 @@ #include #include -/* The table unit needs only scr_trap from the rest of the runtime. */ +/* The table unit needs only scr_trap and scr_throw_error_msg from the + * rest of the runtime; refusals are recorded here. */ _Noreturn void scr_trap(const char *msg) { fputs(msg, stderr); abort(); } +static char refused[128]; +void scr_throw_error_msg(int kind, const char *msg, size_t len) { + assert(kind == SCR_ERR_TYPE && len < sizeof refused); + memcpy(refused, msg, len); + refused[len] = 0; +} + enum { THREADS = 8, KEYS = 20000, ROUNDS = 3 }; /* A stand-in for a published value: written by its creator before the @@ -161,6 +169,19 @@ int main(void) { big->data = calloc(100, sizeof(uint64_t)); for (unsigned i = 0; i < 100; i++) memcpy(&big->data[i], &(double){i}, sizeof(double)); assert(scr_itable_insert_arr(lists, big, 6)->value == 6 && scr_itable_find_arr(lists, big)->value == 6); + /* Keys of more than 65536 elements are refused before anything is read, + * however long their sparse length. */ + ScrArr *longest = make_f64s(NULL, 0); + longest->len = 65536; + assert(scr_itable_insert_arr(lists, longest, 7)->value == 7 && !refused[0]); + ScrArr *huge = make_f64s(NULL, 0); + huge->len = (size_t)1 << 29; + assert(!scr_itable_find_arr(lists, huge) && !strcmp(refused, "InternTable array keys can have at most 65536 elements")); + refused[0] = 0; + longest->len = 65537; + assert(!scr_itable_insert_arr(lists, longest, 8) && refused[0]); + refused[0] = 0; + assert(scr_itable_size(lists) == 6); ScrITable *paths = scr_itable_open(paths_name, SCR_ITABLE_KEY_STRS); ScrStr *ab = make_str("ab"), *c = make_str("c"), *a = make_str("a"), *bc = make_str("bc"); ScrArr *p1 = make_strs((ScrStr *[]){ab, c}, 2), *p2 = make_strs((ScrStr *[]){a, bc}, 2); @@ -168,7 +189,17 @@ int main(void) { const ScrITableEntry *e1 = scr_itable_insert_arr(paths, p1, 7); assert(scr_itable_insert_arr(paths, p2, 8) != e1 && scr_itable_find_arr(paths, p3) == e1); assert(scr_itable_size(paths) == 2); - ScrArr *arrays[] = {one, same, shorter, holed, empty_list, big, p1, p2, p3}; + /* String keys whose encoding exceeds 2^31 - 1 bytes are refused. */ + ScrStr *wide = calloc(1, sizeof(ScrStr) + 1); + assert(wide); + wide->rc = 1; + wide->len = (size_t)0x7fffffff - 2; + ScrArr *heavy = make_strs((ScrStr *[]){wide, wide}, 2); + assert(!scr_itable_find_arr(paths, heavy) && + !strcmp(refused, "InternTable array keys can encode at most 2147483647 bytes")); + refused[0] = 0; + free(wide); + ScrArr *arrays[] = {one, same, shorter, holed, empty_list, big, longest, huge, p1, p2, p3, heavy}; for (unsigned i = 0; i < sizeof arrays / sizeof *arrays; i++) { free(arrays[i]->data); free(arrays[i]->present); diff --git a/tests/corpus/5726-intern-table-key-limits.ts b/tests/corpus/5726-intern-table-key-limits.ts new file mode 100644 index 000000000..cd87f1b3a --- /dev/null +++ b/tests/corpus/5726-intern-table-key-limits.ts @@ -0,0 +1,39 @@ +import { InternTable } from "@scriptc/experimental"; + +// InternTable (@scriptc/experimental) array keys can have at most 65536 +// elements: longer keys, including sparse arrays with a large length, make +// every method throw a TypeError and store nothing. Node.js prints the same. + +const lists = new InternTable("limits.lists"); +const names = new InternTable("limits.names"); + +function attempt(what: string, run: () => unknown): void { + try { + console.log(what, run()); + } catch (error) { + console.log(what, (error as Error).name, (error as Error).message); + } +} + +const longest = new Array(65536); +longest[0] = 1; +attempt("longest", () => lists.getOrInsert(longest, 1)); +attempt("longest again", () => lists.get(longest)); + +const longer = new Array(65537); +attempt("longer get", () => lists.get(longer)); +attempt("longer has", () => lists.has(longer)); +attempt("longer insert", () => lists.getOrInsert(longer, 2)); +attempt("longer computed", () => lists.getOrInsertComputed(longer, () => 3)); + +const sparse: number[] = [1, 2]; +sparse.length = 2 ** 29; +attempt("sparse get", () => lists.get(sparse)); +attempt("sparse insert", () => lists.getOrInsert(sparse, 4)); +attempt("sparse computed", () => lists.getOrInsertComputed(sparse, () => 5)); + +const words: string[] = ["a"]; +words.length = 2 ** 30; +attempt("words get", () => names.get(words)); +attempt("words insert", () => names.getOrInsert(words, 6)); +console.log(lists.size, names.size, lists.get([1]), names.get(["a"])); diff --git a/tests/harness/wasi-differential.test.ts b/tests/harness/wasi-differential.test.ts index 334124b4e..d2cd28f2f 100644 --- a/tests/harness/wasi-differential.test.ts +++ b/tests/harness/wasi-differential.test.ts @@ -289,6 +289,8 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => { // InternTable handles carry 32-bit pointers in f64 slots. "5720-intern-table-basics.ts", "5721-intern-table-values.ts", + // Array key sizes are checked in 32-bit size_t arithmetic. + "5726-intern-table-key-limits.ts", ])("the LLVM target runs %s byte-identically to Node", async (fixture) => { const entry = join(repoRoot, "tests/corpus", fixture); const outDir = await mkdtemp("/tmp/scriptc-wasi-");