From 5335347e80a749518cb59d219ac8894aa711db8b Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 25 Sep 2026 00:15:19 -0500 Subject: [PATCH] feat(codemode): add WeakMap and WeakSet (#51257) --- packages/codemode/interpreter-support.md | 5 + packages/codemode/src/interpreter/globals.ts | 4 +- .../codemode/src/interpreter/intrinsics.ts | 4 + packages/codemode/src/interpreter/objects.ts | 17 +++ packages/codemode/src/stdlib/collections.ts | 133 ++++++++++++++++++ packages/codemode/src/stdlib/web.ts | 6 +- packages/codemode/test/parity.test.ts | 48 +++++++ packages/codemode/test/test262/README.md | 2 +- packages/codemode/test/test262/manifest.json | 4 +- packages/codemode/test/test262/skipped.txt | 6 + 10 files changed, 223 insertions(+), 6 deletions(-) diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 7b3e5b23c58..95fa44f663e 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -526,6 +526,11 @@ reject }` object. - [x] Map and Set values serialize to `{}` at host/JSON boundaries. - [x] Set composition and relation methods: `union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, and `isDisjointFrom`, including supported Set-like operands. +- [x] `WeakMap` (`get`, `set`, `has`, `delete`, `getOrInsert`, `getOrInsertComputed`) and `WeakSet` (`add`, `has`, + `delete`), constructed from iterables. Keys must be program objects: a primitive or tool reference throws + `Invalid value used as weak map key`, while `has`/`delete`/`get` with one answer `false`/`undefined`. Entries are + held by a host weak collection, so nothing is retained past the key's own lifetime. As in JS they have no `size`, + iteration, or `clear`, `structuredClone` rejects them, and they serialize to `{}` at host boundaries. ## URL and URI helpers diff --git a/packages/codemode/src/interpreter/globals.ts b/packages/codemode/src/interpreter/globals.ts index acfbad43acd..f56ed2122f5 100644 --- a/packages/codemode/src/interpreter/globals.ts +++ b/packages/codemode/src/interpreter/globals.ts @@ -2,7 +2,7 @@ import { Effect } from "effect" import { Arr, Callable, coerceToInteger, coerceToString, get, Obj, type Value } from "./objects.js" import { arrayGlobal } from "../stdlib/array.js" import { textDecoderGlobal, textEncoderGlobal, uint8ArrayGlobal } from "../stdlib/bytes.js" -import { mapGlobal, setGlobal } from "../stdlib/collections.js" +import { mapGlobal, setGlobal, weakMapGlobal, weakSetGlobal } from "../stdlib/collections.js" import { consoleGlobal } from "../stdlib/console.js" import { dateGlobal } from "../stdlib/date.js" import { jsonGlobal } from "../stdlib/json.js" @@ -111,6 +111,8 @@ const table: Record = { RegExp: (ctx) => regexpGlobal(ctx), Map: (ctx) => mapGlobal(ctx), Set: (ctx) => setGlobal(ctx), + WeakMap: (ctx) => weakMapGlobal(ctx), + WeakSet: (ctx) => weakSetGlobal(ctx), URL: (ctx) => urlGlobal(ctx), URLSearchParams: (ctx) => urlSearchParamsGlobal(ctx), Headers: (ctx) => headersGlobal(ctx), diff --git a/packages/codemode/src/interpreter/intrinsics.ts b/packages/codemode/src/interpreter/intrinsics.ts index bb1ce831ab9..2c48aaeb469 100644 --- a/packages/codemode/src/interpreter/intrinsics.ts +++ b/packages/codemode/src/interpreter/intrinsics.ts @@ -27,6 +27,8 @@ const builtins = [ "RegExp", "Map", "Set", + "WeakMap", + "WeakSet", "URL", "URLSearchParams", "Headers", @@ -88,6 +90,8 @@ export const createBuiltins = (): Builtins => { RegExp: plain(), Map: plain(), Set: plain(), + WeakMap: plain(), + WeakSet: plain(), URL: plain(), URLSearchParams: plain(), Headers: plain(), diff --git a/packages/codemode/src/interpreter/objects.ts b/packages/codemode/src/interpreter/objects.ts index 8b9c29466c6..b2b8886ab13 100644 --- a/packages/codemode/src/interpreter/objects.ts +++ b/packages/codemode/src/interpreter/objects.ts @@ -356,6 +356,23 @@ export class SetObj extends Wrapper { } } +/** Keys are program objects, so a host WeakMap gives the same lifetime rule as JavaScript without any bookkeeping. */ +export class WeakMapObj extends Wrapper { + override readonly tag = "WeakMap" + readonly map = new WeakMap() + override inspect() { + return "WeakMap { }" + } +} + +export class WeakSetObj extends Wrapper { + override readonly tag = "WeakSet" + readonly set = new WeakSet() + override inspect() { + return "WeakSet { }" + } +} + export class URLSearchParamsObj extends Wrapper { override readonly tag = "URLSearchParams" constructor( diff --git a/packages/codemode/src/stdlib/collections.ts b/packages/codemode/src/stdlib/collections.ts index 5defb53d72b..7fa2da0fc3c 100644 --- a/packages/codemode/src/stdlib/collections.ts +++ b/packages/codemode/src/stdlib/collections.ts @@ -16,6 +16,8 @@ import { PromiseObj, SetObj, type Value, + WeakMapObj, + WeakSetObj, } from "../interpreter/objects.js" import { describeValue, isOpaque } from "../interpreter/references.js" import { @@ -402,3 +404,134 @@ export const setGlobal = (ctx: Interpreter) => { define(proto, IteratorSymbol, get(proto, "values"), hidden) return set } + +// CanBeHeldWeakly: only program objects; tool references are rebuilt on every access, so they could never be found again. +const weakKey = (value: Value, label: string) => { + if (value instanceof Obj) return value + throw typeError(`Invalid value used ${label}: ${describeValue(value)} cannot be held weakly.`) +} + +export const weakMapGlobal = (ctx: Interpreter) => { + const builtins = ctx.builtins + const proto = builtins.WeakMap + const weakMap = constructor(builtins, proto, { + name: "WeakMap", + call: requiresNew("WeakMap"), + construct: (args, newTarget) => { + const target = new WeakMapObj(prototypeFrom(newTarget, proto)) + if (args[0] === undefined || args[0] === null) return Effect.succeed(target) + return Effect.gen(function* () { + const cursor = yield* ctx.iterate(args[0]!) + if (cursor === undefined) { + throw typeError( + `new WeakMap(...) expects an iterable of [key, value] pairs, received ${describeValue(args[0])}.`, + ) + } + while (true) { + const step = yield* cursor.next + if (step.done) return target + yield* preserveConsumerError( + cursor.close, + Effect.sync(() => { + if (!(step.value instanceof Obj)) { + throw typeError("new WeakMap(...) expects [key, value] pairs as entry objects.") + } + target.map.set(weakKey(getOwn(step.value, 0), "as weak map key"), getOwn(step.value, 1)) + }), + ) + } + }) + }, + }) + const self = (thisValue: Value, name: string) => receiver(WeakMapObj, thisValue, `WeakMap.prototype.${name}`).map + // Lookups pass any key through: the host collection answers false for a non-object, as the spec requires. + const key = (value: Value) => weakKey(value, "as weak map key") + methods(builtins, proto, [ + [ + "get", + 1, + (thisValue, args) => { + const target = self(thisValue, "get") + return args[0] instanceof Obj ? target.get(args[0]) : undefined + }, + ], + ["has", 1, (thisValue, args) => self(thisValue, "has").has(args[0] as Obj)], + ["delete", 1, (thisValue, args) => self(thisValue, "delete").delete(args[0] as Obj)], + [ + "set", + 2, + (thisValue, args) => { + self(thisValue, "set").set(key(args[0]), args[1]) + return thisValue + }, + ], + [ + "getOrInsert", + 2, + (thisValue, args) => { + const target = self(thisValue, "getOrInsert") + const k = key(args[0]) + if (!target.has(k)) target.set(k, args[1]) + return target.get(k) + }, + ], + [ + "getOrInsertComputed", + 2, + (thisValue, args) => { + const target = self(thisValue, "getOrInsertComputed") + const k = key(args[0]) + const apply = applyCollectionCallback(ctx, args[1], "WeakMap.getOrInsertComputed") + if (target.has(k)) return target.get(k) + return Effect.map(apply([k]), (value) => { + target.set(k, value) + return value + }) + }, + ], + ]) + return weakMap +} + +export const weakSetGlobal = (ctx: Interpreter) => { + const builtins = ctx.builtins + const proto = builtins.WeakSet + const weakSet = constructor(builtins, proto, { + name: "WeakSet", + call: requiresNew("WeakSet"), + construct: (args, newTarget) => { + const target = new WeakSetObj(prototypeFrom(newTarget, proto)) + if (args[0] === undefined || args[0] === null) return Effect.succeed(target) + return Effect.gen(function* () { + const cursor = yield* ctx.iterate(args[0]!) + if (cursor === undefined) { + throw typeError(`new WeakSet(...) expects a synchronous iterable, received ${describeValue(args[0])}.`) + } + while (true) { + const step = yield* cursor.next + if (step.done) return target + yield* preserveConsumerError( + cursor.close, + Effect.sync(() => { + target.set.add(weakKey(step.value, "in weak set")) + }), + ) + } + }) + }, + }) + const self = (thisValue: Value, name: string) => receiver(WeakSetObj, thisValue, `WeakSet.prototype.${name}`).set + methods(builtins, proto, [ + ["has", 1, (thisValue, args) => self(thisValue, "has").has(args[0] as Obj)], + ["delete", 1, (thisValue, args) => self(thisValue, "delete").delete(args[0] as Obj)], + [ + "add", + 1, + (thisValue, args) => { + self(thisValue, "add").add(weakKey(args[0], "in weak set")) + return thisValue + }, + ], + ]) + return weakSet +} diff --git a/packages/codemode/src/stdlib/web.ts b/packages/codemode/src/stdlib/web.ts index 5d000057c17..a5a78bfaf50 100644 --- a/packages/codemode/src/stdlib/web.ts +++ b/packages/codemode/src/stdlib/web.ts @@ -16,6 +16,8 @@ import { Obj, RegExpObj, SetObj, + WeakMapObj, + WeakSetObj, coerceToString, type Value, } from "../interpreter/objects.js" @@ -84,7 +86,9 @@ export const structuredCloneGlobal = (ctx: Interpreter) => if (hasOwn(value, "cause")) define(copy, "cause", clone(getOwn(value, "cause")), hidden) return copy } - if (isRuntimeReference(value)) throw typeError(`DataCloneError: ${describeValue(value)} could not be cloned.`) + if (isRuntimeReference(value) || value instanceof WeakMapObj || value instanceof WeakSetObj) { + throw typeError(`DataCloneError: ${describeValue(value)} could not be cloned.`) + } const copy = remember( value instanceof Arr ? new Arr(builtins.Array, new Array(value.items.length)) : new Obj(builtins.Object), ) diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index 050e9b57d0b..c70a855bd63 100644 --- a/packages/codemode/test/parity.test.ts +++ b/packages/codemode/test/parity.test.ts @@ -1936,3 +1936,51 @@ describe("String and Number method arguments convert through ToPrimitive", () => ) }) }) + +describe("WeakMap and WeakSet", () => { + test("hold program objects by identity and answer like JS for non-object keys", async () => { + expect( + await value(` + const k = {} + const f = () => 1 + const wm = new WeakMap([[k, 1]]) + const ws = new WeakSet([k]) + return [ + wm.set(f, "fn") === wm, wm.get(k), wm.get(f), wm.has({}), wm.get(1), wm.has(1), wm.delete("s"), + wm.getOrInsert(k, 9), wm.getOrInsertComputed({}, (key) => typeof key), + ws.add(f) === ws, ws.has(k), ws.has(f), ws.has(1), ws.delete(k), ws.has(k), + String(wm), wm.size, "clear" in wm, Symbol.iterator in ws, JSON.stringify(wm), + ] + `), + ).toEqual([ + true, + 1, + "fn", + false, + null, + false, + false, + 1, + "object", + true, + true, + true, + false, + true, + false, + "[object WeakMap]", + null, + false, + false, + "{}", + ]) + }) + + test("reject primitive keys, plain calls, bad receivers, and cloning", async () => { + expect((await error(`new WeakMap().set(1, 1)`)).message).toContain("Invalid value used as weak map key") + expect((await error(`new WeakSet([1])`)).message).toContain("Invalid value used in weak set") + expect((await error(`WeakMap()`)).message).toContain("new") + expect((await error(`WeakMap.prototype.get.call(new Map(), {})`)).message).toContain("incompatible receiver") + expect((await error(`structuredClone(new WeakSet())`)).message).toContain("DataCloneError") + }) +}) diff --git a/packages/codemode/test/test262/README.md b/packages/codemode/test/test262/README.md index fd9ce311d7c..4ed7ff511e4 100644 --- a/packages/codemode/test/test262/README.md +++ b/packages/codemode/test/test262/README.md @@ -27,7 +27,7 @@ Without them the runner registers no tests, so CI is unaffected. Licensed under manifest marks unsupported, or when its code matches one of the manifest's `boundaries` patterns. The sync checks the checkout is at the pinned revision, so every machine runs the same files. Boundaries are intentional limits of the interpreter, not compatibility work: classes, prototype objects, property descriptors, -accessors, boxed primitives, typed arrays and buffers, weak collections, `Reflect` and `Proxy`, sloppy mode, `eval`, +accessors, boxed primitives, typed arrays and buffers, `WeakRef` and `FinalizationRegistry`, `Reflect` and `Proxy`, sloppy mode, `eval`, `Symbol()`, and the `$262` host API. If one of those decisions changes, delete its entry and re-sync; the tests are upstream, not lost. diff --git a/packages/codemode/test/test262/manifest.json b/packages/codemode/test/test262/manifest.json index 6af9fcb0d30..08db8ddde0b 100644 --- a/packages/codemode/test/test262/manifest.json +++ b/packages/codemode/test/test262/manifest.json @@ -9,7 +9,7 @@ "property descriptors": "Object\\.(defineProperty|defineProperties|getOwnPropertyDescriptors?|getOwnPropertyNames|setPrototypeOf)\\b", "boxed primitives": "\\b(new\\s+(String|Number|Boolean)\\b|Object\\s*\\(\\s*(true|false|-?\\d|['\"]))", "typed arrays and buffers": "\\b(ArrayBuffer|SharedArrayBuffer|DataView|Int8Array|Uint8ClampedArray|Int16Array|Uint16Array|Int32Array|Uint32Array|Float16Array|Float32Array|Float64Array|BigInt64Array|BigUint64Array)\\b", - "weak collections": "\\b(WeakMap|WeakSet|WeakRef|FinalizationRegistry)\\b", + "weak collections": "\\b(WeakRef|FinalizationRegistry)\\b", "Reflect and Proxy": "\\b(Reflect|Proxy)\\b", "sloppy mode": "\\bwith\\s*\\(", "eval": "\\b(eval|Function)\\b", @@ -74,8 +74,6 @@ "Uint16Array", "Uint32Array", "uint8array-base64", - "WeakMap", - "WeakSet", "WeakRef", "FinalizationRegistry", "Intl-enumeration", diff --git a/packages/codemode/test/test262/skipped.txt b/packages/codemode/test/test262/skipped.txt index b16ed98c79c..a4b92d47eab 100644 --- a/packages/codemode/test/test262/skipped.txt +++ b/packages/codemode/test/test262/skipped.txt @@ -1695,3 +1695,9 @@ language/types/object/S8.6.2_A5_T3.js # TypeError: Cannot read properties of un language/types/object/S8.6.2_A5_T4.js # TypeError: Cannot read properties of undefined (reading '…'). language/types/reference/S8.7.1_A1.js # TypeError: Cannot read properties of undefined (reading '…'). language/types/reference/S8.7.2_A3.js # TypeError: Cannot read properties of undefined (reading '…'). +built-ins/WeakMap/iterable-with-object-keys.js # Called WeakMap#set for each object Expected SameValue(«0», «2») to be true +built-ins/WeakMap/iterator-close-after-set-failure.js # Expected a Test262Error but got a TypeError +built-ins/WeakMap/set-not-callable-throws.js # Expected a TypeError to be thrown but no exception was thrown at all +built-ins/WeakSet/add-not-callable-throws.js # Expected a TypeError to be thrown but no exception was thrown at all +built-ins/WeakSet/iterable-with-object-values.js # Called WeakSet#add for each object Expected SameValue(«0», «2») to be true +built-ins/WeakSet/iterator-close-after-add-failure.js # Expected a Test262Error but got a TypeError