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-");