feat(codemode): add Set method parity (#38000)

This commit is contained in:
Aiden Cline 2026-07-20 16:48:35 -05:00 committed by GitHub
commit f5a487ffcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 615 additions and 7 deletions

View file

@ -103,7 +103,7 @@ export const invokeIntrinsic = <R>(
// 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 = <R>(
throw new InterpreterRuntimeError(`Method '${ref.name}' is not available in CodeMode.`, node)
}
const coerceDateSetterArgument = <R>(
const coerceNumericArgument = <R>(
runner: CallbackRunner<R>,
value: unknown,
node: AstNode,
@ -424,7 +424,7 @@ export const invokeGroupBy = <R>(
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 = <R>(
})
}
const groupByItems = (source: unknown): Iterable<unknown> | undefined => {
const supportedIterableItems = (source: unknown): Iterable<unknown> | 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 = <R>(
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 = <R>(
runner: CallbackRunner<R>,
target: CodeModeSet,
name: string,
source: unknown,
node: AstNode,
): Effect.Effect<unknown, unknown, R> =>
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 = <R>(runner: CallbackRunner<R>, 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<string, unknown>
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 = <R>(
runner: CallbackRunner<R>,
target: CodeModeURLSearchParams,

View file

@ -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<unknown> | undefined => {
if (Array.isArray(value)) return value