feat(codemode): support generator functions (#38172)

This commit is contained in:
Aiden Cline
2026-07-21 20:36:42 -05:00
committed by GitHub
parent e84938b309
commit c8a40450e5
13 changed files with 2232 additions and 392 deletions
+43 -16
View File
@@ -29,7 +29,8 @@ ultimate source of truth.
## Values and literals
- [x] `null`, `undefined`, booleans, finite and non-finite numbers, and strings.
- [x] Array literals, including holes and spread from arrays, strings, Maps, Sets, and URLSearchParams.
- [x] Array literals, including holes and spread from arrays, strings, Maps, Sets, URLSearchParams, custom synchronous
iterators, and synchronous generators.
- [x] Object literals with shorthand, computed string/number keys, and spread from plain data objects; `null` and
`undefined` are no-ops, while arrays are rejected.
- [x] Template literals with interpolation.
@@ -57,7 +58,9 @@ ultimate source of truth.
- [ ] Hoist function declarations accepted directly in switch cases.
- [x] Computed object destructuring keys such as `const { [field]: value } = record`.
- [x] Object destructuring from arrays, such as `const { length } = values`.
- [x] Array destructuring from supported non-array iterables: strings, Maps, Sets, and URLSearchParams.
- [x] Array binding and assignment destructuring from strings, Maps, Sets, URLSearchParams, custom synchronous
iterators, and synchronous generators, including stepwise elisions/rest and `IteratorClose` on early completion
or binding/default failure.
## Statements and control flow
@@ -65,7 +68,8 @@ ultimate source of truth.
- [x] `if`/`else` and conditional expressions.
- [x] `switch`, including default clauses and fallthrough.
- [x] `for`, `while`, and `do...while`.
- [x] `for...of` over arrays, strings, Maps, Sets, and URLSearchParams.
- [x] `for...of` over arrays, strings, Maps, Sets, URLSearchParams, custom synchronous iterators, and confined
synchronous generators. Abrupt completion invokes the iterator's optional `return()`.
- [x] `for...in` over own keys of plain objects, arrays, and tool references.
- [x] Unlabeled `break` and `continue`.
- [x] `try`, `catch`, optional catch bindings, and `finally`.
@@ -75,7 +79,7 @@ ultimate source of truth.
`Symbol.asyncIterator` or the `Symbol.iterator` fallback. Each iterator step is sequential, yielded promises and
plain values from synchronous collections and sync iterators are awaited before binding, and abrupt loop
completion invokes the iterator's optional `return()`. Custom async iterators control their yielded values, as in
JavaScript; only their `next()` results are awaited. Async generators remain outside the supported subset.
JavaScript; only their `next()` results are awaited. Confined sync and async generators are iterable here.
## Functions and callbacks
@@ -104,7 +108,27 @@ ultimate source of truth.
- [ ] User-defined constructor calls.
- [ ] `Function.prototype.call`, `apply`, and `bind` for CodeMode functions.
- [ ] Classes and private fields.
- [ ] Generator functions and `yield`.
- [x] Synchronous and async generator declarations/expressions, `yield`, and `yield*`, including lazy bodies,
`next(value)`, `return(value)`, `throw(value)`, exhaustion, promise adoption, async request ordering,
`try`/`catch`/`finally`, and sync/async iterator symbols. Async `yield*` awaits values while adapting a sync
iterator but preserves values supplied by a manually implemented async iterator. Generator values are opaque
runtime references.
- [x] Synchronous generators and custom synchronous iterators are consumed stepwise by array/argument spread, array
destructuring, `Array.from`, Map/Set/URLSearchParams construction, `Object.fromEntries`, Object/Map `groupBy`,
Promise combinators, `AggregateError`, and `Math.sumPrecise`. Mapper/grouping callbacks interleave with iterator
steps; synchronous consumers preserve yielded promise objects rather than awaiting them. Async generators are
rejected by every synchronous consumer.
- [x] Synchronous iterator acquisition and result validation follow `IteratorClose` boundaries: consumer errors and
intentional early stops invoke `return()`, acquisition/`next()` failures do not, and an original consumer error
wins over a cleanup failure. Async iterator consumption remains limited to `for await...of` and async `yield*`.
- [x] Portable generator protocol coverage is adapted from pinned Test262 cases for suspended-start, suspended-yield,
and completed states; sync and async `next`/`return`/`throw`; finally yields and completion overrides; rejected
yielded promises; mixed async request queues; sync and async `yield*` forwarding; malformed methods/results;
and declaration, expression, and object-method forms with closure and parameter behavior. The adapted suite
deliberately skips Test262 variants whose observation mechanism requires unsupported getter definitions,
proxies, prototype inspection or mutation, non-arrow `this`, classes, or arbitrary symbols. It also skips tests
asserting exact promise reaction-turn counts beyond the observable ordering guarantee documented below. These
are interpreter-surface boundaries, not claims that the corresponding full Test262 families pass unchanged.
## Expressions and operators
@@ -130,8 +154,8 @@ ultimate source of truth.
- [x] Tool calls start eagerly and return supervised, run-once CodeMode promises.
- [x] Direct `await`, repeated awaits, and implicit resolution when a promise is returned from a function/program.
- [x] `Promise.resolve` and `Promise.reject`.
- [x] `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any` over supported collections containing
promises and plain values.
- [x] `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any` over finite collections, custom synchronous
iterators, and synchronous generators containing promises and plain values.
- [x] `Promise.all` preserves result order and rejects on the first observed failure without cancelling siblings.
- [x] `Promise.allSettled` returns plain fulfilled/rejected outcome records.
- [x] `Promise.race` settles from the first result without cancelling losers at settlement time.
@@ -175,14 +199,16 @@ ultimate source of truth.
- [x] `Object()` and `new Object()` return `{}` for nullish arguments and pass objects through unchanged;
primitive wrapper objects (`Object(1)`) are rejected explicitly.
- [x] Computed property names and object spread.
- [x] `Object.keys`, `Object.values`, `Object.entries`, `Object.hasOwn`, `Object.assign`, and `Object.fromEntries`.
- [x] `Object.keys`, `Object.values`, `Object.entries`, `Object.hasOwn`, `Object.assign`, and `Object.fromEntries`, with
synchronous iterator support for `fromEntries`.
- [x] `Object.keys` over arrays and tool references.
- [x] Object identity is preserved by in-CodeMode Object helpers.
- [x] Prototype traversal and mutation through `__proto__`, `constructor`, and `prototype` are blocked.
- [ ] Legal own data fields named `__proto__`, `constructor`, or `prototype` are rejected at JSON/tool boundaries and
cannot be created, read, or written in CodeMode; tool path segments with those names remain supported.
- [x] `Object.is` for supported data values.
- [x] `Object.groupBy` over supported collection iterables, with string-key coercion and null-prototype results.
- [x] `Object.groupBy` over finite collections and custom synchronous iterators/generators, with string-key coercion
and null-prototype results.
## Arrays
@@ -190,7 +216,7 @@ ultimate source of truth.
array of that length; invalid lengths throw `RangeError`. Iteration, spread, join, and JSON handle holes like
JavaScript, and host results normalize holes to `null`.
- [x] Static methods: `Array.isArray`, `Array.of`, and `Array.from`, including the `Array.from` mapper form with
`(value, index)` arguments.
`(value, index)` arguments and stepwise synchronous iterator consumption.
- [x] Iteration/transformation: `map`, `filter`, `flatMap`, and `forEach`.
- [x] Searching/tests: `find`, `findIndex`, `findLast`, `findLastIndex`, `some`, `every`, `includes`, `indexOf`, and
`lastIndexOf`.
@@ -245,7 +271,8 @@ ultimate source of truth.
for feature detection. Calling any undefined value reports a native-style `TypeError` naming the callee, for
example `Math.sum is not a function.` Blocked members (`constructor`, `__proto__`, ...) still throw,
and unknown `Promise` statics keep their descriptive error.
- [x] `Math.sumPrecise` over supported collection iterables, rejecting non-number elements without coercion.
- [x] `Math.sumPrecise` over finite collections and custom synchronous iterators/generators, rejecting non-number
elements without coercion.
- [x] Global coercing `isFinite` and `isNaN`; opaque runtime references reject as data errors, like `Number(...)`.
## JSON and console
@@ -297,10 +324,10 @@ ultimate source of truth.
## Map and Set
- [x] Static `Map.groupBy` over supported collection iterables, preserving key identity.
- [x] `new Map()` from entry arrays or another Map.
- [x] Static `Map.groupBy` over finite collections and custom synchronous iterators/generators, preserving key identity.
- [x] `new Map()` from synchronous iterables of entries.
- [x] Map `get`, `set`, `has`, `delete`, `clear`, `size`, and `forEach`.
- [x] `new Set()` from arrays, strings, or another Set.
- [x] `new Set()` from synchronous iterables.
- [x] Set `add`, `has`, `delete`, `clear`, `size`, and `forEach`.
- [x] Materialized `keys`, `values`, and `entries` arrays for Map and Set.
- [x] Spread, `for...of`, `Array.from`, and `Object.fromEntries` integration.
@@ -316,7 +343,7 @@ ultimate source of truth.
- [x] Readable URL fields: `href`, `origin`, `protocol`, `username`, `password`, `host`, `hostname`, `port`,
`pathname`, `search`, and `hash`.
- [x] Writable URL fields except `origin`.
- [x] `new URLSearchParams()` from query strings, data objects, pairs, Maps, and URLSearchParams.
- [x] `new URLSearchParams()` from query strings, data objects, synchronous iterables of pairs, and URLSearchParams.
- [x] URLSearchParams `append`, `delete`, `get`, `getAll`, `has`, `set`, `sort`, `forEach`, `keys`, `values`,
`entries`, `toString`, and `size`.
- [x] URL values serialize to their href; URLSearchParams serialize to `{}`.
@@ -326,7 +353,7 @@ ultimate source of truth.
- [x] `Error`, `TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`, `EvalError`, and `URIError`, callable with
or without `new`.
- [x] `AggregateError` with the `(errors, message?)` signature and an own `errors` array, constructed directly or by
an all-rejected `Promise.any`.
an all-rejected `Promise.any`; direct construction accepts custom synchronous iterators and generators.
- [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization.
- [x] `instanceof` for Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, Promise, and Error types.
- [x] Catchable user throws, runtime failures raised during interpreted evaluation, awaited tool failures, and awaited
+26 -13
View File
@@ -1,9 +1,10 @@
import { Effect } from "effect"
import type { Diagnostic } from "../codemode.js"
import { ToolError } from "../tool-error.js"
import { copyOut, ToolRuntimeError, type SafeObject } from "../tool-runtime.js"
import { type AstNode, formatLocation, InterpreterRuntimeError, ProgramThrow, sourceLocation } from "./model.js"
import { containsRuntimeReference } from "./references.js"
import { spreadItems } from "../stdlib/collections.js"
import { type SyncIteratorRunner } from "./iterator.js"
import { coerceToString, createAggregateErrorValue, createErrorValue, errorConstructors } from "../stdlib/value.js"
export const normalizeError = (error: unknown): Diagnostic => {
@@ -79,15 +80,27 @@ export const caughtErrorValue = (thrown: unknown): unknown => {
return createErrorValue(name, normalizeError(thrown).message)
}
export const constructErrorValue = (name: string, args: Array<unknown>, node: AstNode): SafeObject => {
if (name !== "AggregateError") return createErrorValue(name, args[0] === undefined ? "" : coerceToString(args[0]))
const errors = spreadItems(args[0])
if (errors === undefined) {
throw new InterpreterRuntimeError(
"new AggregateError(...) expects an array of errors (e.g. new AggregateError(errors, message?)).",
node,
).as("TypeError")
}
// Error values must not alias caller-owned arrays.
return createAggregateErrorValue([...errors], args[1] === undefined ? "" : coerceToString(args[1]))
}
export const constructErrorValue = (name: string, args: Array<unknown>): SafeObject =>
createErrorValue(name, args[0] === undefined ? "" : coerceToString(args[0]))
export const constructAggregateErrorValue = <R>(
runner: SyncIteratorRunner<R>,
args: Array<unknown>,
node: AstNode,
): Effect.Effect<SafeObject, unknown, R> =>
Effect.gen(function* () {
const cursor = yield* runner.syncIterator(args[0], node)
if (cursor === undefined) {
throw new InterpreterRuntimeError("new AggregateError(...) expects a synchronous iterable of errors.", node).as(
"TypeError",
)
}
const errors: Array<unknown> = []
while (true) {
const step = yield* cursor.next
if (step.done) {
return createAggregateErrorValue(errors, args[1] === undefined ? "" : coerceToString(args[1]))
}
errors.push(step.value)
}
})
@@ -0,0 +1,21 @@
import { Effect, Exit } from "effect"
import type { AstNode } from "./model.js"
export type IteratorCursor<R> = {
readonly next: Effect.Effect<{ readonly done: boolean; readonly value: unknown }, unknown, R>
readonly close: Effect.Effect<void, unknown, R>
}
export type SyncIteratorRunner<R> = {
readonly syncIterator: (value: unknown, node: AstNode) => Effect.Effect<IteratorCursor<R> | undefined, unknown, R>
}
export const preserveConsumerError = <A, R>(
cursor: IteratorCursor<R>,
effect: Effect.Effect<A, unknown, R>,
): Effect.Effect<A, unknown, R> =>
Effect.flatMap(Effect.exit(effect), (exit) =>
Exit.isSuccess(exit)
? Effect.succeed(exit.value)
: Effect.andThen(Effect.exit(cursor.close), Effect.failCause(exit.cause)),
)
+55 -38
View File
@@ -2,6 +2,7 @@ import { Effect } from "effect"
import {
type AstNode,
CodeModeFunction,
CodeModeGenerator,
CoercionFunction,
ErrorConstructorReference,
GlobalMethodReference,
@@ -33,6 +34,7 @@ import { invokeRegExpMethod, invokeRegExpStatic, matchToValue, toHostRegex } fro
import { invokeStringStatic } from "../stdlib/string.js"
import { invokeURLMethod, invokeURLStatic, uriArgument } from "../stdlib/url.js"
import { boundedData, coerceToNumber, coerceToString, errorBrandName } from "../stdlib/value.js"
import { preserveConsumerError, type SyncIteratorRunner } from "./iterator.js"
export type CallbackRunner<R> = {
readonly invokeFunction: (fn: CodeModeFunction, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
@@ -360,19 +362,12 @@ const invokeArrayStatic = (name: string, args: Array<unknown>, node: AstNode): u
return Array.isArray(args[0])
case "of":
return [...args]
case "from":
return arrayFromItems(args[0], node)
default:
throw new InterpreterRuntimeError(`Array.${name} is not available.`, node)
}
}
const arrayFromItems = (source: unknown, node: AstNode): Array<unknown> => {
if (source instanceof CodeModeMap) return Array.from(source.map.entries(), ([key, item]) => [key, item])
if (source instanceof CodeModeSet) return Array.from(source.set.values())
if (source instanceof CodeModeURLSearchParams) {
return Array.from(source.params.entries(), ([key, value]) => [key, value])
}
const arrayLikeSource = (source: unknown, node: AstNode): { readonly length: number; readonly source: object } => {
if (source instanceof CodeModePromise) {
throw new InterpreterRuntimeError(
"Array.from received an un-awaited Promise; await it before creating the array.",
@@ -380,15 +375,16 @@ const arrayFromItems = (source: unknown, node: AstNode): Array<unknown> => {
"InvalidDataValue",
)
}
if (typeof source === "string") return Array.from(source)
if (Array.isArray(source)) return [...source]
if (
source !== null &&
typeof source === "object" &&
(Object.getPrototypeOf(source) === Object.prototype || Object.getPrototypeOf(source) === null) &&
typeof (source as { length?: unknown }).length === "number"
) {
return Array.from(source as ArrayLike<unknown>)
const length = (source as { length: number }).length
const normalized = Number.isNaN(length) || length <= 0 ? 0 : Math.trunc(length)
if (normalized > 4_294_967_295) throw new RangeError("Invalid array length")
return { length: normalized, source }
}
throw new InterpreterRuntimeError(
"Array.from expects an array, string, Map, Set, or array-like value.",
@@ -398,24 +394,42 @@ const arrayFromItems = (source: unknown, node: AstNode): Array<unknown> => {
}
export const invokeArrayFrom = <R>(
runner: CallbackRunner<R>,
runner: CallbackRunner<R> & SyncIteratorRunner<R>,
args: Array<unknown>,
node: AstNode,
): Effect.Effect<unknown, unknown, R> => {
const items = arrayFromItems(args[0], node)
if (args.length < 2 || args[1] === undefined) return Effect.succeed(items)
const apply = applyCollectionCallback(runner, args[1], "Array.from", node)
const source = args[0]
const apply =
args.length < 2 || args[1] === undefined ? undefined : applyCollectionCallback(runner, args[1], "Array.from", node)
return Effect.gen(function* () {
const values: Array<unknown> = []
for (let index = 0; index < items.length; index += 1) {
values.push(yield* apply([items[index], index]))
const cursor = yield* runner.syncIterator(source, node)
if (cursor === undefined) {
if (source instanceof CodeModeGenerator) {
throw new InterpreterRuntimeError("Array.from expects a synchronous iterable or array-like value.", node).as(
"TypeError",
)
}
const arrayLike = arrayLikeSource(source, node)
const values: Array<unknown> = []
for (let index = 0; index < arrayLike.length; index += 1) {
const item = Reflect.get(arrayLike.source, index)
values.push(apply === undefined ? item : yield* apply([item, index]))
}
return values
}
const values: Array<unknown> = []
let index = 0
while (true) {
const step = yield* cursor.next
if (step.done) return values
values.push(apply === undefined ? step.value : yield* preserveConsumerError(cursor, apply([step.value, index])))
index += 1
}
return values
})
}
export const invokeGroupBy = <R>(
runner: CallbackRunner<R>,
runner: CallbackRunner<R> & SyncIteratorRunner<R>,
namespace: "Map" | "Object",
args: Array<unknown>,
node: AstNode,
@@ -425,47 +439,50 @@ 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 = supportedIterableItems(source)
if (items === undefined) {
throw new InterpreterRuntimeError(`${namespace}.groupBy expects an iterable collection.`, node).as("TypeError")
}
return Effect.gen(function* () {
const cursor = yield* runner.syncIterator(source, node)
if (cursor === undefined) {
throw new InterpreterRuntimeError(`${namespace}.groupBy expects an iterable collection.`, node).as("TypeError")
}
if (namespace === "Map") {
const result = new CodeModeMap()
let index = 0
for (const item of items) {
const key = yield* apply([item, index])
while (true) {
const step = yield* cursor.next
if (step.done) return result
const item = step.value
const key = yield* preserveConsumerError(cursor, apply([item, index]))
const group = result.map.get(key)
if (group === undefined) result.map.set(key, [item])
else (group as Array<unknown>).push(item)
index += 1
}
return result
}
const result: SafeObject = Object.create(null) as SafeObject
let index = 0
for (const item of items) {
const key = yield* coerceGroupByPropertyKey(runner, yield* apply([item, index]), node)
while (true) {
const step = yield* cursor.next
if (step.done) return result
const item = step.value
const key = yield* preserveConsumerError(
cursor,
Effect.flatMap(apply([item, index]), (value) => coerceGroupByPropertyKey(runner, value, node)),
)
if (isBlockedMember(key)) {
throw new InterpreterRuntimeError(`Property '${key}' is not available.`, node)
return yield* preserveConsumerError(
cursor,
Effect.fail(new InterpreterRuntimeError(`Property '${key}' is not available.`, node)),
)
}
const group = result[key]
if (group === undefined) result[key] = [item]
else (group as Array<unknown>).push(item)
index += 1
}
return result
})
}
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()
if (source instanceof CodeModeURLSearchParams) return source.params.entries()
}
const coerceGroupByPropertyKey = <R>(
runner: CallbackRunner<R>,
value: unknown,
@@ -1,3 +1,4 @@
import type { Effect } from "effect"
import type { SafeObject } from "../tool-runtime.js"
import type { CodeModePromise, CodeModeRegExp, CodeModeURL } from "../values.js"
@@ -45,6 +46,27 @@ export class CodeModeFunction {
readonly body: AstNode,
readonly capturedScopes: ReadonlyArray<Map<string, Binding>>,
readonly async: boolean,
readonly generator: boolean,
) {}
}
export type GeneratorRequestKind = "next" | "return" | "throw"
export class CodeModeGenerator {
constructor(
readonly asynchronous: boolean,
readonly request: (
kind: GeneratorRequestKind,
value: unknown,
node: AstNode,
) => Effect.Effect<unknown, unknown, unknown>,
) {}
}
export class GeneratorMethodReference {
constructor(
readonly generator: CodeModeGenerator,
readonly kind: GeneratorRequestKind | "iterator",
) {}
}
@@ -128,6 +150,10 @@ export class ProgramThrow {
constructor(readonly value: unknown) {}
}
export class GeneratorReturn {
constructor(readonly value: unknown) {}
}
export class ErrorConstructorReference {
constructor(readonly name: string) {}
}
+73 -78
View File
@@ -13,9 +13,9 @@ import {
import { caughtErrorValue, normalizeError } from "./errors.js"
import { applyCollectionCallback, isSupportedCallback, type CallbackRunner, type SupportedCallback } from "./methods.js"
import { typeofValue } from "./references.js"
import { spreadItems } from "../stdlib/collections.js"
import { createAggregateErrorValue } from "../stdlib/value.js"
import { CodeModePromise } from "../values.js"
import type { SyncIteratorRunner } from "./iterator.js"
// Observation only controls rejection reporting; program completion interrupts all promise work.
export class PromiseRuntime<R> {
@@ -64,6 +64,10 @@ export class PromiseRuntime<R> {
return Fiber.await(promise.fiber)
}
fork(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<void, never, R> {
return Effect.asVoid(Effect.forkIn(effect, this.scope, { startImmediately: true }))
}
diagnostics(): Array<Diagnostic> {
return [...this.failures].sort(([left], [right]) => left - right).map(([, failure]) => failure)
}
@@ -84,7 +88,7 @@ export const selfResolutionError = (node?: AstNode): InterpreterRuntimeError =>
new InterpreterRuntimeError("Chaining cycle detected: a promise cannot resolve with itself.", node).as("TypeError")
export const invokePromiseMethod = <R>(
runner: CallbackRunner<R>,
runner: CallbackRunner<R> & SyncIteratorRunner<R>,
promises: PromiseRuntime<R>,
ref: PromiseMethodReference,
args: Array<unknown>,
@@ -98,79 +102,69 @@ export const invokePromiseMethod = <R>(
return promises.create(Effect.fail(new ProgramThrow(args[0])))
}
const spread = spreadItems(args[0])
if (spread === undefined) {
return promises.create(
Effect.fail(
new InterpreterRuntimeError(
`Promise.${ref.name} expects an array of promises or plain values (e.g. Promise.${ref.name}(items.map((item) => tools.ns.tool(item)))).`,
return promises.create(
Effect.gen(function* () {
const cursor = yield* runner.syncIterator(args[0], node)
if (cursor === undefined) {
throw new InterpreterRuntimeError(
`Promise.${ref.name} expects an array or other synchronous iterable.`,
node,
).as("TypeError"),
),
)
}
const items = Array.from(spread)
).as("TypeError")
}
const items: Array<unknown> = []
while (true) {
const step = yield* cursor.next
if (step.done) break
items.push(step.value)
if (step.value instanceof CodeModePromise) promises.markObserved(step.value)
}
for (const item of items) {
if (item instanceof CodeModePromise) promises.markObserved(item)
}
switch (ref.name) {
case "all": {
const observations = items.map((item) =>
item instanceof CodeModePromise ? Effect.flatten(promises.await(item)) : Effect.succeed(item),
)
return promises.create(settleAfterTurn(Effect.all(observations, { concurrency: "unbounded" })))
}
case "allSettled": {
const observations = items.map((item) =>
item instanceof CodeModePromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)),
)
return promises.create(
settleAfterTurn(
Effect.gen(function* () {
const outcomes: Array<unknown> = []
for (const observation of observations) {
const exit = yield* observation
if (Exit.isSuccess(exit)) {
outcomes.push(
Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }),
)
continue
}
if (Cause.hasInterruptsOnly(exit.cause)) {
// Teardown interruption is not a program-level rejection.
return yield* Effect.failCause(exit.cause)
}
outcomes.push(
Object.assign(Object.create(null) as SafeObject, {
status: "rejected",
reason: caughtErrorValue(Cause.squash(exit.cause)),
}),
)
}
return outcomes
}),
),
)
}
case "race": {
if (items.length === 0) {
return promises.create(
Effect.fail(
new InterpreterRuntimeError(
"Promise.race([]) would never settle; provide at least one promise or value.",
node,
if (ref.name === "all") {
return yield* settleAfterTurn(
Effect.all(
items.map((item) =>
item instanceof CodeModePromise ? Effect.flatten(promises.await(item)) : Effect.succeed(item),
),
{ concurrency: "unbounded" },
),
)
}
if (ref.name === "allSettled") {
const outcomes: Array<unknown> = []
for (const item of items) {
const exit = item instanceof CodeModePromise ? yield* promises.await(item) : Exit.succeed(item)
if (Exit.isSuccess(exit)) {
outcomes.push(Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }))
continue
}
if (Cause.hasInterruptsOnly(exit.cause)) return yield* Effect.failCause(exit.cause)
outcomes.push(
Object.assign(Object.create(null) as SafeObject, {
status: "rejected",
reason: caughtErrorValue(Cause.squash(exit.cause)),
}),
)
}
yield* Effect.yieldNow
return outcomes
}
if (ref.name === "race") {
if (items.length === 0) {
throw new InterpreterRuntimeError(
"Promise.race([]) would never settle; provide at least one promise or value.",
node,
)
}
return yield* settleAfterTurn(
Effect.flatten(
Effect.raceAll(
items.map((item) =>
item instanceof CodeModePromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)),
),
),
),
)
}
const observations = items.map((item) =>
item instanceof CodeModePromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)),
)
return promises.create(settleAfterTurn(Effect.flatten(Effect.raceAll(observations))))
}
case "any": {
const flipped = items.map((item) =>
item instanceof CodeModePromise
? Effect.flatMap(promises.await(item), (exit) => {
@@ -180,17 +174,18 @@ export const invokePromiseMethod = <R>(
})
: Effect.fail(new PromiseAnyFulfilled(item)),
)
const body = Effect.all(flipped, { concurrency: "unbounded" }).pipe(
Effect.flatMap((reasons) =>
Effect.fail(new ProgramThrow(createAggregateErrorValue(reasons, "All promises were rejected"))),
),
Effect.catch((error) =>
error instanceof PromiseAnyFulfilled ? Effect.succeed(error.value) : Effect.fail(error),
return yield* settleAfterTurn(
Effect.all(flipped, { concurrency: "unbounded" }).pipe(
Effect.flatMap((reasons) =>
Effect.fail(new ProgramThrow(createAggregateErrorValue(reasons, "All promises were rejected"))),
),
Effect.catch((error) =>
error instanceof PromiseAnyFulfilled ? Effect.succeed(error.value) : Effect.fail(error),
),
),
)
return promises.create(settleAfterTurn(body))
}
}
}),
)
}
export const invokePromiseInstanceMethod = <R>(
@@ -1,10 +1,12 @@
import {
type AstNode,
CodeModeFunction,
CodeModeGenerator,
CoercionFunction,
ErrorConstructorReference,
GlobalMethodReference,
GlobalNamespace,
GeneratorMethodReference,
InterpreterRuntimeError,
IntrinsicReference,
JsonMethodReference,
@@ -21,6 +23,8 @@ import { isCodeModeValue, CodeModePromise } from "../values.js"
export const isRuntimeReference = (value: unknown): boolean =>
value instanceof CodeModeFunction ||
value instanceof CodeModeGenerator ||
value instanceof GeneratorMethodReference ||
value instanceof ToolReference ||
value instanceof IntrinsicReference ||
value instanceof GlobalNamespace ||
@@ -107,6 +111,7 @@ export const rejectCircularInsertion = (container: object, value: unknown, label
export const typeofValue = (value: unknown): string => {
if (
value instanceof CodeModeFunction ||
value instanceof GeneratorMethodReference ||
value instanceof CoercionFunction ||
value instanceof IntrinsicReference ||
value instanceof GlobalMethodReference ||
+637 -201
View File
@@ -1,4 +1,4 @@
import { Cause, Effect, Exit } from "effect"
import { Cause, Deferred, Effect, Exit } from "effect"
import { isBlockedMember, ToolReference, ToolRuntimeError, type SafeObject } from "../tool-runtime.js"
import {
type AstNode,
@@ -6,11 +6,15 @@ import {
asNode,
type Binding,
CodeModeFunction,
CodeModeGenerator,
CoercionFunction,
ComputedValue,
ErrorConstructorReference,
GlobalMethodReference,
GlobalNamespace,
GeneratorMethodReference,
GeneratorReturn,
type GeneratorRequestKind,
type GlobalNamespaceName,
getArray,
getBoolean,
@@ -35,11 +39,10 @@ import {
SearchFunction,
SymbolNamespace,
type StatementResult,
supportedSyntaxMessage,
unsupportedSyntax,
UriFunction,
} from "./model.js"
import { caughtErrorValue, constructErrorValue } from "./errors.js"
import { caughtErrorValue, constructAggregateErrorValue, constructErrorValue } from "./errors.js"
import {
arrayStatics,
type CallbackRunner,
@@ -48,6 +51,7 @@ import {
invokeGroupBy,
invokeIntrinsic,
} from "./methods.js"
import { preserveConsumerError, type SyncIteratorRunner } from "./iterator.js"
import {
constructPromise,
invokePromiseInstanceMethod,
@@ -57,13 +61,13 @@ import {
} from "./promises.js"
import { containsOpaqueReference, isRuntimeReference, rejectCircularInsertion, typeofValue } from "./references.js"
import { ScopeStack } from "./scope.js"
import { arrayMethods, mapMethods, mapStatics, setMethods, spreadItems } from "../stdlib/collections.js"
import { arrayMethods, mapMethods, mapStatics, setMethods } from "../stdlib/collections.js"
import { consoleMethods, formatConsoleMessage } from "../stdlib/console.js"
import { dateMethods, dateStatics } from "../stdlib/date.js"
import { invokeJsonMethod, jsonStatics, type JsonMethodName } from "../stdlib/json.js"
import { mathConstants, mathMethods } from "../stdlib/math.js"
import { invokeMathSumPrecise, mathConstants, mathMethods } from "../stdlib/math.js"
import { numberConstants, numberMethods, numberStatics } from "../stdlib/number.js"
import { objectMethodsPreservingIdentity, objectStatics } from "../stdlib/object.js"
import { invokeObjectFromEntries, objectMethodsPreservingIdentity, objectStatics } from "../stdlib/object.js"
import { promiseStatics } from "../stdlib/promise.js"
import {
escapeRegexHint,
@@ -216,11 +220,52 @@ const loopDeclaration = (left: AstNode, statement: "for...of" | "for...in") => {
}
type CustomIterator = {
iterator: SafeObject
iterator: SafeObject | CodeModeGenerator
next: unknown
asynchronous: boolean
}
type OpaqueMemberReference =
| ToolReference
| PromiseMethodReference
| PromiseInstanceMethodReference
| IntrinsicReference
| GlobalMethodReference
| JsonMethodReference
| GeneratorMethodReference
const isOpaqueMemberReference = (value: unknown): value is OpaqueMemberReference =>
value instanceof ToolReference ||
value instanceof PromiseMethodReference ||
value instanceof PromiseInstanceMethodReference ||
value instanceof IntrinsicReference ||
value instanceof GlobalMethodReference ||
value instanceof JsonMethodReference ||
value instanceof GeneratorMethodReference
const copyIteratorSymbols = (source: object, target: object, consumed?: ReadonlySet<PropertyKey>): void => {
for (const symbol of IteratorSymbols) {
if (!consumed?.has(symbol) && Object.hasOwn(source, symbol))
Reflect.set(target, symbol, Reflect.get(source, symbol))
}
}
type GeneratorRequest = {
kind: GeneratorRequestKind
value: unknown
response: Deferred.Deferred<unknown, unknown>
}
type GeneratorState = {
started: boolean
completed: boolean
draining: boolean
active?: GeneratorRequest
pending: Array<GeneratorRequest>
pendingIndex: number
available?: Deferred.Deferred<void>
}
export class Interpreter<R> {
private scopes: ScopeStack
private readonly invokeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
@@ -228,10 +273,13 @@ export class Interpreter<R> {
private readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
private readonly logs: Array<string>
private readonly promises: PromiseRuntime<R>
private readonly runner: CallbackRunner<R> = {
private generatorState?: GeneratorState
private generatorAsync = false
private readonly runner: CallbackRunner<R> & SyncIteratorRunner<R> = {
invokeFunction: (fn, args) => this.invokeFunction(fn, args),
invokeCallable: (callable, args, node) => this.invokeCallable(callable, args, node),
settlePromise: (promise) => this.settlePromise(promise),
syncIterator: (value, node) => this.syncIterator(value, node),
}
constructor(
@@ -403,16 +451,12 @@ export class Interpreter<R> {
}
private createFunction(node: AstNode): CodeModeFunction {
if (node.generator === true) {
throw new InterpreterRuntimeError("Generator functions are not supported.", node, "UnsupportedSyntax", [
supportedSyntaxMessage,
])
}
return new CodeModeFunction(
getArray(node, "params").map((parameter, index) => asNode(parameter, `params[${index}]`)),
getNode(node, "body"),
this.scopes.capture(),
node.async === true,
node.generator === true,
)
}
@@ -646,14 +690,20 @@ export class Interpreter<R> {
const right = yield* self.evaluateExpression(getNode(node, "right"))
const body = getNode(node, "body")
const iterable = spreadItems(right)
const iterator = iterable === undefined && awaiting ? yield* self.customIterator(right, node) : undefined
if (iterable === undefined && iterator === undefined) {
const iterator = yield* self.customIterator(right, node, awaiting)
const cursor = iterator === undefined ? yield* self.syncIterator(right, node) : undefined
if (iterator === undefined && cursor === undefined) {
throw new InterpreterRuntimeError(
`${awaiting ? "for await...of" : "for...of"} requires an array, string, Map, Set, or URLSearchParams${awaiting ? ", or custom iterator" : ""} value.`,
`${awaiting ? "for await...of" : "for...of"} requires an array, string, Map, Set, or URLSearchParams, or custom iterator value.`,
node,
)
).as("TypeError")
}
const close = () =>
iterator
? self.closeIterator(iterator, node, awaiting)
: awaiting
? Effect.andThen(cursor?.close ?? Effect.void, Effect.yieldNow)
: (cursor?.close ?? Effect.void)
let assignment: AstNode | undefined
@@ -687,45 +737,35 @@ export class Interpreter<R> {
),
)
if (iterable !== undefined) {
for (const value of iterable) {
const result = yield* evaluateBody(awaiting ? yield* self.awaitValue(value) : value)
if (result.kind === "return") return result
if (result.kind === "break") {
if (result.label !== undefined && !labels?.has(result.label)) return result
return { kind: "none" } satisfies StatementResult
}
if (result.kind === "continue" && result.label !== undefined && !labels?.has(result.label)) return result
}
return { kind: "none" } satisfies StatementResult
}
if (iterator === undefined) throw new InterpreterRuntimeError("Custom iterator is unavailable.", node)
while (true) {
const step = yield* self.nextIteratorResult(iterator, node)
const current = iterator
? yield* self.nextIteratorResult(iterator, node, awaiting)
: yield* cursor?.next ?? Effect.fail(new InterpreterRuntimeError("Iterator is unavailable.", node))
const step = cursor && awaiting ? { done: current.done, value: yield* self.awaitValue(current.value) } : current
if (step.done) return { kind: "none" } satisfies StatementResult
const bodyExit = yield* Effect.exit(evaluateBody(step.value))
if (!Exit.isSuccess(bodyExit)) {
// Process interruption must remain prompt; user cleanup cannot extend a timeout.
if (!Cause.hasInterruptsOnly(bodyExit.cause)) yield* Effect.exit(self.closeIterator(iterator, node))
if (!Cause.hasInterruptsOnly(bodyExit.cause)) {
yield* Effect.exit(close())
}
return yield* Effect.failCause(bodyExit.cause)
}
const result = bodyExit.value
if (result.kind === "return") {
yield* self.closeIterator(iterator, node)
yield* close()
return result
}
if (result.kind === "break") {
yield* self.closeIterator(iterator, node)
yield* close()
if (result.label !== undefined && !labels?.has(result.label)) return result
return { kind: "none" } satisfies StatementResult
}
if (result.kind === "continue" && result.label !== undefined && !labels?.has(result.label)) {
yield* self.closeIterator(iterator, node)
yield* close()
return result
}
}
@@ -742,26 +782,86 @@ export class Interpreter<R> {
return value instanceof CodeModePromise ? this.settlePromise(value) : Effect.as(Effect.yieldNow, value)
}
private customIterator(value: unknown, node: AstNode) {
private awaitAsyncFromSyncValue(
iterator: CustomIterator,
value: unknown,
node: AstNode,
closeOnRejection: boolean,
): Effect.Effect<unknown, unknown, R> {
const self = this
return Effect.gen(function* () {
const settled = yield* Effect.exit(self.awaitValue(value))
if (Exit.isSuccess(settled)) return settled.value
if (closeOnRejection && !Cause.hasInterruptsOnly(settled.cause)) {
yield* Effect.exit(self.closeIterator(iterator, node, false))
}
return yield* Effect.failCause(settled.cause)
})
}
private syncIterator(value: unknown, node: AstNode) {
const iterator = Array.isArray(value)
? value[Symbol.iterator]()
: typeof value === "string"
? value[Symbol.iterator]()
: value instanceof CodeModeMap
? value.map.entries()
: value instanceof CodeModeSet
? value.set.values()
: value instanceof CodeModeURLSearchParams
? value.params.entries()
: undefined
if (iterator !== undefined) {
return Effect.succeed({
next: Effect.sync(() => {
const step = iterator.next()
return { done: Boolean(step.done), value: step.value }
}),
close: Effect.void,
})
}
const self = this
return Effect.map(this.customIterator(value, node, false), (iterator) =>
iterator === undefined
? undefined
: {
next: self.nextIteratorResult(iterator, node, false),
close: Effect.suspend(() => self.closeIterator(iterator, node, false)),
},
)
}
private customIterator(value: unknown, node: AstNode, allowAsync = true) {
if (value instanceof CodeModeGenerator) {
if (value.asynchronous && !allowAsync) return Effect.succeed(undefined)
return Effect.succeed({
iterator: value,
next: new GeneratorMethodReference(value, "next"),
asynchronous: value.asynchronous,
})
}
if (!isRecord(value) || isRuntimeReference(value)) return Effect.succeed(undefined)
const asyncMethod = Reflect.get(value, AsyncIteratorSymbol)
const asyncMethod = allowAsync ? Reflect.get(value, AsyncIteratorSymbol) : undefined
const method = asyncMethod ?? Reflect.get(value, IteratorSymbol)
if (method === undefined || method === null) return Effect.succeed(undefined)
const self = this
return Effect.map(
this.invokeCallable(this.requireIteratorMethod(method, "Iterator method", node), [], node),
(iterator) => {
const object = self.requireIteratorObject(iterator, "Iterator method result", node)
const object = self.requireIterator(iterator, node)
return {
iterator: object,
next: self.requireIteratorMethod(object.next, "Iterator next", node),
next:
object instanceof CodeModeGenerator
? new GeneratorMethodReference(object, "next")
: self.requireIteratorMethod(object.next, "Iterator next", node),
asynchronous: asyncMethod !== undefined && asyncMethod !== null,
}
},
)
}
private nextIteratorResult(iterator: CustomIterator, node: AstNode) {
private nextIteratorResult(iterator: CustomIterator, node: AstNode, awaiting: boolean) {
const self = this
return Effect.gen(function* () {
if (iterator.asynchronous) {
@@ -775,7 +875,7 @@ export class Interpreter<R> {
const called = yield* Effect.exit(self.invokeCallable(iterator.next, [], node))
if (!Exit.isSuccess(called)) {
yield* Effect.yieldNow
if (awaiting) yield* Effect.yieldNow
return yield* Effect.failCause(called.cause)
}
const captured = yield* Effect.exit(
@@ -785,16 +885,24 @@ export class Interpreter<R> {
}),
)
if (!Exit.isSuccess(captured)) {
yield* Effect.yieldNow
if (awaiting) yield* Effect.yieldNow
return yield* Effect.failCause(captured.cause)
}
return { done: captured.value.done, value: yield* self.awaitValue(captured.value.value) }
return {
done: captured.value.done,
value: awaiting
? yield* self.awaitAsyncFromSyncValue(iterator, captured.value.value, node, !captured.value.done)
: captured.value.value,
}
})
}
private closeIterator(iterator: CustomIterator, node: AstNode): Effect.Effect<void, unknown, R> {
const close = iterator.iterator.return
if (close === undefined || close === null) return iterator.asynchronous ? Effect.void : Effect.yieldNow
private closeIterator(iterator: CustomIterator, node: AstNode, awaiting = true): Effect.Effect<void, unknown, R> {
const close =
iterator.iterator instanceof CodeModeGenerator
? new GeneratorMethodReference(iterator.iterator, "return")
: iterator.iterator.return
if (close === undefined || close === null) return iterator.asynchronous || !awaiting ? Effect.void : Effect.yieldNow
const self = this
return Effect.gen(function* () {
const method = self.requireIteratorMethod(close, "Iterator return", node)
@@ -809,17 +917,17 @@ export class Interpreter<R> {
const called = yield* Effect.exit(self.invokeCallable(method, [], node))
if (!Exit.isSuccess(called)) {
yield* Effect.yieldNow
if (awaiting) yield* Effect.yieldNow
return yield* Effect.failCause(called.cause)
}
const captured = yield* Effect.exit(
Effect.sync(() => self.requireIteratorObject(called.value, "Iterator return() result", node).value),
)
if (!Exit.isSuccess(captured)) {
yield* Effect.yieldNow
if (awaiting) yield* Effect.yieldNow
return yield* Effect.failCause(captured.cause)
}
yield* self.awaitValue(captured.value)
if (awaiting) yield* self.awaitValue(captured.value)
})
}
@@ -828,6 +936,12 @@ export class Interpreter<R> {
throw new InterpreterRuntimeError(`${context} must be an object.`, node).as("TypeError")
}
private requireIterator(value: unknown, node: AstNode): SafeObject | CodeModeGenerator {
return value instanceof CodeModeGenerator
? value
: this.requireIteratorObject(value, "Iterator method result", node)
}
private requireIteratorMethod(value: unknown, context: string, node: AstNode): unknown {
if (typeofValue(value) === "function") return value
throw new InterpreterRuntimeError(`${context} must be a function.`, node).as("TypeError")
@@ -966,7 +1080,7 @@ export class Interpreter<R> {
const attempted = Effect.matchCauseEffect(this.evaluateStatement(body), {
onFailure: (cause) => {
if (cause.reasons.some(Cause.isInterruptReason) || !handler) {
if (cause.reasons.some(Cause.isInterruptReason) || Cause.squash(cause) instanceof GeneratorReturn || !handler) {
return Effect.failCause(cause)
}
@@ -1059,10 +1173,7 @@ export class Interpreter<R> {
for (const [key, item] of Object.entries(value as SafeObject)) {
if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item
}
for (const symbol of IteratorSymbols) {
if (!consumed.has(symbol) && Object.hasOwn(value, symbol))
Reflect.set(rest, symbol, Reflect.get(value, symbol))
}
copyIteratorSymbols(value, rest, consumed)
yield* self.declarePattern(getNode(property, "argument"), rest, mutable, property, initialize)
continue
}
@@ -1084,21 +1195,9 @@ export class Interpreter<R> {
}
if (pattern.type === "ArrayPattern") {
const items = spreadItems(value)
if (items === undefined) {
throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern)
}
for (const [index, item] of getArray(pattern, "elements").entries()) {
if (item === null) continue
const element = asNode(item, `elements[${index}]`)
if (element.type === "RestElement") {
yield* self.declarePattern(getNode(element, "argument"), items.slice(index), mutable, element, initialize)
break
}
yield* self.declarePattern(element, items[index], mutable, pattern, initialize)
}
return
return yield* self.destructureArrayPattern(pattern, value, (target, item, context) =>
self.declarePattern(target, item, mutable, context, initialize),
)
}
throw new InterpreterRuntimeError(`Unsupported binding pattern '${pattern.type}'.`, pattern)
@@ -1142,10 +1241,7 @@ export class Interpreter<R> {
for (const [key, item] of Object.entries(source)) {
if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item
}
for (const symbol of IteratorSymbols) {
if (!consumed.has(symbol) && Object.hasOwn(source, symbol))
Reflect.set(rest, symbol, Reflect.get(source, symbol))
}
copyIteratorSymbols(source, rest, consumed)
yield* self.assignPattern(getNode(property, "argument"), rest, property)
continue
}
@@ -1160,26 +1256,63 @@ export class Interpreter<R> {
}
if (pattern.type === "ArrayPattern") {
const items = spreadItems(value)
if (items === undefined) {
throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern)
}
for (const [index, item] of getArray(pattern, "elements").entries()) {
if (item === null) continue
const element = asNode(item, `elements[${index}]`)
if (element.type === "RestElement") {
yield* self.assignPattern(getNode(element, "argument"), items.slice(index), element)
break
}
yield* self.assignPattern(element, items[index], pattern)
}
return
return yield* self.destructureArrayPattern(pattern, value, (target, item, context) =>
self.assignPattern(target, item, context),
)
}
throw new InterpreterRuntimeError(`Unsupported assignment pattern '${pattern.type}'.`, node)
})
}
private destructureArrayPattern(
pattern: AstNode,
value: unknown,
consume: (target: AstNode, value: unknown, context: AstNode) => Effect.Effect<void, unknown, R>,
): Effect.Effect<void, unknown, R> {
const self = this
return Effect.gen(function* () {
const cursor = yield* self.syncIterator(value, pattern)
if (cursor === undefined) {
throw new InterpreterRuntimeError("Array destructuring requires a supported iterable value.", pattern).as(
"TypeError",
)
}
let done = false
for (const [index, item] of getArray(pattern, "elements").entries()) {
if (done) {
if (item === null) continue
const element = asNode(item, `elements[${index}]`)
yield* consume(
element.type === "RestElement" ? getNode(element, "argument") : element,
element.type === "RestElement" ? [] : undefined,
element,
)
if (element.type === "RestElement") return
continue
}
const step = yield* cursor.next
done = step.done
if (item === null) continue
const element = asNode(item, `elements[${index}]`)
if (element.type === "RestElement") {
const rest: Array<unknown> = []
if (!step.done) rest.push(step.value)
while (!done) {
const next = yield* cursor.next
done = next.done
if (!done) rest.push(next.value)
}
yield* consume(getNode(element, "argument"), rest, element)
return
}
const consumed = consume(element, step.done ? undefined : step.value, pattern)
yield* step.done ? consumed : preserveConsumerError(cursor, consumed)
}
if (!done) yield* cursor.close
})
}
private destructuringPropertyKey(property: AstNode): Effect.Effect<PropertyKey, unknown, R> {
if (property.type !== "Property" || getString(property, "kind") !== "init") {
throw new InterpreterRuntimeError("Unsupported object destructuring property.", property)
@@ -1259,6 +1392,8 @@ export class Interpreter<R> {
value instanceof CodeModePromise ? self.settlePromise(value) : Effect.as(Effect.yieldNow, value),
)
}
case "YieldExpression":
return this.evaluateYieldExpression(node)
case "NewExpression":
return this.evaluateNewExpression(node)
default:
@@ -1280,7 +1415,11 @@ export class Interpreter<R> {
)
}
if (errorConstructors.has(name)) {
return Effect.map(this.evaluateCallArguments(argNodes), (args) => constructErrorValue(name, args, node))
return Effect.flatMap(this.evaluateCallArguments(argNodes), (args) =>
name === "AggregateError"
? constructAggregateErrorValue(self.runner, args, node)
: Effect.succeed(constructErrorValue(name, args)),
)
}
// Array and Object construct identically with or without new, like JS.
if (name === "Array") {
@@ -1298,13 +1437,13 @@ export class Interpreter<R> {
case "RegExp":
return self.constructRegExp(args, node)
case "Map":
return self.constructMap(args[0], node)
return yield* self.constructMap(args[0], node)
case "Set":
return self.constructSet(args[0], node)
return yield* self.constructSet(args[0], node)
case "URL":
return self.constructURL(args, node)
default:
return self.constructURLSearchParams(args[0], node)
return yield* self.constructURLSearchParams(args[0], node)
}
})
}
@@ -1390,44 +1529,53 @@ export class Interpreter<R> {
}
}
private constructMap(init: unknown, node: AstNode): CodeModeMap {
private constructMap(init: unknown, node: AstNode): Effect.Effect<CodeModeMap, unknown, R> {
const target = new CodeModeMap()
if (init === undefined || init === null) return target
const entries = Array.isArray(init)
? init
: init instanceof CodeModeMap
? Array.from(init.map.entries(), ([key, item]): Array<unknown> => [key, item])
: undefined
if (entries === undefined) {
throw new InterpreterRuntimeError(
"new Map(...) expects an array of [key, value] pairs, a Map, or no argument.",
node,
)
}
for (const pair of entries) {
if (!Array.isArray(pair)) {
throw new InterpreterRuntimeError("new Map(...) expects [key, value] pairs.", node)
if (init === undefined || init === null) return Effect.succeed(target)
const self = this
return Effect.gen(function* () {
const cursor = yield* self.syncIterator(init, node)
if (cursor === undefined) {
throw new InterpreterRuntimeError(
"new Map(...) expects an iterable of [key, value] pairs or no argument.",
node,
).as("TypeError")
}
target.map.set(pair[0], pair[1])
}
return target
while (true) {
const step = yield* cursor.next
if (step.done) return target
yield* preserveConsumerError(
cursor,
Effect.sync(() => {
if (!isRecord(step.value) || isRuntimeReference(step.value)) {
throw new InterpreterRuntimeError("new Map(...) expects [key, value] pairs as entry objects.", node).as(
"TypeError",
)
}
target.map.set(step.value[0], step.value[1])
}),
)
}
})
}
private constructSet(init: unknown, node: AstNode): CodeModeSet {
private constructSet(init: unknown, node: AstNode): Effect.Effect<CodeModeSet, unknown, R> {
const target = new CodeModeSet()
if (init === undefined || init === null) return target
const items = Array.isArray(init)
? init
: init instanceof CodeModeSet
? Array.from(init.set.values())
: typeof init === "string"
? Array.from(init)
: undefined
if (items === undefined) {
throw new InterpreterRuntimeError("new Set(...) expects an array, Set, string, or no argument.", node)
}
for (const item of items) target.set.add(item)
return target
if (init === undefined || init === null) return Effect.succeed(target)
const self = this
return Effect.gen(function* () {
const cursor = yield* self.syncIterator(init, node)
if (cursor === undefined) {
throw new InterpreterRuntimeError("new Set(...) expects a synchronous iterable or no argument.", node).as(
"TypeError",
)
}
while (true) {
const step = yield* cursor.next
if (step.done) return target
target.set.add(step.value)
}
})
}
private constructURL(args: Array<unknown>, node: AstNode): CodeModeURL {
@@ -1448,47 +1596,79 @@ export class Interpreter<R> {
}
}
private constructURLSearchParams(init: unknown, node: AstNode): CodeModeURLSearchParams {
if (init === undefined) return new CodeModeURLSearchParams(new URLSearchParams())
private constructURLSearchParams(init: unknown, node: AstNode): Effect.Effect<CodeModeURLSearchParams, unknown, R> {
if (init === undefined) return Effect.succeed(new CodeModeURLSearchParams(new URLSearchParams()))
if (init instanceof CodeModeURLSearchParams) {
return new CodeModeURLSearchParams(new URLSearchParams(init.params))
return Effect.succeed(new CodeModeURLSearchParams(new URLSearchParams(init.params)))
}
if (typeof init === "string") return new CodeModeURLSearchParams(new URLSearchParams(init))
if (typeof init === "string") return Effect.succeed(new CodeModeURLSearchParams(new URLSearchParams(init)))
if (init === null || typeof init === "number" || typeof init === "boolean") {
return new CodeModeURLSearchParams(new URLSearchParams(coerceToString(init)))
return Effect.succeed(new CodeModeURLSearchParams(new URLSearchParams(coerceToString(init))))
}
if (init instanceof CodeModeMap) {
return this.constructURLSearchParams(
Array.from(init.map.entries(), ([key, value]) => [key, value]),
node,
)
}
if (Array.isArray(init)) {
const entries = init.map((pair) => {
if (!Array.isArray(pair) || pair.length !== 2) {
throw new InterpreterRuntimeError(
"new URLSearchParams(...) expects an array of [name, value] pairs.",
node,
).as("TypeError")
const self = this
return Effect.gen(function* () {
const cursor = yield* self.syncIterator(init, node)
if (cursor !== undefined) {
const entries: Array<Array<string>> = []
while (true) {
const step = yield* cursor.next
if (step.done) {
if (entries.some((entry) => entry.length !== 2)) {
throw new InterpreterRuntimeError(
"new URLSearchParams(...) expects iterable [name, value] pairs.",
node,
).as("TypeError")
}
return new CodeModeURLSearchParams(
new URLSearchParams(entries.map((entry): [string, string] => [entry[0] ?? "", entry[1] ?? ""])),
)
}
entries.push(yield* preserveConsumerError(cursor, self.readURLSearchParamsPair(step.value, node)))
}
return [uriArgument(pair[0], "URLSearchParams name"), uriArgument(pair[1], "URLSearchParams value")] as [
string,
string,
]
})
return new CodeModeURLSearchParams(new URLSearchParams(entries))
}
if (isCodeModeValue(init)) return new CodeModeURLSearchParams(new URLSearchParams())
const data = boundedData(init, "new URLSearchParams input")
if (data === null || typeof data !== "object") {
throw new InterpreterRuntimeError(
"new URLSearchParams(...) expects a query string, data object, array of pairs, or URLSearchParams.",
node,
).as("TypeError")
}
return new CodeModeURLSearchParams(
new URLSearchParams(Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)]))),
)
}
if (isRuntimeReference(init)) {
throw new InterpreterRuntimeError(
"new URLSearchParams(...) expects a query string, data object, or synchronous iterable pairs.",
node,
).as("TypeError")
}
if (isCodeModeValue(init)) return new CodeModeURLSearchParams(new URLSearchParams())
const data = boundedData(init, "new URLSearchParams input")
if (data === null || typeof data !== "object") {
throw new InterpreterRuntimeError(
"new URLSearchParams(...) expects a query string, data object, iterable pairs, or URLSearchParams.",
node,
).as("TypeError")
}
return new CodeModeURLSearchParams(
new URLSearchParams(
Object.fromEntries(Object.entries(data).map(([key, value]) => [key, coerceToString(value)])),
),
)
})
}
private readURLSearchParamsPair(value: unknown, node: AstNode): Effect.Effect<Array<string>, unknown, R> {
const self = this
return Effect.gen(function* () {
const cursor = yield* self.syncIterator(value, node)
if (cursor === undefined) {
throw new InterpreterRuntimeError("new URLSearchParams(...) expects iterable [name, value] pairs.", node).as(
"TypeError",
)
}
const items: Array<string> = []
while (true) {
const step = yield* cursor.next
if (step.done) return items
items.push(
yield* preserveConsumerError(
cursor,
Effect.sync(() => uriArgument(step.value, "URLSearchParams pair value")),
),
)
}
})
}
private evaluateBinaryExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
@@ -1503,6 +1683,8 @@ export class Interpreter<R> {
}
private applyBinaryOperator(operator: string, lhs: unknown, rhs: unknown, node: AstNode): unknown {
if (operator === "===") return lhs === rhs
if (operator === "!==") return lhs !== rhs
if (containsOpaqueReference(lhs) || containsOpaqueReference(rhs)) {
throw new InterpreterRuntimeError("Binary operators require data values.", node, "InvalidDataValue")
}
@@ -1532,12 +1714,8 @@ export class Interpreter<R> {
return (l as number) ** (r as number)
case "==":
return bothObjects ? lhs === rhs : l == r
case "===":
return lhs === rhs
case "!=":
return bothObjects ? lhs !== rhs : l != r
case "!==":
return lhs !== rhs
case "<":
return (l as string) < (r as string)
case "<=":
@@ -1773,6 +1951,11 @@ export class Interpreter<R> {
if (callable instanceof CodeModeFunction) {
return yield* self.invokeFunction(callable, args)
}
if (callable instanceof GeneratorMethodReference) {
if (callable.kind === "iterator") return callable.generator
const requested = callable.generator.request(callable.kind, args[0], node) as Effect.Effect<unknown, unknown, R>
return callable.generator.asynchronous ? yield* self.createPromise(requested) : yield* requested
}
if (callable instanceof IntrinsicReference) {
return yield* invokeIntrinsic(self.runner, callable, args, node)
}
@@ -1782,6 +1965,7 @@ export class Interpreter<R> {
return self.invokeObjectMethodOnTools(callable.name, args[0], node)
}
if (callable.namespace === "Object" && objectMethodsPreservingIdentity.has(callable.name)) {
if (callable.name === "fromEntries") return yield* invokeObjectFromEntries(self.runner, args[0], node)
return invokeGlobalMethod(callable, args, node)
}
if (callable.namespace === "Array" && callable.name === "from") {
@@ -1790,6 +1974,9 @@ export class Interpreter<R> {
if ((callable.namespace === "Object" || callable.namespace === "Map") && callable.name === "groupBy") {
return yield* invokeGroupBy(self.runner, callable.namespace, args, node)
}
if (callable.namespace === "Math" && callable.name === "sumPrecise") {
return yield* invokeMathSumPrecise(self.runner, args[0], node)
}
if (callable.namespace === "Array" && callable.name === "of") {
return invokeGlobalMethod(callable, args, node)
}
@@ -1808,7 +1995,8 @@ export class Interpreter<R> {
return yield* self.invokeSearch(args)
}
if (callable instanceof ErrorConstructorReference) {
return constructErrorValue(callable.name, args, node)
if (callable.name === "AggregateError") return yield* constructAggregateErrorValue(self.runner, args, node)
return constructErrorValue(callable.name, args)
}
if (callable instanceof GlobalNamespace) {
// Real JS permits calling Array, Object, Date, and RegExp without new.
@@ -1868,10 +2056,16 @@ export class Interpreter<R> {
const argNode = asNode(arg, `arguments[${index}]`)
if (argNode.type === "SpreadElement") {
const spread = yield* self.evaluateExpression(getNode(argNode, "argument"))
const items = spreadItems(spread)
if (items === undefined)
throw new InterpreterRuntimeError("Spread arguments require an array, string, Map, or Set.", argNode)
args.push(...items)
const cursor = yield* self.syncIterator(spread, argNode)
if (cursor === undefined)
throw new InterpreterRuntimeError("Spread arguments require a synchronous iterable.", argNode).as(
"TypeError",
)
while (true) {
const step = yield* cursor.next
if (step.done) break
args.push(step.value)
}
} else {
args.push(yield* self.evaluateExpression(argNode))
}
@@ -1906,6 +2100,7 @@ export class Interpreter<R> {
return yield* invocation.evaluateExpression(fn.body)
})
if (fn.generator) return Effect.succeed(this.createGenerator(invocation, run, fn.async))
if (!fn.async) return run
// The initial yield assigns `box.own` before the body can self-resolve.
const box: { own?: CodeModePromise } = {}
@@ -1924,6 +2119,250 @@ export class Interpreter<R> {
)
}
private createGenerator(
invocation: Interpreter<R>,
run: Effect.Effect<unknown, unknown, R>,
asynchronous: boolean,
): CodeModeGenerator {
const state: GeneratorState = { started: false, completed: false, draining: false, pending: [], pendingIndex: 0 }
invocation.generatorState = state
invocation.generatorAsync = asynchronous
const generator = new CodeModeGenerator(asynchronous, (kind, value, node) => {
const request = { kind, value, response: Deferred.makeUnsafe<unknown, unknown>() }
if (!asynchronous && state.active) {
return Effect.fail(new InterpreterRuntimeError("Generator is already running.", node).as("TypeError"))
}
if (asynchronous && (state.completed || (!state.started && kind !== "next"))) {
state.started = true
state.completed = true
state.pending.push(request)
if (state.draining) return Deferred.await(request.response)
state.draining = true
return Effect.andThen(
this.promises.fork(
invocation
.completeGeneratorRequests(state, true)
.pipe(Effect.ensuring(Effect.sync(() => (state.draining = false)))),
),
Deferred.await(request.response),
)
}
if (state.completed) {
if (kind === "throw") return Effect.fail(new ProgramThrow(value))
return Effect.succeed({ value: kind === "return" ? value : undefined, done: true })
}
if (!state.started && kind !== "next") {
state.completed = true
if (kind === "throw") return Effect.fail(new ProgramThrow(value))
return Effect.succeed({ value, done: true })
}
state.pending.push(request)
if (state.available) {
const available = state.available
state.available = undefined
Deferred.doneUnsafe(available, Exit.succeed(undefined))
}
if (!state.started) {
state.started = true
const body = Effect.gen(function* () {
state.active = yield* invocation.takeGeneratorRequest(state)
const exit = yield* Effect.exit(
run.pipe(
Effect.flatMap((result) => (asynchronous ? invocation.awaitValue(result) : Effect.succeed(result))),
Effect.catch((error) =>
error instanceof GeneratorReturn
? asynchronous
? invocation.awaitValue(error.value)
: Effect.succeed(error.value)
: Effect.fail(error),
),
),
)
const active = state.active
state.active = undefined
if (active) {
Deferred.doneUnsafe(
active.response,
Exit.isSuccess(exit) ? Exit.succeed({ value: exit.value, done: true }) : exit,
)
}
yield* invocation.completeGeneratorRequests(state, asynchronous)
state.completed = true
})
return Effect.andThen(this.promises.fork(body), Deferred.await(request.response))
}
return Deferred.await(request.response)
})
return generator
}
private completeGeneratorRequests(state: GeneratorState, asynchronous: boolean): Effect.Effect<void, never, R> {
const self = this
return Effect.gen(function* () {
while (true) {
const pending = self.dequeueGeneratorRequest(state)
if (!pending) return
if (pending.kind === "throw") {
Deferred.doneUnsafe(pending.response, Exit.fail(new ProgramThrow(pending.value)))
continue
}
if (asynchronous && pending.kind === "return") {
const resolved = yield* Effect.exit(self.awaitValue(pending.value))
Deferred.doneUnsafe(
pending.response,
Exit.isSuccess(resolved) ? Exit.succeed({ value: resolved.value, done: true }) : resolved,
)
continue
}
Deferred.doneUnsafe(
pending.response,
Exit.succeed({ value: pending.kind === "return" ? pending.value : undefined, done: true }),
)
}
})
}
private takeGeneratorRequest(state: GeneratorState): Effect.Effect<GeneratorRequest> {
const next = this.dequeueGeneratorRequest(state)
if (next) return Effect.succeed(next)
state.available = Deferred.makeUnsafe<void>()
return Effect.andThen(
Deferred.await(state.available),
Effect.sync(() => this.dequeueGeneratorRequest(state)!),
)
}
private dequeueGeneratorRequest(state: GeneratorState): GeneratorRequest | undefined {
const request = state.pending[state.pendingIndex]
if (!request) return undefined
state.pendingIndex += 1
if (state.pendingIndex === state.pending.length) {
state.pending = []
state.pendingIndex = 0
}
return request
}
private evaluateYieldExpression(node: AstNode): Effect.Effect<unknown, unknown, R> {
const argument = getOptionalNode(node, "argument")
const self = this
return Effect.gen(function* () {
if (!self.generatorState) throw new InterpreterRuntimeError("yield is only valid inside a generator.", node)
if (node.delegate === true) {
const value = argument ? yield* self.evaluateExpression(argument) : undefined
return yield* self.delegateYield(value, node)
}
const value = argument ? yield* self.evaluateExpression(argument) : undefined
const yielded = self.generatorAsync ? yield* self.awaitValue(value) : value
return yield* self.suspendGenerator(yielded, node)
})
}
private suspendGenerator(value: unknown, node: AstNode): Effect.Effect<unknown, unknown, R> {
const state = this.generatorState
if (!state?.active) throw new InterpreterRuntimeError("Generator has no active request.", node)
Deferred.doneUnsafe(state.active.response, Exit.succeed({ value, done: false }))
state.active = undefined
return Effect.flatMap(this.takeGeneratorRequest(state), (request) => {
state.active = request
if (request.kind === "next") return Effect.succeed(request.value)
if (request.kind === "throw") return Effect.fail(new ProgramThrow(request.value))
return this.generatorAsync
? Effect.flatMap(this.awaitValue(request.value), (value) => Effect.fail(new GeneratorReturn(value)))
: Effect.fail(new GeneratorReturn(request.value))
})
}
private delegateYield(value: unknown, node: AstNode): Effect.Effect<unknown, unknown, R> {
const self = this
return Effect.gen(function* () {
if (
Array.isArray(value) ||
typeof value === "string" ||
value instanceof CodeModeMap ||
value instanceof CodeModeSet ||
value instanceof CodeModeURLSearchParams
) {
const cursor = yield* self.syncIterator(value, node)
if (!cursor) throw new InterpreterRuntimeError("Built-in iterator is unavailable.", node)
while (true) {
const step = yield* cursor.next
if (step.done) return undefined
const resumed = yield* Effect.exit(
self.suspendGenerator(self.generatorAsync ? yield* self.awaitValue(step.value) : step.value, node),
)
if (Exit.isSuccess(resumed)) continue
const error = Cause.squash(resumed.cause)
if (error instanceof GeneratorReturn) {
yield* cursor.close
return yield* Effect.fail(error)
}
if (error instanceof ProgramThrow) {
yield* cursor.close
throw new InterpreterRuntimeError("The delegated iterator does not provide a throw() method.", node).as(
"TypeError",
)
}
return yield* Effect.failCause(resumed.cause)
}
}
const iterator = yield* self.customIterator(value, node, self.generatorAsync)
if (!iterator)
throw new InterpreterRuntimeError("yield* requires a compatible iterable value.", node).as("TypeError")
let kind: GeneratorRequestKind = "next"
let input: unknown = undefined
while (true) {
const method =
kind === "next"
? iterator.next
: iterator.iterator instanceof CodeModeGenerator
? new GeneratorMethodReference(iterator.iterator, kind)
: iterator.iterator[kind]
if (method === undefined || method === null) {
if (kind === "return") return yield* Effect.fail(new GeneratorReturn(input))
yield* self.closeIterator(iterator, node, self.generatorAsync)
throw new InterpreterRuntimeError("The delegated iterator does not provide a throw() method.", node).as(
"TypeError",
)
}
const called = yield* self.invokeCallable(
self.requireIteratorMethod(method, `Iterator ${kind}`, node),
[input],
node,
)
const result = self.requireIteratorObject(
iterator.asynchronous ? yield* self.awaitValue(called) : called,
`Iterator ${kind}() result`,
node,
)
const done = Boolean(result.done)
const resultValue: unknown =
self.generatorAsync && !iterator.asynchronous
? yield* self.awaitAsyncFromSyncValue(iterator, result.value, node, kind !== "return" && !done)
: result.value
if (done) {
if (kind === "return") return yield* Effect.fail(new GeneratorReturn(resultValue))
return resultValue
}
const resumed: Exit.Exit<unknown, unknown> = yield* Effect.exit(self.suspendGenerator(resultValue, node))
if (Exit.isSuccess(resumed)) {
kind = "next"
input = resumed.value
continue
}
const error: unknown = Cause.squash(resumed.cause)
if (!(error instanceof GeneratorReturn) && !(error instanceof ProgramThrow)) {
return yield* Effect.failCause(resumed.cause)
}
kind = error instanceof GeneratorReturn ? "return" : "throw"
input = error.value
}
})
}
private evaluateObjectExpression(node: AstNode): Effect.Effect<Record<string, unknown>, unknown, R> {
const objectValue: Record<string, unknown> = Object.create(null) as Record<string, unknown>
const properties = getArray(node, "properties")
@@ -1942,9 +2381,7 @@ export class Interpreter<R> {
if (isBlockedMember(key)) throw new InterpreterRuntimeError(`Property '${key}' is not available.`, property)
objectValue[key] = value
}
for (const symbol of IteratorSymbols) {
if (Object.hasOwn(spread, symbol)) Reflect.set(objectValue, symbol, Reflect.get(spread, symbol))
}
copyIteratorSymbols(spread, objectValue)
continue
}
@@ -1997,10 +2434,14 @@ export class Interpreter<R> {
const element = asNode(elementValue, "elements")
if (element.type === "SpreadElement") {
const spread = yield* self.evaluateExpression(getNode(element, "argument"))
const items = spreadItems(spread)
if (items === undefined)
throw new InterpreterRuntimeError("Array spread requires an array, string, Map, or Set.", element)
values.push(...items)
const cursor = yield* self.syncIterator(spread, element)
if (cursor === undefined)
throw new InterpreterRuntimeError("Array spread requires a synchronous iterable.", element).as("TypeError")
while (true) {
const step = yield* cursor.next
if (step.done) break
values.push(step.value)
}
} else {
values.push(yield* self.evaluateExpression(element))
}
@@ -2061,6 +2502,7 @@ export class Interpreter<R> {
| IntrinsicReference
| GlobalMethodReference
| JsonMethodReference
| GeneratorMethodReference
| ComputedValue
| typeof OptionalShortCircuit
| undefined,
@@ -2205,6 +2647,19 @@ export class Interpreter<R> {
)
}
if (objectValue instanceof CodeModeGenerator) {
if (key === "next" || key === "return" || key === "throw") {
return new GeneratorMethodReference(objectValue, key)
}
if (
(key === IteratorSymbol && !objectValue.asynchronous) ||
(key === AsyncIteratorSymbol && objectValue.asynchronous)
) {
return new GeneratorMethodReference(objectValue, "iterator")
}
return new ComputedValue(undefined)
}
if (isRuntimeReference(objectValue)) {
throw new InterpreterRuntimeError(
"Runtime references are opaque and do not expose properties.",
@@ -2241,16 +2696,7 @@ export class Interpreter<R> {
return Effect.map(this.getMemberReference(node), (reference) => {
if (reference === OptionalShortCircuit) return OptionalShortCircuit
if (reference instanceof ComputedValue) return reference.value
if (
reference === undefined ||
reference instanceof ToolReference ||
reference instanceof PromiseMethodReference ||
reference instanceof PromiseInstanceMethodReference ||
reference instanceof IntrinsicReference ||
reference instanceof GlobalMethodReference ||
reference instanceof JsonMethodReference
)
return reference
if (reference === undefined || isOpaqueMemberReference(reference)) return reference
if (Array.isArray(reference.target)) {
if (reference.key === "length") return reference.target.length
if (typeof reference.key === "string") return new IntrinsicReference(reference.target, reference.key)
@@ -2278,12 +2724,7 @@ export class Interpreter<R> {
if (
reference instanceof ComputedValue ||
reference === undefined ||
reference instanceof ToolReference ||
reference instanceof PromiseMethodReference ||
reference instanceof PromiseInstanceMethodReference ||
reference instanceof IntrinsicReference ||
reference instanceof GlobalMethodReference ||
reference instanceof JsonMethodReference ||
isOpaqueMemberReference(reference) ||
reference.target instanceof CodeModeURL
) {
throw new InterpreterRuntimeError("Only data fields may be deleted.", target, "InvalidDataValue")
@@ -2307,12 +2748,7 @@ export class Interpreter<R> {
reference === OptionalShortCircuit ||
reference instanceof ComputedValue ||
reference === undefined ||
reference instanceof ToolReference ||
reference instanceof PromiseMethodReference ||
reference instanceof PromiseInstanceMethodReference ||
reference instanceof IntrinsicReference ||
reference instanceof GlobalMethodReference ||
reference instanceof JsonMethodReference
isOpaqueMemberReference(reference)
) {
throw new InterpreterRuntimeError("Only data fields may be assigned.", node)
}
+28 -12
View File
@@ -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)
}),
)
}
})
+44 -32
View File
@@ -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]
}),
)
}
})
}
+1 -1
View File
@@ -553,7 +553,7 @@ export const prepare = <R>(tools: Tools<R>, catalogBudget = defaultCatalogBudget
"## Language",
"",
"Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.",
"Modules/imports, classes, generators, timers, fetch, eval, prototype access, and unlisted methods are unavailable. Use tools for external operations. Use await with try/catch.",
"Modules/imports, classes, timers, fetch, eval, prototype access, and unlisted methods are unavailable. Use tools for external operations. Use await with try/catch.",
"Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
]
+2 -1
View File
@@ -732,9 +732,10 @@ describe("CodeMode public contract", () => {
expect(instructions).toContain("not a general-purpose runtime")
expect(instructions).not.toContain("Standard modern JavaScript works")
expect(instructions).not.toContain("TypeScript type annotations")
for (const missing of ["Modules/imports", "classes", "generators", "fetch"]) {
for (const missing of ["Modules/imports", "classes", "fetch"]) {
expect(instructions).toContain(missing)
}
expect(instructions).not.toContain("generators")
expect(instructions).not.toContain("new Promise(...) are unavailable")
expect(instructions).not.toContain("promise chaining")
expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers")
@@ -0,0 +1,1271 @@
/*
* Portable portions adapted from Test262 at revision
* 250f204f23a9249ff204be2baec29600faae7b75. Exact source paths are cited
* beside the corresponding tests below.
*
* Copyright (C) 2013-2017 the V8 project authors. All rights reserved.
* Copyright (C) 2018 Valerie Young. All rights reserved.
* Copyright (C) 2020 Alexey Shvayka. All rights reserved.
* Copyright (C) 2022 Kevin Gibbons. All rights reserved.
* Copyright Ecma International. All rights reserved.
* Test262 portions are governed by the BSD license in LICENSE.test262.
*/
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { CodeMode } from "../src/index.js"
const execute = (code: string) => Effect.runPromise(CodeMode.execute({ code, tools: {} }))
const value = async (code: string) => {
const result = await execute(code)
if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
return result.value
}
describe("confined generators", () => {
// test/built-ins/GeneratorPrototype/next/return-yield-expr.js
test("is lazy and preserves next(value), nested suspension, return, and exhaustion", async () => {
expect(
await value(`
const events = []
function* generate() {
events.push("start")
const received = yield 1 + (yield 2)
return received
}
const iterator = generate()
const before = events.slice()
const first = iterator.next(99)
const second = iterator.next(3)
const third = iterator.next(7)
const fourth = iterator.next(8)
return [before, events, first, second, third, fourth]
`),
).toEqual([
[],
["start"],
{ value: 2, done: false },
{ value: 4, done: false },
{ value: 7, done: true },
{ value: null, done: true },
])
})
test("routes throw and return through catch and finally", async () => {
expect(
await value(`
function* generate() {
try {
try { yield "try" } catch (error) { yield "caught " + error }
} finally {
yield "finally"
}
}
const iterator = generate()
return [iterator.next(), iterator.throw("boom"), iterator.return("done"), iterator.next()]
`),
).toEqual([
{ value: "try", done: false },
{ value: "caught boom", done: false },
{ value: "finally", done: false },
{ value: "done", done: true },
])
})
test("throws into a suspended generator and after exhaustion", async () => {
expect(
await value(`
function* generate() { yield 1 }
const iterator = generate()
iterator.next()
let suspended
let exhausted
try { iterator.throw("first") } catch (error) { suspended = error }
try { iterator.throw("second") } catch (error) { exhausted = error }
return [suspended, exhausted, iterator.next()]
`),
).toEqual(["first", "second", { value: null, done: true }])
})
test("rejects synchronous generator reentry", async () => {
expect(
await value(`
let iterator
function* generate() {
try { iterator.next() } catch (error) { return error.name }
}
iterator = generate()
return iterator.next()
`),
).toEqual({ value: "TypeError", done: true })
})
test("delegates yield*, forwards next values, and receives the delegate return value", async () => {
expect(
await value(`
function* inner() {
const input = yield 1
return input * 2
}
function* outer() {
const result = yield* inner()
return result + 1
}
const iterator = outer()
return [iterator.next(), iterator.next(4)]
`),
).toEqual([
{ value: 1, done: false },
{ value: 9, done: true },
])
})
test("delegates throw and return to custom iterators", async () => {
expect(
await value(`
const calls = []
let step = 0
const delegate = {
[Symbol.iterator]: () => delegate,
next(...args) {
calls.push(["next", args.length, args[0]])
step += 1
return step === 1 ? { value: "one", done: false } : { value: "end", done: true }
},
throw(value) {
calls.push(["throw", value])
return { value: "recovered", done: false }
},
return(value) {
calls.push(["return", value])
return { value: value + "!", done: true }
},
}
function* generate() { return yield* delegate }
const iterator = generate()
const first = iterator.next()
const second = iterator.throw("x")
const third = iterator.return("stop")
return [first, second, third, calls]
`),
).toEqual([
{ value: "one", done: false },
{ value: "recovered", done: false },
{ value: "stop!", done: true },
[
["next", 1, null],
["throw", "x"],
["return", "stop"],
],
])
})
test("uses the missing-throw delegation path for built-in iterables", async () => {
expect(
await value(`
function* generate() { yield* [1, 2] }
const iterator = generate()
iterator.next()
try { iterator.throw("boom") } catch (error) { return error.name }
`),
).toBe("TypeError")
})
test("exposes only the appropriate iterator symbol and works in for...of", async () => {
expect(
await value(`
function* generate() { yield 1; yield 2 }
const iterator = generate()
const symbols = [iterator[Symbol.iterator]() === iterator, iterator[Symbol.asyncIterator]]
const values = []
for (const item of iterator) values.push(item)
return [symbols, values]
`),
).toEqual([
[true, null],
[1, 2],
])
})
test("accepts generator methods as iterator acquisition results", async () => {
expect(
await value(`
const sync = {
*[Symbol.iterator]() { yield 1; yield 2 },
}
const asynchronous = {
async *[Symbol.asyncIterator]() { yield 3; yield 4 },
}
const values = []
for (const item of sync) values.push(item)
for await (const item of asynchronous) values.push(item)
return values
`),
).toEqual([1, 2, 3, 4])
})
test("closes a generator when for...of exits abruptly", async () => {
expect(
await value(`
const events = []
function* generate() {
try { yield 1; yield 2 } finally { events.push("closed") }
}
for (const item of generate()) break
return events
`),
).toEqual(["closed"])
})
test("async generator requests are promises and execute in request order", async () => {
expect(
await value(`
const events = []
async function* generate() {
events.push("start")
const input = yield Promise.resolve(1)
events.push("received " + input)
return Promise.resolve(3)
}
const iterator = generate()
const first = iterator.next()
const second = iterator.next(2)
const third = iterator.next(4)
const promiseFlags = [first instanceof Promise, second instanceof Promise, third instanceof Promise]
return [promiseFlags, await Promise.all([first, second, third]), events]
`),
).toEqual([
[true, true, true],
[
{ value: 1, done: false },
{ value: 3, done: true },
{ value: null, done: true },
],
["start", "received 2"],
])
})
test("keeps requests queued while a completed generator adopts return values", async () => {
expect(
await value(`
const events = []
let resolve
const pending = new Promise((done) => { resolve = done })
async function* generate() { return 1 }
const iterator = generate()
const first = iterator.next()
const returned = iterator.return(pending)
const later = first.then(() => iterator.next()).then(() => events.push("later"))
returned.then(() => events.push("returned"))
await first
await Promise.resolve()
const before = events.slice()
resolve(9)
await Promise.all([returned, later])
return [before, events]
`),
).toEqual([[], ["returned", "later"]])
})
test("serializes requests made after async generator exhaustion", async () => {
expect(
await value(`
const events = []
let resolve
const pending = new Promise((done) => { resolve = done })
async function* generate() { return 1 }
const iterator = generate()
await iterator.next()
const returned = iterator.return(pending).then(() => events.push("returned"))
const later = iterator.next().then(() => events.push("later"))
await Promise.resolve()
const before = events.slice()
resolve(9)
await Promise.all([returned, later])
return [before, events]
`),
).toEqual([[], ["returned", "later"]])
})
test("async generators adopt yielded, returned, and return-request promises", async () => {
expect(
await value(`
async function* yielded() { yield Promise.resolve(1) }
async function* returned() { return Promise.resolve(2) }
async function* pending() { yield 0 }
const first = yielded()
const second = returned()
const third = pending()
const exhausted = returned()
await third.next()
await exhausted.next()
return await Promise.all([
first.next(),
second.next(),
third.return(Promise.resolve(3)),
exhausted.return(Promise.resolve(4)),
])
`),
).toEqual([
{ value: 1, done: false },
{ value: 2, done: true },
{ value: 3, done: true },
{ value: 4, done: true },
])
})
test("awaits return-request promises before injecting completion", async () => {
expect(
await value(`
const events = []
async function* generate() {
try {
yield 1
} catch (error) {
events.push("caught " + error)
yield "recovered"
} finally {
events.push("finally")
}
}
const iterator = generate()
const first = await iterator.next()
const returned = await iterator.return(Promise.reject("bad"))
const beforeNext = events.slice()
const last = await iterator.next()
return [first, returned, beforeNext, last, events]
`),
).toEqual([
{ value: 1, done: false },
{ value: "recovered", done: false },
["caught bad"],
{ value: null, done: true },
["caught bad", "finally"],
])
})
test("loop consumers call iterator next with no arguments", async () => {
expect(
await value(`
const calls = []
let syncStep = 0
const sync = {
[Symbol.iterator]: () => sync,
next(...args) {
calls.push(["sync", args.length])
syncStep += 1
return { value: syncStep, done: syncStep > 1 }
},
}
let asyncStep = 0
const asynchronous = {
[Symbol.asyncIterator]: () => asynchronous,
async next(...args) {
calls.push(["async", args.length])
asyncStep += 1
return { value: asyncStep, done: asyncStep > 1 }
},
}
for (const item of sync) {}
for await (const item of asynchronous) {}
return calls
`),
).toEqual([
["sync", 0],
["sync", 0],
["async", 0],
["async", 0],
])
})
test("supports async generators in for await...of and keeps them out of for...of", async () => {
expect(
await value(`
async function* generate() { yield 1; yield await Promise.resolve(2) }
const values = []
for await (const item of generate()) values.push(item)
let name
try { for (const item of generate()) {} } catch (error) { name = error.name }
return [values, name]
`),
).toEqual([[1, 2], "TypeError"])
})
test("keeps generator references opaque at the data boundary", async () => {
const result = await execute(`function* generate() { yield 1 } return generate()`)
expect(result.ok).toBe(false)
if (result.ok) return
expect(result.error.kind).toBe("InvalidDataValue")
})
// test/built-ins/GeneratorPrototype/return/from-state-suspended-start.js
// test/built-ins/GeneratorPrototype/throw/from-state-suspended-start.js
// test/built-ins/GeneratorPrototype/return/from-state-completed.js
// test/built-ins/GeneratorPrototype/throw/from-state-completed.js
test("honors sync return and throw in suspended-start and completed states", async () => {
expect(
await value(`
const events = []
function* generate() { events.push("body"); yield 1 }
const returned = generate()
const startReturn = returned.return(7)
const afterReturn = returned.next()
const thrown = generate()
let startThrow
try { thrown.throw("start") } catch (error) { startThrow = error }
const afterThrow = thrown.next()
let completedThrow
try { thrown.throw("completed") } catch (error) { completedThrow = error }
return [startReturn, afterReturn, startThrow, afterThrow, completedThrow, events]
`),
).toEqual([
{ value: 7, done: true },
{ value: null, done: true },
"start",
{ value: null, done: true },
"completed",
[],
])
})
// test/built-ins/AsyncGeneratorPrototype/return/return-suspendedStart-promise.js
// test/built-ins/AsyncGeneratorPrototype/throw/throw-suspendedStart.js
// test/built-ins/AsyncGeneratorPrototype/return/return-state-completed.js
// test/built-ins/AsyncGeneratorPrototype/throw/throw-state-completed.js
test("honors async return and throw in suspended-start and completed states", async () => {
expect(
await value(`
const events = []
async function* generate() { events.push("body"); yield 1 }
const returned = generate()
const startReturn = await returned.return(Promise.resolve(7))
const afterReturn = await returned.next()
const thrown = generate()
let startThrow
try { await thrown.throw("start") } catch (error) { startThrow = error }
const afterThrow = await thrown.next()
let completedThrow
try { await thrown.throw("completed") } catch (error) { completedThrow = error }
return [startReturn, afterReturn, startThrow, afterThrow, completedThrow, events]
`),
).toEqual([
{ value: 7, done: true },
{ value: null, done: true },
"start",
{ value: null, done: true },
"completed",
[],
])
})
// test/built-ins/AsyncGeneratorPrototype/return/return-suspendedYield-try-finally.js
// test/built-ins/AsyncGeneratorPrototype/return/return-suspendedYield-try-finally-return.js
// test/built-ins/AsyncGeneratorPrototype/throw/throw-suspendedYield-try-finally-throw.js
test("runs finally yields and lets finally completions override requests", async () => {
expect(
await value(`
async function* yielding() {
try { yield 1 } finally { yield 2 }
}
async function* returning() {
try { yield 1 } finally { return "override" }
}
async function* throwing() {
try { yield 1 } finally { throw "override" }
}
const first = yielding()
await first.next()
const finallyYield = await first.return("sent")
const preservedReturn = await first.next()
const second = returning()
await second.next()
const overriddenReturn = await second.return("sent")
const third = throwing()
await third.next()
let overriddenThrow
try { await third.throw("sent") } catch (error) { overriddenThrow = error }
return [finallyYield, preservedReturn, overriddenReturn, overriddenThrow]
`),
).toEqual([{ value: 2, done: false }, { value: "sent", done: true }, { value: "override", done: true }, "override"])
})
// test/language/statements/async-generator/yield-promise-reject-next-catch.js
// test/language/statements/async-generator/yield-promise-reject-next-yield-star-sync-iterator.js
test("rejects yielded promises and closes direct and sync-delegating async generators", async () => {
expect(
await value(`
async function* direct() { yield Promise.reject("direct") }
async function* delegated() { yield* [Promise.reject("delegated"), "unreachable"] }
const results = []
for (const iterator of [direct(), delegated()]) {
try { await iterator.next() } catch (error) { results.push(error) }
results.push(await iterator.next())
}
return results
`),
).toEqual(["direct", { value: null, done: true }, "delegated", { value: null, done: true }])
})
// test/built-ins/AsyncFromSyncIteratorPrototype/next/for-await-iterator-next-rejected-promise-close.js
// test/built-ins/AsyncFromSyncIteratorPrototype/next/yield-iterator-next-rejected-promise-close.js
// test/built-ins/AsyncFromSyncIteratorPrototype/throw/iterator-result-rejected-promise-close.js
test("closes sync iterators when async-from-sync values reject", async () => {
expect(
await value(`
const events = []
function* loopSource() {
try { yield Promise.reject("loop") } finally { events.push("loop close") }
}
let loopError
try { for await (const item of loopSource()) {} } catch (error) { loopError = error }
function* yieldSource() {
try { yield Promise.reject("yield") } finally { events.push("yield close") }
}
async function* delegate() { yield* yieldSource() }
let yieldError
try { await delegate().next() } catch (error) { yieldError = error }
const throwing = {
[Symbol.iterator]: () => throwing,
next: () => ({ value: 1, done: false }),
throw: () => ({ value: Promise.reject("throw"), done: false }),
return: () => { events.push("throw close"); return {} },
}
async function* throwDelegate() { yield* throwing }
const iterator = throwDelegate()
await iterator.next()
let throwError
try { await iterator.throw("sent") } catch (error) { throwError = error }
return [loopError, yieldError, throwError, events]
`),
).toEqual(["loop", "yield", "throw", ["loop close", "yield close", "throw close"]])
})
test("does not close rejected terminal or delegated return values", async () => {
expect(
await value(`
const events = []
const terminal = {
[Symbol.iterator]: () => terminal,
next: () => ({ value: Promise.reject("terminal"), done: true }),
return: () => { events.push("terminal close"); return {} },
}
let terminalError
try { for await (const item of terminal) {} } catch (error) { terminalError = error }
let returnCount = 0
const returned = {
[Symbol.iterator]: () => returned,
next: () => ({ value: 1, done: false }),
return: () => {
returnCount += 1
return { value: Promise.reject("return"), done: false }
},
}
async function* delegate() { yield* returned }
const iterator = delegate()
await iterator.next()
let returnError
try { await iterator.return("sent") } catch (error) { returnError = error }
return [terminalError, returnError, returnCount, events]
`),
).toEqual(["terminal", "return", 1, []])
})
test("serializes a mixed async next, throw, return, and next request queue", async () => {
expect(
await value(`
async function* generate() {
try {
try { yield 1; yield 2 } catch (error) { yield "caught " + error }
} finally {
yield "finally"
}
}
const iterator = generate()
const first = iterator.next()
const thrown = iterator.throw("x")
const returned = iterator.return("done")
const last = iterator.next()
return await Promise.all([first, thrown, returned, last])
`),
).toEqual([
{ value: 1, done: false },
{ value: "caught x", done: false },
{ value: "finally", done: false },
{ value: "done", done: true },
])
})
// test/built-ins/AsyncGeneratorPrototype/next/request-queue-order.js
// test/built-ins/AsyncGeneratorPrototype/throw/request-queue-order-state-executing.js
// test/built-ins/AsyncGeneratorPrototype/return/request-queue-order-state-executing.js
test("orders async requests enqueued while generators are executing", async () => {
expect(
await value(`
const events = []
let returned, returnRequest
async function* returnWhileExecuting() {
returnRequest = returned.return(42).then((result) => events.push(["return", result]))
yield 1
}
returned = returnWhileExecuting()
const firstReturn = returned.next().then((result) => events.push(["first return", result]))
await Promise.all([firstReturn, returnRequest])
let thrown, throwRequest
async function* throwWhileExecuting() {
throwRequest = thrown.throw("boom").catch((error) => events.push(["throw", error]))
yield 2
}
thrown = throwWhileExecuting()
const firstThrow = thrown.next().then((result) => events.push(["first throw", result]))
await Promise.all([firstThrow, throwRequest])
async function* queued() { yield "first"; yield "second" }
const iterator = queued()
const first = iterator.next()
const second = iterator.next()
const third = iterator.next()
await Promise.all([
third.then(() => events.push("third")),
second.then(() => events.push("second")),
first.then(() => events.push("first")),
])
return events
`),
).toEqual([
["first return", { value: 1, done: false }],
["return", { value: 42, done: true }],
["first throw", { value: 2, done: false }],
["throw", "boom"],
"first",
"second",
"third",
])
})
// test/language/statements/async-generator/yield-star-promise-not-unwrapped.js
// test/language/statements/async-generator/yield-star-sync-next.js
test("preserves async iterator promise values but unwraps async-from-sync values", async () => {
expect(
await value(`
const asyncValue = Promise.resolve("async")
const asynchronous = {
[Symbol.asyncIterator]: () => asynchronous,
next: () => ({ value: asyncValue, done: false }),
}
const syncValue = Promise.resolve("sync")
const synchronous = {
[Symbol.iterator]: () => synchronous,
next: () => ({ value: syncValue, done: false }),
}
async function* delegate(value) { yield* value }
const asyncResult = await delegate(asynchronous).next()
const syncResult = await delegate(synchronous).next()
return [asyncResult.value === asyncValue, await asyncResult.value, syncResult]
`),
).toEqual([true, "async", { value: "sync", done: false }])
})
test("captures delegated result done before awaiting async-from-sync values", async () => {
expect(
await value(`
const result = { value: Promise.resolve(1), done: false }
result.value.then(() => { result.done = true })
const source = {
[Symbol.iterator]: () => source,
next: () => result,
}
async function* delegate() { yield* source }
return await delegate().next()
`),
).toEqual({ value: 1, done: false })
})
// test/language/statements/async-generator/yield-star-async-next.js
// test/language/statements/async-generator/yield-star-sync-return.js
// test/language/statements/async-generator/yield-star-async-throw.js
test("forwards next, return, and throw through sync and async yield delegates", async () => {
expect(
await value(`
const calls = []
const make = (symbol, label) => {
let step = 0
const iterator = {
[symbol]: () => iterator,
next(...args) {
calls.push([label, "next", args.length, args[0]])
step += 1
return { value: step, done: false }
},
return(value) {
calls.push([label, "return", value])
return { value: value + "!", done: true }
},
throw(value) {
calls.push([label, "throw", value])
return { value: "caught " + value, done: false }
},
}
return iterator
}
async function* delegate(iterator) { return yield* iterator }
const sync = delegate(make(Symbol.iterator, "sync"))
const asynchronous = delegate(make(Symbol.asyncIterator, "async"))
const results = [await sync.next(9), await sync.next(2), await sync.throw("x"), await sync.return("stop")]
results.push(await asynchronous.next(9), await asynchronous.next(3), await asynchronous.throw("y"))
return [results, calls]
`),
).toEqual([
[
{ value: 1, done: false },
{ value: 2, done: false },
{ value: "caught x", done: false },
{ value: "stop!", done: true },
{ value: 1, done: false },
{ value: 2, done: false },
{ value: "caught y", done: false },
],
[
["sync", "next", 1, null],
["sync", "next", 1, 2],
["sync", "throw", "x"],
["sync", "return", "stop"],
["async", "next", 1, null],
["async", "next", 1, 3],
["async", "throw", "y"],
],
])
})
// test/language/statements/async-generator/yield-star-sync-return.js
// test/language/statements/async-generator/yield-star-async-throw.js
test("continues delegation when return and throw report done false", async () => {
expect(
await value(`
let returns = 0
const synchronous = {
[Symbol.iterator]: () => synchronous,
next: () => ({ value: "next", done: false }),
return(value) {
returns += 1
return { value: returns === 1 ? "return pending" : value, done: returns > 1 }
},
}
let throws = 0
const asynchronous = {
[Symbol.asyncIterator]: () => asynchronous,
next: () => ({ value: "next", done: false }),
throw(value) {
throws += 1
return { value: throws === 1 ? "throw pending" : value, done: throws > 1 }
},
}
async function* delegate(iterator) { return yield* iterator }
const returned = delegate(synchronous)
const thrown = delegate(asynchronous)
return [
await returned.next(),
await returned.return("first"),
await returned.return("second"),
await thrown.next(),
await thrown.throw("first"),
await thrown.throw("second"),
]
`),
).toEqual([
{ value: "next", done: false },
{ value: "return pending", done: false },
{ value: "second", done: true },
{ value: "next", done: false },
{ value: "throw pending", done: false },
{ value: "second", done: true },
])
})
// test/language/expressions/yield/star-rhs-iter-nrml-next-call-non-obj.js
// test/language/expressions/yield/star-rhs-iter-thrw-thrw-call-non-obj.js
// test/language/expressions/yield/star-rhs-iter-rtrn-rtrn-call-non-obj.js
// test/language/statements/async-generator/yield-star-next-not-callable-number-throw.js
test("rejects malformed yield delegate methods and iterator results", async () => {
expect(
await value(`
const run = (iterator, operation) => {
function* generate() {
try { yield* iterator } catch (error) { return error.name }
}
const value = generate()
const first = value.next()
return operation === "next" ? first : value[operation]()
}
const iterable = (fields) => ({ [Symbol.iterator]: () => fields })
const nextResult = run(iterable({ next: () => 1 }), "next")
const throwResult = run(iterable({ next: () => ({ done: false }), throw: () => 1 }), "throw")
const returnResult = run(iterable({ next: () => ({ done: false }), return: () => 1 }), "return")
const badAsync = {
[Symbol.asyncIterator]: () => ({ next: 1 }),
}
async function* asynchronous() {
try { yield* badAsync } catch (error) { return error.name }
}
return [nextResult, throwResult, returnResult, await asynchronous().next()]
`),
).toEqual([
{ value: "TypeError", done: true },
{ value: "TypeError", done: true },
{ value: "TypeError", done: true },
{ value: "TypeError", done: true },
])
})
// test/language/expressions/yield/captured-free-vars.js
// test/language/statements/generators/dflt-params-ref-prior.js
// test/language/expressions/generators/dflt-params-ref-prior.js
// test/language/expressions/object/method-definition/generator-no-yield.js
test("supports declaration, expression, and method forms with closures and parameters", async () => {
expect(
await value(`
const captured = 4
function* declaration(x, y = x, ...rest) { yield captured + y + rest[0] }
const expression = function* (x, y = x) { yield captured + y }
const object = { *method({ value }, extra = 1) { return captured + value + extra } }
return [declaration(2, undefined, 3).next(), expression(5).next(), object.method({ value: 6 }).next()]
`),
).toEqual([
{ value: 9, done: false },
{ value: 9, done: false },
{ value: 11, done: true },
])
})
// test/language/expressions/assignment/dstr/array-elem-iter-nrml-close.js
test("steps holes and rest and closes array binding and assignment patterns early", async () => {
expect(
await value(`
const events = []
function* binding() {
try { events.push("b1"); yield 1; events.push("b2"); yield 2; events.push("b3"); yield 3; yield 4 }
finally { events.push("binding close") }
}
const [first, , third] = binding()
function* assignment() {
try { yield 5; yield 6; yield 7 }
finally { events.push("assignment close") }
}
let head, rest
;[head, ...rest] = assignment()
return [first, third, head, rest, events]
`),
).toEqual([1, 3, 5, [6, 7], ["b1", "b2", "b3", "binding close", "assignment close"]])
})
// test/language/statements/variable/dstr/ary-ptrn-elem-id-init-throws.js
// test/language/expressions/assignment/dstr/array-elem-iter-thrw-close-err.js
test("closes on destructuring defaults and preserves the binding error over return failure", async () => {
expect(
await value(`
const events = []
const iterator = {
[Symbol.iterator]: () => iterator,
next: () => ({ value: undefined, done: false }),
return: () => { events.push("close"); throw "close error" },
}
let caught
try {
const [value = (() => { throw "binding error" })()] = iterator
} catch (error) { caught = error }
return [caught, events]
`),
).toEqual(["binding error", ["close"]])
})
test("does not close an exhausted iterator when a destructuring default fails", async () => {
expect(
await value(`
const events = []
const iterator = {
[Symbol.iterator]: () => iterator,
next: () => ({ done: true }),
return: () => { events.push("close"); return {} },
}
try { const [item = (() => { throw "default" })()] = iterator } catch {}
return events
`),
).toEqual([])
})
test("consumes generators in array and argument spread without awaiting yielded promises", async () => {
expect(
await value(`
function* values() { yield 1; yield Promise.resolve(2); yield 3 }
const array = [...values()]
const args = ((...items) => items)(...values())
return [array[0], array[1] instanceof Promise, await array[1], args[2]]
`),
).toEqual([1, true, 2, 3])
})
test("constructs Map, Set, and URLSearchParams from generators lazily", async () => {
expect(
await value(`
const events = []
function* pairs() { events.push(1); yield ["a", 1]; events.push(2); yield ["b", 2] }
function* values() { yield 1; yield 1; yield 2 }
const map = new Map(pairs())
const set = new Set(values())
const params = new URLSearchParams(pairs())
return [map.get("b"), [...set], params.toString(), events]
`),
).toEqual([2, [1, 2], "a=1&b=2", [1, 2, 1, 2]])
})
test("keeps built-in collection iteration live during callbacks", async () => {
expect(
await value(`
const map = new Map([[1, 1], [2, 2]])
const mapped = Array.from(map, (entry, index) => {
if (index === 0) map.set(3, 3)
return entry[0]
})
const set = new Set([1, 2])
const grouped = Map.groupBy(set, (item, index) => {
if (index === 0) set.add(3)
return "items"
})
return [mapped, grouped.get("items")]
`),
).toEqual([
[1, 2, 3],
[1, 2, 3],
])
})
test("keeps built-in collection iteration live in loops and yield delegation", async () => {
expect(
await value(`
const map = new Map([[1, 1], [2, 2]])
const mapValues = []
for (const [key] of map) {
mapValues.push(key)
if (key === 1) map.set(3, 3)
}
const set = new Set([1, 2])
const setValues = []
for await (const item of set) {
setValues.push(item)
if (item === 1) set.add(3)
}
const params = new URLSearchParams("a=1&b=2")
function* delegate() { yield* params }
const iterator = delegate()
const first = iterator.next()
params.append("c", "3")
return [mapValues, setValues, first, iterator.next(), iterator.next()]
`),
).toEqual([
[1, 2, 3],
[1, 2, 3],
{ value: ["a", "1"], done: false },
{ value: ["b", "2"], done: false },
{ value: ["c", "3"], done: false },
])
})
test("preserves async-from-sync turns for built-in loop completion and close", async () => {
expect(
await value(`
const completed = []
Promise.resolve().then(() => completed.push("reaction"))
for await (const item of []) {}
completed.push("after")
const closed = []
for await (const item of [1]) {
Promise.resolve().then(() => closed.push("reaction"))
break
}
closed.push("after")
return [completed, closed]
`),
).toEqual([
["reaction", "after"],
["reaction", "after"],
])
})
test("reads iterator return only when closing", async () => {
expect(
await value(`
const events = []
const iterator = {
[Symbol.iterator]: () => iterator,
next() {
iterator.return = () => { events.push("new"); return {} }
return { value: 1, done: false }
},
return: () => { events.push("old"); return {} },
}
const [item] = iterator
return [item, events]
`),
).toEqual([1, ["new"]])
})
test("accepts iterable URLSearchParams entry pairs", async () => {
expect(
await value(`
function* pair() { yield "a"; yield 1 }
function* entries() { yield pair() }
return new URLSearchParams(entries()).toString()
`),
).toBe("a=1")
})
test("converts URLSearchParams pair elements before requesting the next", async () => {
expect(
await value(`
const events = []
function* pair() {
try {
events.push("first")
yield (function* () {})()
events.push("second")
yield 2
} finally { events.push("pair close") }
}
function* entries() {
try { yield pair() } finally { events.push("outer close") }
}
let name
try { new URLSearchParams(entries()) } catch (error) { name = error.name }
return [events, name]
`),
).toEqual([["first", "pair close", "outer close"], "Error"])
})
test("validates URLSearchParams pair lengths after converting the outer sequence", async () => {
expect(
await value(`
const events = []
function* entries() {
try {
events.push("one")
yield ["a"]
events.push("two")
yield ["b", 2]
} finally { events.push("outer close") }
}
let name
try { new URLSearchParams(entries()) } catch (error) { name = error.name }
return [events, name]
`),
).toEqual([["one", "two", "outer close"], "TypeError"])
})
test("closes entry constructors when a generator yields a malformed entry", async () => {
expect(
await value(`
const events = []
function* mapEntries() { try { yield ["a", 1]; yield null } finally { events.push("map close") } }
function* parameterEntries() { try { yield ["a"]; yield ["b", 2] } finally { events.push("params close") } }
const names = []
try { new Map(mapEntries()) } catch (error) { names.push(error.name) }
try { new URLSearchParams(parameterEntries()) } catch (error) { names.push(error.name) }
return [names, events]
`),
).toEqual([
["TypeError", "TypeError"],
["map close", "params close"],
])
})
// test/built-ins/Array/from/iter-map-fn-args.js
// test/built-ins/Array/from/iter-map-fn-err.js
test("interleaves Array.from mapping and closes when its mapper fails", async () => {
expect(
await value(`
const events = []
function* source() {
try { events.push("next 1"); yield 1; events.push("next 2"); yield 2 }
finally { events.push("close") }
}
const mapped = Array.from(source(), (item) => { events.push("map " + item); return item * 2 })
let caught
try { Array.from(source(), (item) => { throw "mapper " + item }) } catch (error) { caught = error }
return [mapped, caught, events]
`),
).toEqual([[2, 4], "mapper 1", ["next 1", "map 1", "next 2", "map 2", "close", "next 1", "close"]])
})
test("reads array-like Array.from values immediately before mapping", async () => {
expect(
await value(`
const source = { 0: 1, 1: 2, length: 2 }
return Array.from(source, (item, index) => {
if (index === 0) source[1] = 9
return item
})
`),
).toEqual([1, 9])
})
test("interleaves Object.groupBy and Map.groupBy callbacks and closes on callback failure", async () => {
expect(
await value(`
const events = []
function* source() { try { events.push("next"); yield 1; events.push("next"); yield 2 } finally { events.push("close") } }
const object = Object.groupBy(source(), (item) => { events.push("object " + item); return item % 2 })
const map = Map.groupBy(source(), (item) => { events.push("map " + item); return item % 2 })
let caught
try { Object.groupBy(source(), () => { throw "callback" }) } catch (error) { caught = error }
return [object, Object.fromEntries(map), caught, events]
`),
).toEqual([
{ 0: [2], 1: [1] },
{ 0: [2], 1: [1] },
"callback",
["next", "object 1", "next", "object 2", "close", "next", "map 1", "next", "map 2", "close", "next", "close"],
])
})
test("consumes Promise combinator generators in order and observes rejections", async () => {
expect(
await value(`
function* items() { yield Promise.resolve(1); yield Promise.reject("bad"); yield 3 }
const settled = await Promise.allSettled(items())
let all, any
try { await Promise.all(items()) } catch (error) { all = error }
try { await Promise.any((function* () { yield Promise.reject("a"); yield Promise.reject("b") })()) }
catch (error) { any = error.errors }
const race = await Promise.race((function* () { yield 4; yield Promise.resolve(5) })())
return [settled, all, any, race]
`),
).toEqual([
[
{ status: "fulfilled", value: 1 },
{ status: "rejected", reason: "bad" },
{ status: "fulfilled", value: 3 },
],
"bad",
["a", "b"],
4,
])
})
test("finishes Promise combinator iterator consumption before returning the promise", async () => {
expect(
await value(`
const events = []
function* items() { events.push("first"); yield 1; events.push("second"); yield 2 }
const promise = Promise.all(items())
events.push("after call")
await promise
return events
`),
).toEqual(["first", "second", "after call"])
})
// test/built-ins/Object/fromEntries/iterator-closed-for-null-entry.js
test("closes Object.fromEntries on malformed entries and consumes valid generators", async () => {
expect(
await value(`
const events = []
function* valid() { yield ["a", 1]; yield ["b", 2] }
function* invalid() { try { yield ["a", 1]; yield null; yield ["c", 3] } finally { events.push("close") } }
let name
try { Object.fromEntries(invalid()) } catch (error) { name = error.name }
return [Object.fromEntries(valid()), name, events]
`),
).toEqual([{ a: 1, b: 2 }, "TypeError", ["close"]])
})
test("consumes AggregateError and Math.sumPrecise generators and closes on invalid numbers", async () => {
expect(
await value(`
const events = []
function* errors() { yield "a"; yield "b" }
function* numbers() { yield 1e30; yield 0.1; yield -1e30 }
function* invalid() { try { yield 1; yield "bad"; yield 2 } finally { events.push("close") } }
const aggregate = new AggregateError(errors(), "message")
let name
try { Math.sumPrecise(invalid()) } catch (error) { name = error.name }
return [aggregate.errors, aggregate.message, Math.sumPrecise(numbers()), name, events]
`),
).toEqual([["a", "b"], "message", 0.1, "TypeError", ["close"]])
})
test("rejects async generators in every synchronous iterable consumer", async () => {
expect(
await value(`
async function* source() { yield ["a", 1] }
const checks = [
() => [...source()],
() => ((...items) => items)(...source()),
() => { const [item] = source(); return item },
() => Array.from(source()),
() => new Map(source()),
() => new Set(source()),
() => new URLSearchParams(source()),
() => Object.fromEntries(source()),
() => Object.groupBy(source(), (item) => item),
() => Math.sumPrecise(source()),
() => new AggregateError(source()),
]
const names = []
for (const check of checks) {
try { check() } catch (error) { names.push(error.name) }
}
try { await Promise.all(source()) } catch (error) { names.push(error.name) }
return names
`),
).toEqual(Array(12).fill("TypeError"))
})
// test/built-ins/Array/from/iter-get-iter-err.js
// test/built-ins/Array/from/iter-adv-err.js
test("does not close when iterator acquisition or next-result validation fails", async () => {
expect(
await value(`
const events = []
const acquisition = { [Symbol.iterator]: () => { events.push("acquire"); throw "acquisition" } }
const malformed = {
[Symbol.iterator]: () => malformed,
next: () => { events.push("next"); return 1 },
return: () => { events.push("close"); return {} },
}
for (const source of [acquisition, malformed]) {
try { Array.from(source) } catch {}
}
return events
`),
).toEqual(["acquire", "next"])
})
test("reports synchronous iterator failures before queued promise reactions", async () => {
expect(
await value(`
const events = []
Promise.resolve().then(() => events.push("reaction"))
const iterator = {
[Symbol.iterator]: () => iterator,
next: () => { throw "next" },
}
try { Array.from(iterator) } catch { events.push("catch") }
await Promise.resolve()
return events
`),
).toEqual(["catch", "reaction"])
})
})