feat(codemode): add WeakMap and WeakSet (#51257)

This commit is contained in:
Aiden Cline
2026-09-25 00:15:19 -05:00
committed by GitHub
parent 16b18dff13
commit 5335347e80
10 changed files with 223 additions and 6 deletions
+5
View File
@@ -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
+3 -1
View File
@@ -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<string, Factory> = {
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),
@@ -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(),
@@ -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<Obj, Value>()
override inspect() {
return "WeakMap { <items unknown> }"
}
}
export class WeakSetObj extends Wrapper {
override readonly tag = "WeakSet"
readonly set = new WeakSet<Obj>()
override inspect() {
return "WeakSet { <items unknown> }"
}
}
export class URLSearchParamsObj extends Wrapper {
override readonly tag = "URLSearchParams"
constructor(
+133
View File
@@ -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 = <R>(ctx: Interpreter<R>) => {
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 = <R>(ctx: Interpreter<R>) => {
const builtins = ctx.builtins
const proto = builtins.WeakMap
const weakMap = constructor<R>(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 = <R>(ctx: Interpreter<R>) => {
const builtins = ctx.builtins
const proto = builtins.WeakSet
const weakSet = constructor<R>(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
}
+5 -1
View File
@@ -16,6 +16,8 @@ import {
Obj,
RegExpObj,
SetObj,
WeakMapObj,
WeakSetObj,
coerceToString,
type Value,
} from "../interpreter/objects.js"
@@ -84,7 +86,9 @@ export const structuredCloneGlobal = <R>(ctx: Interpreter<R>) =>
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),
)
+48
View File
@@ -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")
})
})
+1 -1
View File
@@ -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.
+1 -3
View File
@@ -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",
@@ -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