From f5a487ffcd726b55a0b39dc769fc2628233bf146 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:48:35 -0500 Subject: [PATCH] feat(codemode): add Set method parity (#38000) --- packages/codemode/interpreter-support.md | 4 +- packages/codemode/src/interpreter/methods.ts | 137 +++++- packages/codemode/src/stdlib/collections.ts | 18 +- .../codemode/test/set-methods-test262.test.ts | 463 ++++++++++++++++++ 4 files changed, 615 insertions(+), 7 deletions(-) create mode 100644 packages/codemode/test/set-methods-test262.test.ts diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 9cdeb1176a..6807549515 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -295,8 +295,8 @@ ultimate source of truth. - [x] Materialized `keys`, `values`, and `entries` arrays for Map and Set. - [x] Spread, `for...of`, `Array.from`, and `Object.fromEntries` integration. - [x] Map and Set values serialize to `{}` at host/JSON boundaries. -- [ ] Set composition and relation methods: `union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, - `isSupersetOf`, and `isDisjointFrom`. +- [x] Set composition and relation methods: `union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, + `isSupersetOf`, and `isDisjointFrom`, including supported Set-like operands. ## URL and URI helpers diff --git a/packages/codemode/src/interpreter/methods.ts b/packages/codemode/src/interpreter/methods.ts index 895cb737bf..46a78e4e84 100644 --- a/packages/codemode/src/interpreter/methods.ts +++ b/packages/codemode/src/interpreter/methods.ts @@ -103,7 +103,7 @@ export const invokeIntrinsic = ( // Native setters read the current time before argument coercion, whose callbacks may mutate the Date. const initialTime = target.time return Effect.map( - Effect.forEach(args.slice(0, argumentCount), (arg) => coerceDateSetterArgument(runner, arg, node), { + Effect.forEach(args.slice(0, argumentCount), (arg) => coerceNumericArgument(runner, arg, node), { concurrency: 1, }), (values) => invokeDateMethod(target, ref.name, values, node, initialTime), @@ -127,7 +127,7 @@ export const invokeIntrinsic = ( throw new InterpreterRuntimeError(`Method '${ref.name}' is not available in CodeMode.`, node) } -const coerceDateSetterArgument = ( +const coerceNumericArgument = ( runner: CallbackRunner, value: unknown, node: AstNode, @@ -424,7 +424,7 @@ export const invokeGroupBy = ( throw new InterpreterRuntimeError(`${namespace}.groupBy expects an iterable collection.`, node).as("TypeError") } const apply = applyCollectionCallback(runner, args[1], `${namespace}.groupBy`, node) - const items = groupByItems(source) + const items = supportedIterableItems(source) if (items === undefined) { throw new InterpreterRuntimeError(`${namespace}.groupBy expects an iterable collection.`, node).as("TypeError") } @@ -458,7 +458,7 @@ export const invokeGroupBy = ( }) } -const groupByItems = (source: unknown): Iterable | undefined => { +const supportedIterableItems = (source: unknown): Iterable | undefined => { if (Array.isArray(source) || typeof source === "string") return source if (source instanceof CodeModeMap) return source.map.entries() if (source instanceof CodeModeSet) return source.set.values() @@ -655,11 +655,140 @@ const invokeSetMethod = ( return undefined }) } + case "union": + case "intersection": + case "difference": + case "symmetricDifference": + case "isSubsetOf": + case "isSupersetOf": + case "isDisjointFrom": + return invokeSetOperation(runner, target, name, args[0], node) default: throw new InterpreterRuntimeError(`Set method '${name}' is not available in CodeMode.`, node) } } +const invokeSetOperation = ( + runner: CallbackRunner, + target: CodeModeSet, + name: string, + source: unknown, + node: AstNode, +): Effect.Effect => + Effect.gen(function* () { + const other = yield* loadSetRecord(runner, source, name, node) + if (name === "union") { + const result = copySet(target) + for (const item of yield* other.keys()) result.set.add(item) + return result + } + if (name === "intersection") { + const result = new CodeModeSet() + if (target.set.size <= other.size) { + for (const item of target.set.values()) { + if (yield* other.has(item)) result.set.add(item) + } + return result + } + for (const item of yield* other.keys()) { + if (target.set.has(item)) result.set.add(item) + } + return result + } + if (name === "difference") { + const result = copySet(target) + if (target.set.size <= other.size) { + for (const item of result.set.values()) { + if (yield* other.has(item)) result.set.delete(item) + } + return result + } + for (const item of yield* other.keys()) result.set.delete(item) + return result + } + if (name === "symmetricDifference") { + const result = copySet(target) + for (const item of yield* other.keys()) { + if (target.set.has(item)) result.set.delete(item) + else result.set.add(item) + } + return result + } + if (name === "isSubsetOf") { + if (target.set.size > other.size) return false + for (const item of target.set.values()) { + if (!(yield* other.has(item))) return false + } + return true + } + if (name === "isSupersetOf") { + if (target.set.size < other.size) return false + for (const item of yield* other.keys()) { + if (!target.set.has(item)) return false + } + return true + } + if (target.set.size <= other.size) { + for (const item of target.set.values()) { + if (yield* other.has(item)) return false + } + return true + } + for (const item of yield* other.keys()) { + if (target.set.has(item)) return false + } + return true + }) + +const copySet = (source: CodeModeSet): CodeModeSet => { + const result = new CodeModeSet() + for (const item of source.set.values()) result.set.add(item) + return result +} + +const loadSetRecord = (runner: CallbackRunner, source: unknown, name: string, node: AstNode) => { + if (source instanceof CodeModeSet) { + return Effect.succeed({ + size: source.set.size, + has: (item: unknown) => Effect.succeed(source.set.has(item)), + keys: () => Effect.succeed(source.set.values()), + }) + } + if (source instanceof CodeModeMap) { + return Effect.succeed({ + size: source.map.size, + has: (item: unknown) => Effect.succeed(source.map.has(item)), + keys: () => Effect.succeed(source.map.keys()), + }) + } + if (source === null || typeof source !== "object" || isCodeModeValue(source)) { + throw new InterpreterRuntimeError(`Set.${name} expects a Set-like object.`, node).as("TypeError") + } + const object = source as Record + return Effect.gen(function* () { + const size = yield* coerceNumericArgument(runner, object.size, node) + if (Number.isNaN(size)) { + throw new InterpreterRuntimeError(`Set.${name} received a Set-like object with an invalid size.`, node).as( + "TypeError", + ) + } + if (!isSupportedCallback(object.has) || !isSupportedCallback(object.keys)) { + throw new InterpreterRuntimeError(`Set.${name} expects callable 'has' and 'keys' methods.`, node).as("TypeError") + } + const has = object.has + const keys = object.keys + return { + size: Math.max(Math.trunc(size), 0), + has: (item: unknown) => Effect.map(runner.invokeCallable(has, [item], node), Boolean), + keys: () => + Effect.flatMap(runner.invokeCallable(keys, [], node), (result) => { + if (Array.isArray(result)) return Effect.succeed(result) + throw new InterpreterRuntimeError(`Set.${name} expected 'keys' to return an iterator.`, node).as("TypeError") + }), + } + }) +} + const invokeURLSearchParamsMethod = ( runner: CallbackRunner, target: CodeModeURLSearchParams, diff --git a/packages/codemode/src/stdlib/collections.ts b/packages/codemode/src/stdlib/collections.ts index 4811c24d81..d8c0c1fe27 100644 --- a/packages/codemode/src/stdlib/collections.ts +++ b/packages/codemode/src/stdlib/collections.ts @@ -41,7 +41,23 @@ export const mapMethods = new Set(["get", "set", "has", "delete", "clear", "forE export const mapStatics = new Set(["groupBy"]) -export const setMethods = new Set(["add", "has", "delete", "clear", "forEach", "keys", "values", "entries"]) +export const setMethods = new Set([ + "add", + "has", + "delete", + "clear", + "forEach", + "keys", + "values", + "entries", + "union", + "intersection", + "difference", + "symmetricDifference", + "isSubsetOf", + "isSupersetOf", + "isDisjointFrom", +]) export const spreadItems = (value: unknown): Array | undefined => { if (Array.isArray(value)) return value diff --git a/packages/codemode/test/set-methods-test262.test.ts b/packages/codemode/test/set-methods-test262.test.ts new file mode 100644 index 0000000000..808b603f17 --- /dev/null +++ b/packages/codemode/test/set-methods-test262.test.ts @@ -0,0 +1,463 @@ +/* + * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75: + * - test/built-ins/Set/prototype/union/combines-sets.js + * - test/built-ins/Set/prototype/union/combines-empty-sets.js + * - test/built-ins/Set/prototype/union/combines-itself.js + * - test/built-ins/Set/prototype/union/combines-same-sets.js + * - test/built-ins/Set/prototype/union/combines-Map.js + * - test/built-ins/Set/prototype/union/result-order.js + * - test/built-ins/Set/prototype/union/appends-new-values.js + * - test/built-ins/Set/prototype/union/allows-set-like-object.js + * - test/built-ins/Set/prototype/union/size-is-a-number.js + * - test/built-ins/Set/prototype/union/has-is-callable.js + * - test/built-ins/Set/prototype/union/keys-is-callable.js + * - test/built-ins/Set/prototype/union/array-throws.js + * - test/built-ins/Set/prototype/union/converts-negative-zero.js + * - test/built-ins/Set/prototype/union/set-like-class-mutation.js + * - test/built-ins/Set/prototype/intersection/combines-sets.js + * - test/built-ins/Set/prototype/intersection/combines-itself.js + * - test/built-ins/Set/prototype/intersection/result-order.js + * - test/built-ins/Set/prototype/intersection/size-is-a-number.js + * - test/built-ins/Set/prototype/intersection/converts-negative-zero.js + * - test/built-ins/Set/prototype/intersection/combines-Map.js + * - test/built-ins/Set/prototype/intersection/allows-set-like-object.js + * - test/built-ins/Set/prototype/intersection/set-like-class-mutation.js + * - test/built-ins/Set/prototype/difference/combines-sets.js + * - test/built-ins/Set/prototype/difference/combines-itself.js + * - test/built-ins/Set/prototype/difference/result-order.js + * - test/built-ins/Set/prototype/difference/size-is-a-number.js + * - test/built-ins/Set/prototype/difference/converts-negative-zero.js + * - test/built-ins/Set/prototype/difference/combines-Map.js + * - test/built-ins/Set/prototype/difference/allows-set-like-object.js + * - test/built-ins/Set/prototype/difference/set-like-class-mutation.js + * - test/built-ins/Set/prototype/symmetricDifference/combines-sets.js + * - test/built-ins/Set/prototype/symmetricDifference/combines-itself.js + * - test/built-ins/Set/prototype/symmetricDifference/result-order.js + * - test/built-ins/Set/prototype/symmetricDifference/size-is-a-number.js + * - test/built-ins/Set/prototype/symmetricDifference/converts-negative-zero.js + * - test/built-ins/Set/prototype/symmetricDifference/set-like-class-mutation.js + * - test/built-ins/Set/prototype/symmetricDifference/combines-Map.js + * - test/built-ins/Set/prototype/symmetricDifference/allows-set-like-object.js + * - test/built-ins/Set/prototype/isSubsetOf/compares-sets.js + * - test/built-ins/Set/prototype/isSubsetOf/size-is-a-number.js + * - test/built-ins/Set/prototype/isSubsetOf/compares-Map.js + * - test/built-ins/Set/prototype/isSubsetOf/allows-set-like-object.js + * - test/built-ins/Set/prototype/isSubsetOf/compares-itself.js + * - test/built-ins/Set/prototype/isSubsetOf/compares-same-sets.js + * - test/built-ins/Set/prototype/isSubsetOf/set-like-class-mutation.js + * - test/built-ins/Set/prototype/isSupersetOf/compares-sets.js + * - test/built-ins/Set/prototype/isSupersetOf/size-is-a-number.js + * - test/built-ins/Set/prototype/isSupersetOf/compares-Map.js + * - test/built-ins/Set/prototype/isSupersetOf/allows-set-like-object.js + * - test/built-ins/Set/prototype/isSupersetOf/compares-itself.js + * - test/built-ins/Set/prototype/isSupersetOf/compares-same-sets.js + * - test/built-ins/Set/prototype/isSupersetOf/converts-negative-zero.js + * - test/built-ins/Set/prototype/isDisjointFrom/compares-sets.js + * - test/built-ins/Set/prototype/isDisjointFrom/size-is-a-number.js + * - test/built-ins/Set/prototype/isDisjointFrom/compares-Map.js + * - test/built-ins/Set/prototype/isDisjointFrom/allows-set-like-object.js + * - test/built-ins/Set/prototype/isDisjointFrom/compares-itself.js + * - test/built-ins/Set/prototype/isDisjointFrom/compares-same-sets.js + * - test/built-ins/Set/prototype/isDisjointFrom/converts-negative-zero.js + * - test/built-ins/Set/prototype/isDisjointFrom/set-like-class-mutation.js + * + * Copyright (C) 2023 Anthony Frehner. All rights reserved. + * Copyright (C) 2023 Anthony Frehner and Kevin Gibbons. All rights reserved. + * Copyright (C) 2023 Kevin Gibbons. All rights reserved. + * Copyright (C) 2023 Kevin Gibbons, Anthony Frehner. All rights reserved. + * Test262 portions are governed by the BSD license in LICENSE.test262. + * Set-like `keys` methods return arrays instead of iterator objects because + * CodeMode materializes supported iterators and does not support generators. + */ +import { describe, expect, test } from "bun:test" +import { Effect } from "effect" +import { CodeMode } from "../src/index.js" + +const value = async (code: string) => { + const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} })) + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) + return result.value +} + +describe("Set composition Test262 parity", () => { + test("combines Sets without mutating either operand", async () => { + expect( + await value(` + const left = new Set([1, 2]) + const right = new Set([2, 3]) + const union = left.union(right) + const intersection = left.intersection(right) + const difference = left.difference(right) + const symmetric = left.symmetricDifference(right) + return [ + [...union], union instanceof Set, + [...intersection], intersection instanceof Set, + [...difference], difference instanceof Set, + [...symmetric], symmetric instanceof Set, + [...left], [...right], + [...left.union(left)], left.union(left) !== left, + [...left.intersection(left)], left.intersection(left) !== left, + [...left.difference(left)], left.difference(left) !== left, + [...left.symmetricDifference(left)], left.symmetricDifference(left) !== left, + [...left.union(new Set())], [...new Set().union(left)], + ] + `), + ).toEqual([ + [1, 2, 3], + true, + [2], + true, + [1], + true, + [1, 3], + true, + [1, 2], + [2, 3], + [1, 2], + true, + [1, 2], + true, + [], + true, + [], + true, + [1, 2], + [1, 2], + ]) + }) + + test("preserves the specified result order", async () => { + expect( + await value(` + return [ + [...new Set([1, 3, 5]).intersection(new Set([3, 2, 1]))], + [...new Set([3, 2, 1, 0]).intersection(new Set([1, 3, 5]))], + [...new Set([1, 2, 3, 4]).difference(new Set([6, 5, 3, 2]))], + [...new Set([6, 5, 3, 2]).difference(new Set([1, 2, 3, 4]))], + [...new Set([1, 2, 3, 4]).symmetricDifference(new Set([6, 5, 4, 3]))], + [...new Set([2, 3]).union(new Set([1, 2]))], + [...new Set([1, 2, -3]).union(new Set([-1, 0]))], + ] + `), + ).toEqual([ + [1, 3], + [1, 3], + [1, 4], + [6, 5], + [1, 2, 6, 5], + [2, 3, 1], + [1, 2, -3, -1, 0], + ]) + }) + + test("accepts Maps as Set-like operands", async () => { + expect( + await value(` + const set = new Set([1, 2]) + const map = new Map([[2, "two"], [3, "three"]]) + return [ + [...set.union(map)], + [...set.intersection(map)], + [...set.difference(map)], + [...set.symmetricDifference(map)], + ] + `), + ).toEqual([[1, 2, 3], [2], [1], [1, 3]]) + }) + + test("accepts supported Set-like objects and uses the size-selected method", async () => { + expect( + await value(` + const keysOnly = { + size: 1, + has: () => { throw new Error("has should not be called") }, + keys: () => [2, 3], + } + const hasOnly = { + size: 2, + has: (value) => value === 2, + keys: () => { throw new Error("keys should not be called") }, + } + const set = new Set([1, 2]) + return [ + [...set.union(keysOnly)], + [...set.intersection(hasOnly)], + [...set.difference(hasOnly)], + [...set.symmetricDifference(keysOnly)], + ] + `), + ).toEqual([[1, 2, 3], [2], [1], [1, 3]]) + }) + + test("normalizes negative zero", async () => { + expect( + await value(` + const setlike = { + size: 1, + has: () => { throw new Error("has should not be called") }, + keys: () => [-0], + } + return [ + 1 / new Set([1]).union(setlike).values()[1] === Infinity, + 1 / new Set([0, 1, 2]).intersection(setlike).values()[0] === Infinity, + [...new Set([0, 1]).difference(setlike)], + 1 / new Set([1, 2]).symmetricDifference(setlike).values()[2] === Infinity, + ] + `), + ).toEqual([true, true, [1], true]) + }) + + test("handles duplicate Set-like keys using receiver membership", async () => { + expect( + await value(` + const other = { + size: 4, + has: () => { throw new Error("has should not be called") }, + keys: () => [2, 2, 3, 3], + } + return [...new Set([1, 2]).symmetricDifference(other)] + `), + ).toEqual([1, 3]) + }) + + test("copies receiver data before Set-like key iteration mutates it", async () => { + expect( + await value(` + const unionBase = new Set(["a", "b", "c", "d", "e"]) + const unionOther = { + size: 2, + has: () => { throw new Error("has should not be called") }, + keys: () => { + unionBase.delete("b") + unionBase.delete("c") + unionBase.add("b") + return ["x", "y"] + }, + } + const union = unionBase.union(unionOther) + + const intersectionBase = new Set(["a", "b", "c", "d", "e"]) + const intersectionOther = { + size: 3, + has: () => { throw new Error("has should not be called") }, + keys: () => { + intersectionBase.delete("b") + intersectionBase.delete("c") + intersectionBase.add("b") + return ["x", "b", "b"] + }, + } + const intersection = intersectionBase.intersection(intersectionOther) + + const differenceBase = new Set(["a", "b", "c", "d", "e"]) + const differenceOther = { + size: 3, + has: () => { throw new Error("has should not be called") }, + keys: () => { + differenceBase.delete("b") + differenceBase.delete("c") + differenceBase.add("b") + return ["x", "b", "b"] + }, + } + const difference = differenceBase.difference(differenceOther) + + const symmetricBase = new Set(["a", "b", "c", "d", "e"]) + const symmetricOther = { + size: 4, + has: () => { throw new Error("has should not be called") }, + keys: () => { + symmetricBase.delete("b") + symmetricBase.delete("c") + symmetricBase.add("b") + return ["x", "b", "c", "c"] + }, + } + const symmetric = symmetricBase.symmetricDifference(symmetricOther) + + return [ + [...union], [...unionBase], + [...intersection], [...intersectionBase], + [...difference], [...differenceBase], + [...symmetric], [...symmetricBase], + ] + `), + ).toEqual([ + ["a", "b", "c", "d", "e", "x", "y"], + ["a", "d", "e", "b"], + ["b"], + ["a", "d", "e", "b"], + ["a", "c", "d", "e"], + ["a", "d", "e", "b"], + ["a", "c", "d", "e", "x"], + ["a", "d", "e", "b"], + ]) + }) +}) + +describe("Set relation Test262 parity", () => { + test("compares Sets", async () => { + expect( + await value(` + const set = new Set([1, 2]) + return [ + set.isSubsetOf(new Set([2, 3])), set.isSubsetOf(new Set([1, 2, 3])), + set.isSupersetOf(new Set([2, 3])), set.isSupersetOf(new Set([1])), + set.isDisjointFrom(new Set([2, 3])), set.isDisjointFrom(new Set([3])), + new Set().isSubsetOf(set), set.isSupersetOf(new Set()), new Set().isDisjointFrom(new Set()), + set.isSubsetOf(set), set.isSupersetOf(set), set.isDisjointFrom(set), + set.isSubsetOf(new Set([1, 2])), set.isSupersetOf(new Set([1, 2])), + ] + `), + ).toEqual([false, true, false, true, false, true, true, true, true, true, true, false, true, true]) + }) + + test("compares Maps and supported Set-like objects", async () => { + expect( + await value(` + const set = new Set([1, 2]) + const map = new Map([[1, "one"], [2, "two"], [3, "three"]]) + const hasOnly = { + size: 2, + has: (value) => value === 1 || value === 2, + keys: () => { throw new Error("keys should not be called") }, + } + const keysOnly = { + size: 1, + has: () => { throw new Error("has should not be called") }, + keys: () => [1], + } + return [ + set.isSubsetOf(map), set.isSupersetOf(map), set.isDisjointFrom(map), + set.isSubsetOf(hasOnly), set.isSupersetOf(keysOnly), set.isDisjointFrom(hasOnly), + ] + `), + ).toEqual([true, false, false, true, true, false]) + }) + + test("normalizes negative zero from Set-like keys", async () => { + expect( + await value(` + const setlike = { + size: 1, + has: () => { throw new Error("has should not be called") }, + keys: () => [-0], + } + return [ + new Set([0, 1]).isSupersetOf(setlike), + new Set([0, 1]).isDisjointFrom(setlike), + ] + `), + ).toEqual([true, false]) + }) + + test("observes live receiver mutation while calling Set-like has", async () => { + expect( + await value(` + const subset = new Set(["a", "b", "c"]) + const subsetOther = { + size: 3, + has: (item) => { + if (item === "a") subset.delete("c") + return ["x", "a", "b"].includes(item) + }, + keys: () => { throw new Error("keys should not be called") }, + } + const disjoint = new Set(["a", "b", "c"]) + const disjointOther = { + size: 3, + has: (item) => { + if (item === "a") { + disjoint.delete("b") + disjoint.delete("c") + disjoint.add("b") + } + if (item === "c") throw new Error("deleted value should not be visited") + return false + }, + keys: () => { throw new Error("keys should not be called") }, + } + return [ + subset.isSubsetOf(subsetOther), [...subset], + disjoint.isDisjointFrom(disjointOther), [...disjoint], + ] + `), + ).toEqual([true, ["a", "b"], true, ["a", "b"]]) + }) +}) + +describe("Set-like validation Test262 parity", () => { + test("rejects arrays, invalid sizes, and non-callable methods with TypeError", async () => { + expect( + await value(` + const set = new Set([1]) + const names = [] + let coercionCalls = 0 + const invalid = [ + [], + { size: undefined, has: () => false, keys: () => [] }, + { size: NaN, has: () => false, keys: () => [] }, + { size: { valueOf: () => { coercionCalls += 1; return NaN } }, has: () => false, keys: () => [] }, + { size: "string", has: () => false, keys: () => [] }, + { size: 0, has: undefined, keys: () => [] }, + { size: 0, has: {}, keys: () => [] }, + { size: 0, has: () => false, keys: undefined }, + { size: 0, has: () => false, keys: {} }, + ] + for (const other of invalid) { + try { set.union(other) } catch (error) { names.push(error.name) } + } + return [names, coercionCalls] + `), + ).toEqual([ + [ + "TypeError", + "TypeError", + "TypeError", + "TypeError", + "TypeError", + "TypeError", + "TypeError", + "TypeError", + "TypeError", + ], + 1, + ]) + }) + + test("validates Set-like records for every method", async () => { + expect( + await value(` + const set = new Set([1]) + const invalid = { size: NaN, has: () => false, keys: () => [] } + const names = [] + const operations = [ + () => set.union(invalid), + () => set.intersection(invalid), + () => set.difference(invalid), + () => set.symmetricDifference(invalid), + () => set.isSubsetOf(invalid), + () => set.isSupersetOf(invalid), + () => set.isDisjointFrom(invalid), + ] + for (const operation of operations) { + try { operation() } catch (error) { names.push(error.name) } + } + return names + `), + ).toEqual(["TypeError", "TypeError", "TypeError", "TypeError", "TypeError", "TypeError", "TypeError"]) + }) + + test("requires Set-like keys to return CodeMode's materialized iterator representation", async () => { + expect( + await value(` + const set = new Set([1]) + const names = [] + for (const keys of [() => "ab", () => new Set([2]), () => new Map([[2, true]])]) { + try { set.union({ size: 1, has: () => false, keys }) } catch (error) { names.push(error.name) } + } + return names + `), + ).toEqual(["TypeError", "TypeError", "TypeError"]) + }) +})