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