Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/codemode/interpreter-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
137 changes: 133 additions & 4 deletions packages/codemode/src/interpreter/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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,
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 17 additions & 1 deletion packages/codemode/src/stdlib/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading