feat(codemode): support generator functions (#38172)
This commit is contained in:
parent
e84938b309
commit
c8a40450e5
13 changed files with 2229 additions and 389 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { Effect } from "effect"
|
||||
import { preserveConsumerError, type SyncIteratorRunner } from "../interpreter/iterator.js"
|
||||
import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
|
||||
import { spreadItems } from "./collections.js"
|
||||
|
||||
// Bun exposes ES2026 Math.sumPrecise before TypeScript's standard library types.
|
||||
declare global {
|
||||
|
|
@ -53,17 +54,6 @@ export const mathMethods = new Set([
|
|||
export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNode): number => {
|
||||
if (!mathMethods.has(name)) throw new InterpreterRuntimeError(`Math.${name} is not available.`, node)
|
||||
if (name === "random") return Math.random()
|
||||
if (name === "sumPrecise") {
|
||||
const items = spreadItems(args[0])
|
||||
if (items === undefined) {
|
||||
throw new InterpreterRuntimeError("Math.sumPrecise expects an iterable collection.", node).as("TypeError")
|
||||
}
|
||||
const numbers = Array.from(items)
|
||||
if (!numbers.every((item): item is number => typeof item === "number")) {
|
||||
throw new InterpreterRuntimeError("Math.sumPrecise expects an iterable of numbers.", node).as("TypeError")
|
||||
}
|
||||
return Math.sumPrecise(numbers)
|
||||
}
|
||||
// Validate only the arguments the method consumes; like JS, extras are ignored
|
||||
// (so built-ins work as callbacks receiving (element, index, array)).
|
||||
const num = (index: number): number => {
|
||||
|
|
@ -153,3 +143,29 @@ export const invokeMathMethod = (name: string, args: Array<unknown>, node: AstNo
|
|||
}
|
||||
throw new InterpreterRuntimeError(`Math.${name} is not available.`, node)
|
||||
}
|
||||
|
||||
export const invokeMathSumPrecise = <R>(
|
||||
runner: SyncIteratorRunner<R>,
|
||||
source: unknown,
|
||||
node: AstNode,
|
||||
): Effect.Effect<number, unknown, R> =>
|
||||
Effect.gen(function* () {
|
||||
const cursor = yield* runner.syncIterator(source, node)
|
||||
if (cursor === undefined) {
|
||||
throw new InterpreterRuntimeError("Math.sumPrecise expects a synchronous iterable.", node).as("TypeError")
|
||||
}
|
||||
const numbers: Array<number> = []
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
if (step.done) return Math.sumPrecise(numbers)
|
||||
yield* preserveConsumerError(
|
||||
cursor,
|
||||
Effect.sync(() => {
|
||||
if (typeof step.value !== "number") {
|
||||
throw new InterpreterRuntimeError("Math.sumPrecise expects an iterable of numbers.", node).as("TypeError")
|
||||
}
|
||||
numbers.push(step.value)
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Effect } from "effect"
|
||||
import {
|
||||
type AstNode,
|
||||
AsyncIteratorSymbol,
|
||||
|
|
@ -7,8 +8,9 @@ import {
|
|||
} from "../interpreter/model.js"
|
||||
import { containsOpaqueReference } from "../interpreter/references.js"
|
||||
import { isBlockedMember } from "../tool-runtime.js"
|
||||
import { isCodeModeValue, CodeModeMap, CodeModePromise, CodeModeSet, CodeModeURLSearchParams } from "../values.js"
|
||||
import { isCodeModeValue, CodeModePromise } from "../values.js"
|
||||
import { boundedData, coerceToString } from "./value.js"
|
||||
import { preserveConsumerError, type SyncIteratorRunner } from "../interpreter/iterator.js"
|
||||
|
||||
export const objectMethodsPreservingIdentity = new Set(["assign", "values", "entries", "fromEntries"])
|
||||
|
||||
|
|
@ -39,11 +41,6 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
|
|||
if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available.`, node)
|
||||
out[key] = item
|
||||
}
|
||||
const addEntry = (out: Record<string, unknown>, key: unknown, item: unknown): void => {
|
||||
boundedData(key, "Object.fromEntries key")
|
||||
boundedData(item, "Object.fromEntries value")
|
||||
guardedSet(out, coerceToString(key), item)
|
||||
}
|
||||
switch (name) {
|
||||
case "keys":
|
||||
return Object.keys(requireObject())
|
||||
|
|
@ -79,32 +76,47 @@ export const invokeObjectMethod = (name: string, args: Array<unknown>, node: Ast
|
|||
}
|
||||
return out
|
||||
}
|
||||
case "fromEntries": {
|
||||
if (args[0] instanceof CodeModeMap) {
|
||||
const out: Record<string, unknown> = Object.create(null)
|
||||
for (const [key, item] of args[0].map.entries()) addEntry(out, key, item)
|
||||
return out
|
||||
}
|
||||
if (args[0] instanceof CodeModeURLSearchParams) {
|
||||
const out: Record<string, unknown> = Object.create(null)
|
||||
for (const [key, value] of args[0].params.entries()) guardedSet(out, key, value)
|
||||
return out
|
||||
}
|
||||
const pairs = args[0] instanceof CodeModeSet ? Array.from(args[0].set.values()) : args[0]
|
||||
if (!Array.isArray(pairs)) {
|
||||
boundedData(args[0], "Object.fromEntries input")
|
||||
throw new InterpreterRuntimeError("Object.fromEntries expects an array of [key, value] pairs.", node)
|
||||
}
|
||||
const out: Record<string, unknown> = Object.create(null)
|
||||
for (const pair of pairs) {
|
||||
const validated = boundedData(pair, "Object.fromEntries entry")
|
||||
if (validated === null || typeof validated !== "object" || isCodeModeValue(validated))
|
||||
throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] entry objects.", node)
|
||||
const entry = pair as Record<string, unknown>
|
||||
addEntry(out, entry[0], entry[1])
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
throw new InterpreterRuntimeError(`Object.${name} is not available.`, node)
|
||||
}
|
||||
|
||||
export const invokeObjectFromEntries = <R>(
|
||||
runner: SyncIteratorRunner<R>,
|
||||
source: unknown,
|
||||
node: AstNode,
|
||||
): Effect.Effect<Record<string, unknown>, unknown, R> => {
|
||||
const out: Record<string, unknown> = Object.create(null)
|
||||
return Effect.gen(function* () {
|
||||
const cursor = yield* runner.syncIterator(source, node)
|
||||
if (cursor === undefined) {
|
||||
throw new InterpreterRuntimeError("Object.fromEntries expects a synchronous iterable of entries.", node).as(
|
||||
"TypeError",
|
||||
)
|
||||
}
|
||||
while (true) {
|
||||
const step = yield* cursor.next
|
||||
if (step.done) return out
|
||||
yield* preserveConsumerError(
|
||||
cursor,
|
||||
Effect.sync(() => {
|
||||
if (
|
||||
step.value === null ||
|
||||
typeof step.value !== "object" ||
|
||||
isCodeModeValue(step.value) ||
|
||||
containsOpaqueReference(step.value)
|
||||
) {
|
||||
throw new InterpreterRuntimeError("Object.fromEntries expects [key, value] entry objects.", node).as(
|
||||
"TypeError",
|
||||
)
|
||||
}
|
||||
const entry = step.value as Record<string, unknown>
|
||||
boundedData(entry[0], "Object.fromEntries key")
|
||||
boundedData(entry[1], "Object.fromEntries value")
|
||||
const key = coerceToString(entry[0])
|
||||
if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available.`, node)
|
||||
out[key] = entry[1]
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue