feat(codemode): support Promise.any and new Promise construction (#36339)
This commit is contained in:
parent
59202356fe
commit
fe09a2e9b7
10 changed files with 741 additions and 47 deletions
|
|
@ -64,10 +64,13 @@ path lookup, namespace browsing, deterministic ranking, and pagination.
|
||||||
### Tool execution
|
### Tool execution
|
||||||
|
|
||||||
Every sandbox promise starts eagerly on a run-once fiber owned by the whole CodeMode execution, including tool calls,
|
Every sandbox promise starts eagerly on a run-once fiber owned by the whole CodeMode execution, including tool calls,
|
||||||
async functions, chained `.then`/`.catch`/`.finally` reactions, `Promise.all`, `Promise.allSettled`, `Promise.race`,
|
async functions, chained `.then`/`.catch`/`.finally` reactions, `new Promise(executor)` constructions, and the
|
||||||
`Promise.resolve`, and `Promise.reject`. Nested functions therefore cannot end the lifetime of work they started.
|
`Promise.all`/`allSettled`/`race`/`any`/`resolve`/`reject` statics. Nested functions therefore cannot end the lifetime
|
||||||
|
of work they started.
|
||||||
Independent aggregate batches overlap, and rejection is observed at the eventual `await` or chained rejection handler.
|
Independent aggregate batches overlap, and rejection is observed at the eventual `await` or chained rejection handler.
|
||||||
`Promise.race` uses native non-cancelling settlement semantics: its first result wins while losers continue running.
|
`Promise.race` and `Promise.any` use native non-cancelling settlement semantics: the deciding member wins while losers
|
||||||
|
continue running, and an all-rejected `Promise.any` rejects with an `AggregateError`. `new Promise(...)` hands the
|
||||||
|
executor first-class resolve/reject callables that may escape and settle the promise later, exactly once.
|
||||||
Reaction ordering matches what V8 makes observable - handlers and await continuations are deferred and run in attach
|
Reaction ordering matches what V8 makes observable - handlers and await continuations are deferred and run in attach
|
||||||
order, and a combinator settles one reaction turn after its deciding member - without promising exact microtask-count
|
order, and a combinator settles one reaction turn after its deciding member - without promising exact microtask-count
|
||||||
parity beyond that. At normal completion CodeMode interrupts everything still running - race losers,
|
parity beyond that. At normal completion CodeMode interrupts everything still running - race losers,
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ ultimate source of truth.
|
||||||
- [x] Sequence expressions (the comma operator).
|
- [x] Sequence expressions (the comma operator).
|
||||||
- [x] `await` for sandbox promises; a plain value passes through unchanged, though every `await` still defers its
|
- [x] `await` for sandbox promises; a plain value passes through unchanged, though every `await` still defers its
|
||||||
continuation one reaction turn.
|
continuation one reaction turn.
|
||||||
- [x] `new` for Error types, Date, RegExp, Map, Set, URL, and URLSearchParams.
|
- [x] `new` for Error types, Date, RegExp, Map, Set, URL, URLSearchParams, and Promise.
|
||||||
- [x] Arithmetic operators: `+`, `-`, `*`, `/`, `%`, and `**`.
|
- [x] Arithmetic operators: `+`, `-`, `*`, `/`, `%`, and `**`.
|
||||||
- [x] Equality and ordering: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, and `>=`.
|
- [x] Equality and ordering: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, and `>=`.
|
||||||
- [x] Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`, and `>>>`.
|
- [x] Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`, and `>>>`.
|
||||||
|
|
@ -103,15 +103,15 @@ ultimate source of truth.
|
||||||
- [x] Prefix and postfix `++` and `--`.
|
- [x] Prefix and postfix `++` and `--`.
|
||||||
- [x] Plain, arithmetic, bitwise, and logical assignment operators.
|
- [x] Plain, arithmetic, bitwise, and logical assignment operators.
|
||||||
- [ ] Unary `void` and `delete`.
|
- [ ] Unary `void` and `delete`.
|
||||||
- [ ] Arbitrary constructors and `new Promise(...)`.
|
- [ ] Arbitrary constructors.
|
||||||
|
|
||||||
## Promises and tools
|
## Promises and tools
|
||||||
|
|
||||||
- [x] Tool calls start eagerly and return supervised, run-once sandbox promises.
|
- [x] Tool calls start eagerly and return supervised, run-once sandbox promises.
|
||||||
- [x] Direct `await`, repeated awaits, and implicit resolution when a promise is returned from a function/program.
|
- [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.resolve` and `Promise.reject`.
|
||||||
- [x] `Promise.all`, `Promise.allSettled`, and `Promise.race` over supported collections containing promises and plain
|
- [x] `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any` over supported collections containing
|
||||||
values.
|
promises and plain values.
|
||||||
- [x] `Promise.all` preserves result order and rejects on the first observed failure without cancelling siblings.
|
- [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.allSettled` returns plain fulfilled/rejected outcome records.
|
||||||
- [x] `Promise.race` settles from the first result without cancelling losers at settlement time.
|
- [x] `Promise.race` settles from the first result without cancelling losers at settlement time.
|
||||||
|
|
@ -130,9 +130,14 @@ ultimate source of truth.
|
||||||
diagnostics. A combinator abandoned inside its final settlement turn counts as pending and is interrupted
|
diagnostics. A combinator abandoned inside its final settlement turn counts as pending and is interrupted
|
||||||
without a warning.
|
without a warning.
|
||||||
- [x] `try`/`catch` can handle awaited tool and promise failures.
|
- [x] `try`/`catch` can handle awaited tool and promise failures.
|
||||||
- [ ] `Promise.any`.
|
- [x] `Promise.any`: first fulfillment wins; all-rejected rejects with an `AggregateError` whose `errors` array holds
|
||||||
|
the catch-normalized reasons in input order, and empty input rejects with an empty `AggregateError`.
|
||||||
|
- [x] `new Promise((resolve, reject) => ...)`: the executor runs synchronously and receives first-class resolve/reject
|
||||||
|
callables that settle the promise exactly once (they may escape the executor and settle later); an executor
|
||||||
|
throw rejects unless the promise already settled, resolving with a promise adopts it, and resolving with the
|
||||||
|
promise itself rejects with a `TypeError`. Resolver callables work as `.then`/`.catch` handlers and collection
|
||||||
|
callbacks but remain opaque references that cannot cross the data boundary.
|
||||||
- [ ] Thenable assimilation (objects with a `then` method are plain data, not promises).
|
- [ ] Thenable assimilation (objects with a `then` method are plain data, not promises).
|
||||||
- [ ] Custom promise construction with `new Promise(...)`.
|
|
||||||
- [ ] Async iterables, host streams, and stream consumption.
|
- [ ] Async iterables, host streams, and stream consumption.
|
||||||
|
|
||||||
## Objects and properties
|
## Objects and properties
|
||||||
|
|
@ -163,7 +168,7 @@ ultimate source of truth.
|
||||||
- [ ] The mapper and `thisArg` forms of `Array.from`.
|
- [ ] The mapper and `thisArg` forms of `Array.from`.
|
||||||
- [ ] `Array.prototype.toSpliced`.
|
- [ ] `Array.prototype.toSpliced`.
|
||||||
- [ ] Canonical index handling: a key such as `"01"` must not alias index `1`.
|
- [ ] Canonical index handling: a key such as `"01"` must not alias index `1`.
|
||||||
- [ ] Complete sparse-array parity.
|
- [ ] Complete sparse-array parity. Promise combinators do consume holes as `undefined` members, as in JS.
|
||||||
- [ ] Correct `findLast` return behavior when its predicate mutates the examined element.
|
- [ ] Correct `findLast` return behavior when its predicate mutates the examined element.
|
||||||
|
|
||||||
## Strings
|
## Strings
|
||||||
|
|
@ -268,6 +273,8 @@ ultimate source of truth.
|
||||||
|
|
||||||
- [x] `Error`, `TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`, `EvalError`, and `URIError`, callable with
|
- [x] `Error`, `TypeError`, `RangeError`, `SyntaxError`, `ReferenceError`, `EvalError`, and `URIError`, callable with
|
||||||
or without `new`.
|
or without `new`.
|
||||||
|
- [x] `AggregateError` with the `(errors, message?)` signature and an own `errors` array, constructed directly or by
|
||||||
|
an all-rejected `Promise.any`.
|
||||||
- [x] Error `name`/`message`, error inheritance through `instanceof`, and plain-data serialization.
|
- [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] `instanceof` for Date, RegExp, Map, Set, URL, URLSearchParams, Array, Object, Promise, and Error types.
|
||||||
- [x] Catchable interpreter failures and awaited tool failures.
|
- [x] Catchable interpreter failures and awaited tool failures.
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ export class ComputedValue {
|
||||||
|
|
||||||
export class PromiseNamespace {}
|
export class PromiseNamespace {}
|
||||||
|
|
||||||
export type PromiseMethodName = "all" | "allSettled" | "race" | "resolve" | "reject"
|
export type PromiseMethodName = "all" | "allSettled" | "race" | "any" | "resolve" | "reject"
|
||||||
|
|
||||||
export class PromiseMethodReference {
|
export class PromiseMethodReference {
|
||||||
constructor(readonly name: PromiseMethodName) {}
|
constructor(readonly name: PromiseMethodName) {}
|
||||||
|
|
@ -76,6 +76,12 @@ export class PromiseInstanceMethodReference {
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The resolve/reject callables handed to a `new Promise(executor)` executor. `settle` closes
|
||||||
|
// over the promise's deferred and is first-settlement-wins; later calls are no-ops, as in JS.
|
||||||
|
export class PromiseCapabilityFunction {
|
||||||
|
constructor(readonly settle: (value: unknown) => void) {}
|
||||||
|
}
|
||||||
|
|
||||||
export type GlobalNamespaceName =
|
export type GlobalNamespaceName =
|
||||||
| "Object"
|
| "Object"
|
||||||
| "Math"
|
| "Math"
|
||||||
|
|
@ -131,7 +137,7 @@ export type DiagnosticKind =
|
||||||
export const OptionalShortCircuit: unique symbol = Symbol("codemode.optional-short-circuit")
|
export const OptionalShortCircuit: unique symbol = Symbol("codemode.optional-short-circuit")
|
||||||
|
|
||||||
export const supportedSyntaxMessage =
|
export const supportedSyntaxMessage =
|
||||||
"Supported orchestration syntax: tools.* calls (they return promises - resolve them with await), data literals, destructuring, optional chaining, template literals, conditionals, switch, loops (incl. for...of and for...in over object/array/tools keys), arrow functions, spread, try/catch, array methods (map/filter/find/findIndex/some/every/reduce/flatMap/forEach/sort/slice/concat/indexOf/lastIndexOf/at/flat/reverse/includes/join), string methods (incl. match/matchAll/replace/split with regular expressions), Date/RegExp/Map/Set/URL/URLSearchParams, URI encoding helpers, Object/Math/JSON helpers, captured console.log/warn/error/dir/table, Promise.all/allSettled/race/resolve/reject over arrays mixing promises and plain values for parallel tool calls, and promise chaining with .then/.catch/.finally."
|
"Supported orchestration syntax: tools.* calls (they return promises - resolve them with await), data literals, destructuring, optional chaining, template literals, conditionals, switch, loops (incl. for...of and for...in over object/array/tools keys), arrow functions, spread, try/catch, array methods (map/filter/find/findIndex/some/every/reduce/flatMap/forEach/sort/slice/concat/indexOf/lastIndexOf/at/flat/reverse/includes/join), string methods (incl. match/matchAll/replace/split with regular expressions), Date/RegExp/Map/Set/URL/URLSearchParams, URI encoding helpers, Object/Math/JSON helpers, captured console.log/warn/error/dir/table, Promise.all/allSettled/race/any/resolve/reject over arrays mixing promises and plain values for parallel tool calls, promise chaining with .then/.catch/.finally, and new Promise((resolve, reject) => ...) construction."
|
||||||
|
|
||||||
export class InterpreterRuntimeError extends Error {
|
export class InterpreterRuntimeError extends Error {
|
||||||
readonly node?: AstNode
|
readonly node?: AstNode
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { parse } from "acorn"
|
import { parse } from "acorn"
|
||||||
import { Cause, Effect, Exit, Fiber, Scope, Semaphore } from "effect"
|
import { Cause, Deferred, Effect, Exit, Fiber, Scope, Semaphore } from "effect"
|
||||||
import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript"
|
import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript"
|
||||||
import {
|
import {
|
||||||
copyIn,
|
copyIn,
|
||||||
|
|
@ -42,6 +42,7 @@ import {
|
||||||
isRecord,
|
isRecord,
|
||||||
type MemberReference,
|
type MemberReference,
|
||||||
OptionalShortCircuit,
|
OptionalShortCircuit,
|
||||||
|
PromiseCapabilityFunction,
|
||||||
PromiseInstanceMethodReference,
|
PromiseInstanceMethodReference,
|
||||||
PromiseMethodReference,
|
PromiseMethodReference,
|
||||||
type PromiseMethodName,
|
type PromiseMethodName,
|
||||||
|
|
@ -94,6 +95,7 @@ import {
|
||||||
coerceToNumber,
|
coerceToNumber,
|
||||||
coerceToString,
|
coerceToString,
|
||||||
compoundOperators,
|
compoundOperators,
|
||||||
|
createAggregateErrorValue,
|
||||||
createErrorValue,
|
createErrorValue,
|
||||||
errorBrandName,
|
errorBrandName,
|
||||||
errorConstructors,
|
errorConstructors,
|
||||||
|
|
@ -226,11 +228,22 @@ const settleAfterTurn = <A, E, R>(body: Effect.Effect<A, E, R>): Effect.Effect<A
|
||||||
const selfResolutionError = (node?: AstNode): InterpreterRuntimeError =>
|
const selfResolutionError = (node?: AstNode): InterpreterRuntimeError =>
|
||||||
new InterpreterRuntimeError("Chaining cycle detected: a promise cannot resolve with itself.", node).as("TypeError")
|
new InterpreterRuntimeError("Chaining cycle detected: a promise cannot resolve with itself.", node).as("TypeError")
|
||||||
|
|
||||||
type ReactionHandler = CodeModeFunction | CoercionFunction | UriFunction
|
// Short-circuit marker for Promise.any: the first fulfillment travels the error channel of
|
||||||
|
// the flipped members so fail-fast Effect.all stops observing on it.
|
||||||
|
class PromiseAnyFulfilled {
|
||||||
|
constructor(readonly value: unknown) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReactionHandler = CodeModeFunction | CoercionFunction | UriFunction | PromiseCapabilityFunction
|
||||||
|
|
||||||
// Non-callables are ignored as in JS: `.then(undefined, f)` relies on the passthrough.
|
// Non-callables are ignored as in JS: `.then(undefined, f)` relies on the passthrough.
|
||||||
const reactionHandler = (value: unknown, method: string, node: AstNode): ReactionHandler | undefined => {
|
const reactionHandler = (value: unknown, method: string, node: AstNode): ReactionHandler | undefined => {
|
||||||
if (value instanceof CodeModeFunction || value instanceof CoercionFunction || value instanceof UriFunction) {
|
if (
|
||||||
|
value instanceof CodeModeFunction ||
|
||||||
|
value instanceof CoercionFunction ||
|
||||||
|
value instanceof UriFunction ||
|
||||||
|
value instanceof PromiseCapabilityFunction
|
||||||
|
) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
if (typeofValue(value) === "function") {
|
if (typeofValue(value) === "function") {
|
||||||
|
|
@ -250,6 +263,21 @@ const caughtErrorValue = (thrown: unknown): unknown => {
|
||||||
return createErrorValue(name, normalizeError(thrown).message)
|
return createErrorValue(name, normalizeError(thrown).message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `new Error("msg")` and the no-new call form share this; AggregateError alone takes
|
||||||
|
// (errors, message?) with a required errors collection, as in JS.
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
// Copy: spreadItems returns array input itself, and the error value must not alias caller data.
|
||||||
|
return createAggregateErrorValue([...errors], args[1] === undefined ? "" : coerceToString(args[1]))
|
||||||
|
}
|
||||||
|
|
||||||
const isRuntimeReference = (value: unknown): boolean =>
|
const isRuntimeReference = (value: unknown): boolean =>
|
||||||
value instanceof CodeModeFunction ||
|
value instanceof CodeModeFunction ||
|
||||||
value instanceof ToolReference ||
|
value instanceof ToolReference ||
|
||||||
|
|
@ -262,6 +290,7 @@ const isRuntimeReference = (value: unknown): boolean =>
|
||||||
value instanceof SandboxPromise ||
|
value instanceof SandboxPromise ||
|
||||||
value instanceof CoercionFunction ||
|
value instanceof CoercionFunction ||
|
||||||
value instanceof UriFunction ||
|
value instanceof UriFunction ||
|
||||||
|
value instanceof PromiseCapabilityFunction ||
|
||||||
value instanceof ErrorConstructorReference ||
|
value instanceof ErrorConstructorReference ||
|
||||||
isSandboxValue(value)
|
isSandboxValue(value)
|
||||||
|
|
||||||
|
|
@ -305,6 +334,7 @@ const typeofValue = (value: unknown): string => {
|
||||||
value instanceof PromiseMethodReference ||
|
value instanceof PromiseMethodReference ||
|
||||||
value instanceof PromiseInstanceMethodReference ||
|
value instanceof PromiseInstanceMethodReference ||
|
||||||
value instanceof PromiseNamespace ||
|
value instanceof PromiseNamespace ||
|
||||||
|
value instanceof PromiseCapabilityFunction ||
|
||||||
value instanceof ErrorConstructorReference
|
value instanceof ErrorConstructorReference
|
||||||
)
|
)
|
||||||
return "function"
|
return "function"
|
||||||
|
|
@ -1584,19 +1614,10 @@ class Interpreter<R> {
|
||||||
const argNodes = getArray(node, "arguments")
|
const argNodes = getArray(node, "arguments")
|
||||||
const self = this
|
const self = this
|
||||||
if (name === "Promise") {
|
if (name === "Promise") {
|
||||||
throw new InterpreterRuntimeError(
|
return Effect.flatMap(this.evaluateCallArguments(argNodes), (args) => self.constructPromise(args[0], node))
|
||||||
"new Promise(...) is not supported in CodeMode; tool calls already return promises - call the tool and await the result.",
|
|
||||||
node,
|
|
||||||
"UnsupportedSyntax",
|
|
||||||
[supportedSyntaxMessage],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (errorConstructors.has(name)) {
|
if (errorConstructors.has(name)) {
|
||||||
return Effect.gen(function* () {
|
return Effect.map(this.evaluateCallArguments(argNodes), (args) => constructErrorValue(name, args, node))
|
||||||
const arg =
|
|
||||||
argNodes.length > 0 ? yield* self.evaluateExpression(asNode(argNodes[0], "arguments[0]")) : undefined
|
|
||||||
return createErrorValue(name, arg === undefined ? "" : coerceToString(arg))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if (valueConstructors.has(name)) {
|
if (valueConstructors.has(name)) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
|
|
@ -2066,7 +2087,11 @@ class Interpreter<R> {
|
||||||
}
|
}
|
||||||
// `Error("msg")` without `new` constructs an error exactly like `new Error("msg")`, as in JS.
|
// `Error("msg")` without `new` constructs an error exactly like `new Error("msg")`, as in JS.
|
||||||
if (callable instanceof ErrorConstructorReference) {
|
if (callable instanceof ErrorConstructorReference) {
|
||||||
return createErrorValue(callable.name, args[0] === undefined ? "" : coerceToString(args[0]))
|
return constructErrorValue(callable.name, args, node)
|
||||||
|
}
|
||||||
|
if (callable instanceof PromiseCapabilityFunction) {
|
||||||
|
callable.settle(args[0])
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
throw new InterpreterRuntimeError("Only tools are callable in CodeMode.", callee)
|
throw new InterpreterRuntimeError("Only tools are callable in CodeMode.", callee)
|
||||||
})
|
})
|
||||||
|
|
@ -2255,8 +2280,8 @@ class Interpreter<R> {
|
||||||
return this.createPromise(Effect.fail(new ProgramThrow(args[0])))
|
return this.createPromise(Effect.fail(new ProgramThrow(args[0])))
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = spreadItems(args[0])
|
const spread = spreadItems(args[0])
|
||||||
if (items === undefined) {
|
if (spread === undefined) {
|
||||||
return this.createPromise(
|
return this.createPromise(
|
||||||
Effect.fail(
|
Effect.fail(
|
||||||
new InterpreterRuntimeError(
|
new InterpreterRuntimeError(
|
||||||
|
|
@ -2266,6 +2291,8 @@ class Interpreter<R> {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// Densify: JS combinator iteration reads sparse holes as undefined members; .map would skip them.
|
||||||
|
const items = Array.from(spread)
|
||||||
|
|
||||||
// JS makes combinator members "handled" synchronously at the call - their rejections
|
// JS makes combinator members "handled" synchronously at the call - their rejections
|
||||||
// belong to the aggregate from this moment, even ones settling before it runs.
|
// belong to the aggregate from this moment, even ones settling before it runs.
|
||||||
|
|
@ -2331,6 +2358,29 @@ class Interpreter<R> {
|
||||||
// and is interrupted at normal completion (already observed) or by teardown.
|
// and is interrupted at normal completion (already observed) or by teardown.
|
||||||
return this.createPromise(settleAfterTurn(Effect.flatten(Effect.raceAll(observations))))
|
return this.createPromise(settleAfterTurn(Effect.flatten(Effect.raceAll(observations))))
|
||||||
}
|
}
|
||||||
|
case "any": {
|
||||||
|
// De Morgan dual of Promise.all: members are flipped so the first fulfillment
|
||||||
|
// short-circuits fail-fast Effect.all, and all-rejected completes with the reasons
|
||||||
|
// in input order for the AggregateError.
|
||||||
|
const flipped = items.map((item) =>
|
||||||
|
item instanceof SandboxPromise
|
||||||
|
? Effect.flatMap(this.promises.await(item), (exit) => {
|
||||||
|
if (Exit.isSuccess(exit)) return Effect.fail(new PromiseAnyFulfilled(exit.value))
|
||||||
|
if (Cause.hasInterruptsOnly(exit.cause)) return Effect.failCause(exit.cause)
|
||||||
|
return Effect.succeed(caughtErrorValue(Cause.squash(exit.cause)))
|
||||||
|
})
|
||||||
|
: 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 this.createPromise(settleAfterTurn(body))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2405,6 +2455,43 @@ class Interpreter<R> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// new Promise(executor): the promise's fiber awaits a Deferred that resolve/reject settle
|
||||||
|
// exactly once. The executor runs synchronously; its throw rejects the promise unless it
|
||||||
|
// already settled (JS swallows post-settlement executor throws).
|
||||||
|
private constructPromise(executor: unknown, node: AstNode): Effect.Effect<SandboxPromise, unknown, R> {
|
||||||
|
if (!(executor instanceof CodeModeFunction)) {
|
||||||
|
throw new InterpreterRuntimeError(
|
||||||
|
"new Promise(...) expects an executor function (e.g. new Promise((resolve, reject) => { ... })).",
|
||||||
|
node,
|
||||||
|
).as("TypeError")
|
||||||
|
}
|
||||||
|
const self = this
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const deferred = Deferred.makeUnsafe<unknown, unknown>()
|
||||||
|
const box: { own?: SandboxPromise } = {}
|
||||||
|
const promise = yield* self.createPromise(
|
||||||
|
Effect.flatMap(Deferred.await(deferred), (value) => {
|
||||||
|
if (!(value instanceof SandboxPromise)) return Effect.succeed(value)
|
||||||
|
if (value === box.own) return Effect.fail(selfResolutionError(node))
|
||||||
|
return self.settlePromise(value)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
box.own = promise
|
||||||
|
const resolve = new PromiseCapabilityFunction((value) => {
|
||||||
|
Deferred.doneUnsafe(deferred, Exit.succeed(value))
|
||||||
|
})
|
||||||
|
const reject = new PromiseCapabilityFunction((value) => {
|
||||||
|
Deferred.doneUnsafe(deferred, Exit.fail(new ProgramThrow(value)))
|
||||||
|
})
|
||||||
|
const executed = yield* Effect.exit(self.invokeFunction(executor, [resolve, reject]))
|
||||||
|
if (!Exit.isSuccess(executed)) {
|
||||||
|
if (Cause.hasInterruptsOnly(executed.cause)) return yield* Effect.failCause(executed.cause)
|
||||||
|
Deferred.doneUnsafe(deferred, Exit.fail(Cause.squash(executed.cause)))
|
||||||
|
}
|
||||||
|
return promise
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private invokeFunction(fn: CodeModeFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
|
private invokeFunction(fn: CodeModeFunction, args: Array<unknown>): Effect.Effect<unknown, unknown, R> {
|
||||||
const invocation = new Interpreter(this.invokeTool, this.toolKeys, this.promises, this.logs, this.callPermits)
|
const invocation = new Interpreter(this.invokeTool, this.toolKeys, this.promises, this.logs, this.callPermits)
|
||||||
invocation.scopes = [...fn.capturedScopes, new Map<string, Binding>()]
|
invocation.scopes = [...fn.capturedScopes, new Map<string, Binding>()]
|
||||||
|
|
@ -2568,7 +2655,8 @@ class Interpreter<R> {
|
||||||
if (
|
if (
|
||||||
!(callback instanceof CodeModeFunction) &&
|
!(callback instanceof CodeModeFunction) &&
|
||||||
!(callback instanceof CoercionFunction) &&
|
!(callback instanceof CoercionFunction) &&
|
||||||
!(callback instanceof UriFunction)
|
!(callback instanceof UriFunction) &&
|
||||||
|
!(callback instanceof PromiseCapabilityFunction)
|
||||||
) {
|
) {
|
||||||
throw new InterpreterRuntimeError(`${name} expects a function callback.`, node)
|
throw new InterpreterRuntimeError(`${name} expects a function callback.`, node)
|
||||||
}
|
}
|
||||||
|
|
@ -2577,7 +2665,9 @@ class Interpreter<R> {
|
||||||
? Effect.succeed(invokeCoercion(callback, callbackArgs, node))
|
? Effect.succeed(invokeCoercion(callback, callbackArgs, node))
|
||||||
: callback instanceof UriFunction
|
: callback instanceof UriFunction
|
||||||
? Effect.succeed(invokeUriFunction(callback, callbackArgs, node))
|
? Effect.succeed(invokeUriFunction(callback, callbackArgs, node))
|
||||||
: this.invokeFunction(callback, callbackArgs)
|
: callback instanceof PromiseCapabilityFunction
|
||||||
|
? Effect.sync(() => callback.settle(callbackArgs[0]))
|
||||||
|
: this.invokeFunction(callback, callbackArgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
private invokeMapMethod(
|
private invokeMapMethod(
|
||||||
|
|
@ -3185,7 +3275,7 @@ class Interpreter<R> {
|
||||||
return new PromiseMethodReference(key as PromiseMethodName)
|
return new PromiseMethodReference(key as PromiseMethodName)
|
||||||
}
|
}
|
||||||
throw new InterpreterRuntimeError(
|
throw new InterpreterRuntimeError(
|
||||||
`Promise.${String(key)} is not available in CodeMode. Available: Promise.all, Promise.allSettled, Promise.race, Promise.resolve, and Promise.reject; consume promises with await.`,
|
`Promise.${String(key)} is not available in CodeMode. Available: Promise.all, Promise.allSettled, Promise.race, Promise.any, Promise.resolve, and Promise.reject; consume promises with await.`,
|
||||||
propertyNode,
|
propertyNode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { PromiseMethodName } from "../interpreter/model.js"
|
import type { PromiseMethodName } from "../interpreter/model.js"
|
||||||
|
|
||||||
export const promiseStatics = new Set<PromiseMethodName>(["all", "allSettled", "race", "resolve", "reject"])
|
export const promiseStatics = new Set<PromiseMethodName>(["all", "allSettled", "race", "any", "resolve", "reject"])
|
||||||
|
|
||||||
/** Maximum number of eagerly forked tool calls that may run concurrently. */
|
/** Maximum number of eagerly forked tool calls that may run concurrently. */
|
||||||
export const TOOL_CALL_CONCURRENCY = 8
|
export const TOOL_CALL_CONCURRENCY = 8
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export const errorConstructors = new Set([
|
||||||
"ReferenceError",
|
"ReferenceError",
|
||||||
"EvalError",
|
"EvalError",
|
||||||
"URIError",
|
"URIError",
|
||||||
|
"AggregateError",
|
||||||
])
|
])
|
||||||
|
|
||||||
export const valueConstructors = new Set(["Date", "RegExp", "Map", "Set", "URL", "URLSearchParams"])
|
export const valueConstructors = new Set(["Date", "RegExp", "Map", "Set", "URL", "URLSearchParams"])
|
||||||
|
|
@ -20,6 +21,9 @@ export const createErrorValue = (name: string, message: string): SafeObject => {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createAggregateErrorValue = (errors: Array<unknown>, message: string): SafeObject =>
|
||||||
|
Object.assign(createErrorValue("AggregateError", message), { errors })
|
||||||
|
|
||||||
export const errorBrandName = (value: unknown): string | undefined =>
|
export const errorBrandName = (value: unknown): string | undefined =>
|
||||||
value !== null && typeof value === "object"
|
value !== null && typeof value === "object"
|
||||||
? ((value as Record<PropertyKey, unknown>)[ErrorBrand] as string | undefined)
|
? ((value as Record<PropertyKey, unknown>)[ErrorBrand] as string | undefined)
|
||||||
|
|
|
||||||
|
|
@ -611,7 +611,7 @@ export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBu
|
||||||
"## Language",
|
"## 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.",
|
"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, unlisted methods, and new Promise(...) are unavailable. Use Code Mode tools for external operations. Use await with try/catch.",
|
"Modules/imports, classes, generators, timers, fetch, eval, prototype access, and unlisted methods are unavailable. Use Code Mode tools for external operations. Use await with try/catch.",
|
||||||
"Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
|
"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 `{}`.",
|
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -672,9 +672,10 @@ describe("CodeMode public contract", () => {
|
||||||
expect(instructions).toContain("not a general-purpose runtime")
|
expect(instructions).toContain("not a general-purpose runtime")
|
||||||
expect(instructions).not.toContain("Standard modern JavaScript works")
|
expect(instructions).not.toContain("Standard modern JavaScript works")
|
||||||
expect(instructions).not.toContain("TypeScript type annotations")
|
expect(instructions).not.toContain("TypeScript type annotations")
|
||||||
for (const missing of ["Modules/imports", "classes", "generators", "fetch", "new Promise(...)"]) {
|
for (const missing of ["Modules/imports", "classes", "generators", "fetch"]) {
|
||||||
expect(instructions).toContain(missing)
|
expect(instructions).toContain(missing)
|
||||||
}
|
}
|
||||||
|
expect(instructions).not.toContain("new Promise(...) are unavailable")
|
||||||
expect(instructions).not.toContain("promise chaining")
|
expect(instructions).not.toContain("promise chaining")
|
||||||
expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers")
|
expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers")
|
||||||
expect(instructions).not.toContain("host globals")
|
expect(instructions).not.toContain("host globals")
|
||||||
|
|
|
||||||
|
|
@ -573,13 +573,14 @@ describe("Test262 async functions and await", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Test262 expected Promise conformance", () => {
|
describe("Test262 expected Promise conformance", () => {
|
||||||
for (const name of ["all", "allSettled", "race"] as const) {
|
for (const name of ["all", "allSettled", "race", "any"] as const) {
|
||||||
test(`Promise.${name} rejects invalid input with TypeError`, async () => {
|
test(`Promise.${name} rejects invalid input with TypeError`, async () => {
|
||||||
// Sources:
|
// Sources:
|
||||||
// test/built-ins/Promise/all/S25.4.4.1_A3.1_T1.js
|
// test/built-ins/Promise/all/S25.4.4.1_A3.1_T1.js
|
||||||
// test/built-ins/Promise/all/S25.4.4.1_A3.1_T2.js
|
// test/built-ins/Promise/all/S25.4.4.1_A3.1_T2.js
|
||||||
// test/built-ins/Promise/allSettled/iter-arg-is-number-reject.js
|
// test/built-ins/Promise/allSettled/iter-arg-is-number-reject.js
|
||||||
// test/built-ins/Promise/race/iter-arg-is-number-reject.js
|
// test/built-ins/Promise/race/iter-arg-is-number-reject.js
|
||||||
|
// test/built-ins/Promise/any/iter-arg-is-number-reject.js
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
try {
|
try {
|
||||||
|
|
@ -595,7 +596,7 @@ describe("Test262 expected Promise conformance", () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
test.failing("Promise.all consumes sparse positions as undefined", async () => {
|
test("Promise.all consumes sparse positions as undefined", async () => {
|
||||||
// Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
|
// Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
|
|
@ -607,7 +608,7 @@ describe("Test262 expected Promise conformance", () => {
|
||||||
).toEqual([2, true, 1])
|
).toEqual([2, true, 1])
|
||||||
})
|
})
|
||||||
|
|
||||||
test.failing("Promise.allSettled consumes sparse positions as undefined", async () => {
|
test("Promise.allSettled consumes sparse positions as undefined", async () => {
|
||||||
// Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
|
// Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
|
|
@ -619,7 +620,7 @@ describe("Test262 expected Promise conformance", () => {
|
||||||
).toEqual([2, "fulfilled", true, { status: "fulfilled", value: 1 }])
|
).toEqual([2, "fulfilled", true, { status: "fulfilled", value: 1 }])
|
||||||
})
|
})
|
||||||
|
|
||||||
test.failing("Promise.race consumes a sparse first position as undefined", async () => {
|
test("Promise.race consumes a sparse first position as undefined", async () => {
|
||||||
// Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
|
// Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
|
||||||
expect(
|
expect(
|
||||||
await value(`
|
await value(`
|
||||||
|
|
@ -630,6 +631,17 @@ describe("Test262 expected Promise conformance", () => {
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("Promise.any consumes a sparse first position as an undefined fulfillment", async () => {
|
||||||
|
// Source: test/built-ins/Array/from/from-array.js (array iterator hole behavior)
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const input = []
|
||||||
|
input[1] = Promise.reject("loses")
|
||||||
|
return (await Promise.any(input)) === undefined
|
||||||
|
`),
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
test("Promise.all settles after reactions attached to its inputs", async () => {
|
test("Promise.all settles after reactions attached to its inputs", async () => {
|
||||||
// Sources:
|
// Sources:
|
||||||
// test/built-ins/Promise/all/S25.4.4.1_A7.2_T1.js
|
// test/built-ins/Promise/all/S25.4.4.1_A7.2_T1.js
|
||||||
|
|
@ -1002,3 +1014,410 @@ describe("Test262 expected Promise conformance", () => {
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("Test262 Promise.any", () => {
|
||||||
|
test("is a callable static that returns a promise", async () => {
|
||||||
|
// Sources:
|
||||||
|
// test/built-ins/Promise/any/is-function.js
|
||||||
|
// test/built-ins/Promise/any/returns-promise.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const promise = Promise.any([1])
|
||||||
|
return [typeof Promise.any, promise instanceof Promise, await promise]
|
||||||
|
`),
|
||||||
|
).toEqual(["function", true, 1])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("fulfills with the first fulfilled member, ignoring rejections", async () => {
|
||||||
|
// Sources:
|
||||||
|
// test/built-ins/Promise/any/resolved-sequence-mixed.js
|
||||||
|
// test/built-ins/Promise/any/resolved-sequence-with-rejections.js
|
||||||
|
// test/built-ins/Promise/any/reject-ignored-immed.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
return [
|
||||||
|
await Promise.any([Promise.reject("a"), Promise.resolve(1), Promise.resolve(2)]),
|
||||||
|
await Promise.any([Promise.reject("a"), "plain", Promise.reject("b")]),
|
||||||
|
]
|
||||||
|
`),
|
||||||
|
).toEqual([1, "plain"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("a fulfillment wins over a later rejection of another member", async () => {
|
||||||
|
// Sources:
|
||||||
|
// test/built-ins/Promise/any/resolve-ignores-late-rejection.js
|
||||||
|
// test/built-ins/Promise/any/resolve-ignores-late-rejection-deferred.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
let rejectLate
|
||||||
|
const late = new Promise((_, reject) => { rejectLate = reject })
|
||||||
|
const result = await Promise.any([late, Promise.resolve("won")])
|
||||||
|
rejectLate("too late")
|
||||||
|
return result
|
||||||
|
`),
|
||||||
|
).toBe("won")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects with an AggregateError carrying the reasons in input order", async () => {
|
||||||
|
// Sources:
|
||||||
|
// test/built-ins/Promise/any/reject-all-mixed.js
|
||||||
|
// test/built-ins/Promise/any/reject-immed.js
|
||||||
|
// test/built-ins/Promise/any/reject-deferred.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
let rejectLate
|
||||||
|
const late = new Promise((_, reject) => { rejectLate = reject })
|
||||||
|
const aggregate = Promise.any([Promise.reject("first"), late, Promise.reject("third")])
|
||||||
|
rejectLate("second")
|
||||||
|
try {
|
||||||
|
await aggregate
|
||||||
|
return "fulfilled"
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
isAggregate: error instanceof AggregateError,
|
||||||
|
isError: error instanceof Error,
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
errors: error.errors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
).toEqual({
|
||||||
|
isAggregate: true,
|
||||||
|
isError: true,
|
||||||
|
name: "AggregateError",
|
||||||
|
message: "All promises were rejected",
|
||||||
|
errors: ["first", "second", "third"],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects an empty input with an empty AggregateError", async () => {
|
||||||
|
// Source: test/built-ins/Promise/any/iter-arg-is-empty-iterable-reject.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
try {
|
||||||
|
await Promise.any([])
|
||||||
|
return "fulfilled"
|
||||||
|
} catch (error) {
|
||||||
|
return [error instanceof AggregateError, error.errors.length]
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
).toEqual([true, 0])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("consumes a string input as its characters", async () => {
|
||||||
|
// Source: test/built-ins/Promise/any/iter-arg-is-string-resolve.js
|
||||||
|
expect(await value(`return await Promise.any("abc")`)).toBe("a")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects an empty string input with an empty AggregateError", async () => {
|
||||||
|
// Source: test/built-ins/Promise/any/iter-arg-is-empty-string-reject.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
try {
|
||||||
|
await Promise.any("")
|
||||||
|
return "fulfilled"
|
||||||
|
} catch (error) {
|
||||||
|
return [error instanceof AggregateError, error.errors.length]
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
).toEqual([true, 0])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("fulfills with the first member that does not reject", async () => {
|
||||||
|
// Sources:
|
||||||
|
// test/built-ins/Promise/any/resolve-from-reject-catch.js
|
||||||
|
// test/built-ins/Promise/any/resolve-from-resolve-reject-catch.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
return await Promise.any([
|
||||||
|
Promise.reject("a"),
|
||||||
|
new Promise((resolve, reject) => reject("b")),
|
||||||
|
Promise.all([Promise.reject("c")]),
|
||||||
|
Promise.resolve(Promise.reject("d").catch((reason) => reason)),
|
||||||
|
])
|
||||||
|
`),
|
||||||
|
).toBe("d")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("settles after reactions attached to its inputs", async () => {
|
||||||
|
// Source: test/built-ins/Promise/any/resolved-sequence.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const sequence = [1]
|
||||||
|
const input = Promise.resolve(1)
|
||||||
|
const aggregate = Promise.any([input])
|
||||||
|
aggregate.then(() => sequence.push(4))
|
||||||
|
input.then(() => sequence.push(3)).then(() => sequence.push(5))
|
||||||
|
sequence.push(2)
|
||||||
|
await aggregate
|
||||||
|
await Promise.resolve()
|
||||||
|
return sequence
|
||||||
|
`),
|
||||||
|
).toEqual([1, 2, 3, 4, 5])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Test262 AggregateError", () => {
|
||||||
|
test("constructs from an errors collection and an optional message", async () => {
|
||||||
|
// Sources:
|
||||||
|
// test/built-ins/AggregateError/errors-iterabletolist.js
|
||||||
|
// test/built-ins/AggregateError/message-undefined-no-prop.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const input = ["x", "y"]
|
||||||
|
const withMessage = new AggregateError(input, "msg")
|
||||||
|
const bare = new AggregateError([])
|
||||||
|
return [
|
||||||
|
withMessage.name,
|
||||||
|
withMessage.message,
|
||||||
|
withMessage.errors,
|
||||||
|
withMessage.errors !== input,
|
||||||
|
withMessage instanceof AggregateError,
|
||||||
|
withMessage instanceof Error,
|
||||||
|
bare.message,
|
||||||
|
bare.errors,
|
||||||
|
]
|
||||||
|
`),
|
||||||
|
).toEqual(["AggregateError", "msg", ["x", "y"], true, true, true, "", []])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects a non-collection errors argument with TypeError", async () => {
|
||||||
|
// Source: test/built-ins/AggregateError/errors-iterabletolist-failures.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
try {
|
||||||
|
new AggregateError(42)
|
||||||
|
return "constructed"
|
||||||
|
} catch (error) {
|
||||||
|
return error.name
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
).toBe("TypeError")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("is callable without new", async () => {
|
||||||
|
// Source: test/built-ins/AggregateError/newtarget-is-undefined.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const error = AggregateError(["x"], "m")
|
||||||
|
return [error instanceof AggregateError, error instanceof Error, error.name, error.message, error.errors]
|
||||||
|
`),
|
||||||
|
).toEqual([true, true, "AggregateError", "m", ["x"]])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("coerces a non-string message to a string", async () => {
|
||||||
|
// Source: test/built-ins/AggregateError/message-method-prop-cast.js (value coercion only; the
|
||||||
|
// upstream object-with-toString case is omitted because the sandbox has no user toString dispatch)
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
return [
|
||||||
|
new AggregateError([], 42).message,
|
||||||
|
new AggregateError([], false).message,
|
||||||
|
new AggregateError([], true).message,
|
||||||
|
new AggregateError([], null).message,
|
||||||
|
]
|
||||||
|
`),
|
||||||
|
).toEqual(["42", "false", "true", "null"])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Test262 Promise constructor", () => {
|
||||||
|
test("constructs a promise, handing the executor callable resolve/reject", async () => {
|
||||||
|
// Sources:
|
||||||
|
// test/built-ins/Promise/constructor.js
|
||||||
|
// test/built-ins/Promise/exec-args.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
let observed
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
observed = [typeof resolve, typeof reject]
|
||||||
|
resolve("done")
|
||||||
|
})
|
||||||
|
return [promise instanceof Promise, observed, await promise]
|
||||||
|
`),
|
||||||
|
).toEqual([true, ["function", "function"], "done"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("a missing or non-callable executor is a TypeError", async () => {
|
||||||
|
// Source: test/built-ins/Promise/executor-not-callable.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const outcomes = []
|
||||||
|
for (const make of [() => new Promise(), () => new Promise(1), () => new Promise({})]) {
|
||||||
|
try {
|
||||||
|
make()
|
||||||
|
outcomes.push("constructed")
|
||||||
|
} catch (error) {
|
||||||
|
outcomes.push(error.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outcomes
|
||||||
|
`),
|
||||||
|
).toEqual(["TypeError", "TypeError", "TypeError"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("resolves immediately or later through an escaping resolver", async () => {
|
||||||
|
// Sources:
|
||||||
|
// test/built-ins/Promise/resolve-non-thenable-immed.js
|
||||||
|
// test/built-ins/Promise/resolve-non-thenable-deferred.js
|
||||||
|
// test/built-ins/Promise/create-resolving-functions-resolve.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
let settle
|
||||||
|
const deferred = new Promise((resolve) => { settle = resolve })
|
||||||
|
const immediate = new Promise((resolve) => resolve("now"))
|
||||||
|
settle("later")
|
||||||
|
return [await immediate, await deferred]
|
||||||
|
`),
|
||||||
|
).toEqual(["now", "later"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects through reject and through an abrupt executor completion", async () => {
|
||||||
|
// Sources:
|
||||||
|
// test/built-ins/Promise/reject-via-fn-immed.js
|
||||||
|
// test/built-ins/Promise/reject-via-abrupt.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const observe = async (promise) => {
|
||||||
|
try { return ["fulfilled", await promise] } catch (reason) { return ["rejected", reason.message ?? reason] }
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
await observe(new Promise((_, reject) => reject("nope"))),
|
||||||
|
await observe(new Promise(() => { throw new Error("boom") })),
|
||||||
|
]
|
||||||
|
`),
|
||||||
|
).toEqual([
|
||||||
|
["rejected", "nope"],
|
||||||
|
["rejected", "boom"],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("only the first settlement counts", async () => {
|
||||||
|
// Sources:
|
||||||
|
// test/built-ins/Promise/reject-ignored-via-fn-immed.js
|
||||||
|
// test/built-ins/Promise/resolve-ignored-via-fn-immed.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
return [
|
||||||
|
await new Promise((resolve, reject) => { resolve("first"); reject("second"); resolve("third") }),
|
||||||
|
await new Promise((resolve) => { resolve(resolve("inner") === undefined ? "unreached" : "also unreached") }),
|
||||||
|
]
|
||||||
|
`),
|
||||||
|
).toEqual(["first", "inner"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("escaped resolvers keep first-settle-wins in both directions", async () => {
|
||||||
|
// Sources:
|
||||||
|
// test/built-ins/Promise/reject-ignored-via-fn-deferred.js
|
||||||
|
// test/built-ins/Promise/resolve-ignored-via-fn-deferred.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
let resolveRejected, rejectRejected
|
||||||
|
const rejected = new Promise((resolve, reject) => { resolveRejected = resolve; rejectRejected = reject })
|
||||||
|
rejectRejected("first")
|
||||||
|
const lateResolve = resolveRejected("late")
|
||||||
|
let resolveFulfilled, rejectFulfilled
|
||||||
|
const fulfilled = new Promise((resolve, reject) => { resolveFulfilled = resolve; rejectFulfilled = reject })
|
||||||
|
resolveFulfilled()
|
||||||
|
const lateReject = rejectFulfilled(new Promise(() => {}))
|
||||||
|
try {
|
||||||
|
await rejected
|
||||||
|
return "fulfilled"
|
||||||
|
} catch (reason) {
|
||||||
|
return [reason, lateResolve === undefined, (await fulfilled) === undefined, lateReject === undefined]
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
).toEqual(["first", true, true, true])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("a queued reaction chain observes a later rejection through a handler-less then", async () => {
|
||||||
|
// Sources:
|
||||||
|
// test/built-ins/Promise/reject-via-fn-immed-queue.js
|
||||||
|
// test/built-ins/Promise/reject-via-fn-deferred-queue.js
|
||||||
|
// test/built-ins/Promise/reject-via-abrupt-queue.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const observe = (promise) => promise.then(() => "wrong").then(() => "also wrong", (reason) => "caught:" + reason)
|
||||||
|
let reject
|
||||||
|
const deferred = new Promise((_, r) => { reject = r })
|
||||||
|
const chained = observe(deferred)
|
||||||
|
reject("boom")
|
||||||
|
return [
|
||||||
|
await observe(new Promise((_, r) => r("immed"))),
|
||||||
|
await chained,
|
||||||
|
await observe(new Promise(() => { throw "abrupt" })),
|
||||||
|
]
|
||||||
|
`),
|
||||||
|
).toEqual(["caught:immed", "caught:boom", "caught:abrupt"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("an exception after resolve is ignored", async () => {
|
||||||
|
// Source: test/built-ins/Promise/exception-after-resolve-in-executor.js
|
||||||
|
expect(await value(`return await new Promise((resolve) => { resolve("kept"); throw new Error("dropped") })`)).toBe(
|
||||||
|
"kept",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("resolving with a promise adopts its settlement", async () => {
|
||||||
|
// Sources:
|
||||||
|
// test/built-ins/Promise/resolve-thenable-immed.js (promise-adoption portion)
|
||||||
|
// test/built-ins/Promise/all/S25.4.4.1_A2.3_T1.js (resolution adoption semantics)
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const adoptedValue = await new Promise((resolve) => resolve(Promise.resolve("adopted")))
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => resolve(Promise.reject("bad")))
|
||||||
|
return [adoptedValue, "fulfilled"]
|
||||||
|
} catch (reason) {
|
||||||
|
return [adoptedValue, reason]
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
).toEqual(["adopted", "bad"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("resolving with the promise itself rejects with TypeError", async () => {
|
||||||
|
// Source: test/built-ins/Promise/resolve-self.js
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
let settle
|
||||||
|
const promise = new Promise((resolve) => { settle = resolve })
|
||||||
|
settle(promise)
|
||||||
|
try {
|
||||||
|
await promise
|
||||||
|
return "fulfilled"
|
||||||
|
} catch (error) {
|
||||||
|
return error.name
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
).toBe("TypeError")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("executor runs synchronously before the constructor returns", async () => {
|
||||||
|
// Source: test/built-ins/Promise/executor-call-context-strict.js (synchronous Call(executor) step)
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const sequence = []
|
||||||
|
sequence.push("before")
|
||||||
|
new Promise((resolve) => { sequence.push("executor"); resolve() })
|
||||||
|
sequence.push("after")
|
||||||
|
return sequence
|
||||||
|
`),
|
||||||
|
).toEqual(["before", "executor", "after"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test.failing("calling Promise without new throws TypeError", async () => {
|
||||||
|
// Source: test/built-ins/Promise/undefined-newtarget.js
|
||||||
|
// The sandbox currently reports a generic Error ("Only tools are callable in CodeMode.").
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
try {
|
||||||
|
Promise(() => {})
|
||||||
|
return "called"
|
||||||
|
} catch (error) {
|
||||||
|
return error.name
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
).toBe("TypeError")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1062,15 +1062,179 @@ describe("unsupported promise surface", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test("unknown Promise statics list what is available", async () => {
|
test("unknown Promise statics list what is available", async () => {
|
||||||
const diagnostic = await error(`return await Promise.any([tools.host.sleepy({ id: 1 })])`)
|
const diagnostic = await error(`return await Promise.withResolvers()`)
|
||||||
expect(diagnostic.message).toContain("Promise.any is not available")
|
expect(diagnostic.message).toContain("Promise.withResolvers is not available")
|
||||||
expect(diagnostic.message).toContain("Promise.allSettled")
|
expect(diagnostic.message).toContain("Promise.any")
|
||||||
})
|
})
|
||||||
|
})
|
||||||
test("new Promise(...) points at tool calls instead", async () => {
|
|
||||||
const diagnostic = await error(`return new Promise((resolve) => resolve(1))`)
|
describe("Promise.any", () => {
|
||||||
expect(diagnostic.kind).toBe("UnsupportedSyntax")
|
test("first tool success wins; failing and losing calls are handled silently", async () => {
|
||||||
expect(diagnostic.message).toContain("new Promise(...) is not supported")
|
const trace = makeTrace()
|
||||||
expect(diagnostic.message).toContain("already return promises")
|
const result = await run(
|
||||||
|
`
|
||||||
|
const winner = await Promise.any([
|
||||||
|
tools.host.fail({}),
|
||||||
|
tools.host.sleepy({ id: 1, ms: 5 }),
|
||||||
|
tools.host.sleepy({ id: 2, ms: 60000 }),
|
||||||
|
])
|
||||||
|
return winner
|
||||||
|
`,
|
||||||
|
{ trace },
|
||||||
|
)
|
||||||
|
expect(result.ok).toBe(true)
|
||||||
|
if (!result.ok) return
|
||||||
|
expect(result.value).toBe(1)
|
||||||
|
// The slow loser stays execution-owned and is interrupted at completion; the tool
|
||||||
|
// failure was observed by the aggregate, so no rejection warning survives.
|
||||||
|
expect(result.warnings).toBeUndefined()
|
||||||
|
expect(trace.interrupted).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("all members failing rejects with catch-normalized reasons in input order", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
try {
|
||||||
|
await Promise.any([tools.host.fail({}), Promise.reject("plain")])
|
||||||
|
return "fulfilled"
|
||||||
|
} catch (error) {
|
||||||
|
return [error.name, error.errors.map((reason) => reason.message ?? reason)]
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
).toEqual(["AggregateError", ["Lookup refused", "plain"]])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("settles one reaction turn after its deciding member, as in V8", async () => {
|
||||||
|
expect(await value(`return await Promise.race([Promise.any([Promise.resolve(1)]), Promise.resolve(2)])`)).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("a tie is decided by settlement order, not input order", async () => {
|
||||||
|
// Handlers run in attach order, so `first` settles before `second` and wins
|
||||||
|
// despite its later input position - as in real JS.
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const first = Promise.resolve().then(() => "one")
|
||||||
|
const second = Promise.resolve().then(() => "two")
|
||||||
|
return await Promise.any([second, first])
|
||||||
|
`),
|
||||||
|
).toBe("one")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("an abandoned rejecting aggregate is interrupted silently at the return", async () => {
|
||||||
|
const result = await run(`
|
||||||
|
Promise.any([Promise.reject(new Error("boom"))])
|
||||||
|
return "done"
|
||||||
|
`)
|
||||||
|
expect(result.ok).toBe(true)
|
||||||
|
if (!result.ok) return
|
||||||
|
expect(result.value).toBe("done")
|
||||||
|
expect(result.warnings).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("promise construction", () => {
|
||||||
|
test("a deferred gate coordinates tool results across async functions", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
let openGate
|
||||||
|
const gate = new Promise((resolve) => { openGate = resolve })
|
||||||
|
const worker = (async () => {
|
||||||
|
const id = await gate
|
||||||
|
return id * 2
|
||||||
|
})()
|
||||||
|
openGate(await tools.host.sleepy({ id: 21, ms: 5 }))
|
||||||
|
return await worker
|
||||||
|
`),
|
||||||
|
).toBe(42)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("the .then(resolve) bridge settles a constructed promise", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const bridged = new Promise((resolve, reject) => {
|
||||||
|
tools.host.sleepy({ id: 7, ms: 5 }).then(resolve, reject)
|
||||||
|
})
|
||||||
|
return await bridged
|
||||||
|
`),
|
||||||
|
).toBe(7)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("constructed promises participate in combinators", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
let settle
|
||||||
|
const manual = new Promise((resolve) => { settle = resolve })
|
||||||
|
const race = Promise.race([manual, tools.host.sleepy({ id: 3, ms: 60000 })])
|
||||||
|
const all = Promise.all([manual, "plain"])
|
||||||
|
const any = Promise.any([manual, new Promise(() => {})])
|
||||||
|
settle("manual")
|
||||||
|
return [await race, await all, await any]
|
||||||
|
`),
|
||||||
|
).toEqual(["manual", ["manual", "plain"], "manual"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("resolving with a pending promise adopts its later settlement", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
let innerResolve, innerReject
|
||||||
|
const adopted = new Promise((resolve) => resolve(new Promise((resolve) => { innerResolve = resolve })))
|
||||||
|
const adoptedRejection = new Promise((resolve) => resolve(new Promise((_, reject) => { innerReject = reject })))
|
||||||
|
innerResolve("later")
|
||||||
|
innerReject("bad")
|
||||||
|
try {
|
||||||
|
return [await adopted, await adoptedRejection]
|
||||||
|
} catch (reason) {
|
||||||
|
return [await adopted, reason]
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
).toEqual(["later", "bad"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("an async executor's post-await resolve settles the promise", async () => {
|
||||||
|
expect(
|
||||||
|
await value(`
|
||||||
|
const result = new Promise(async (resolve) => {
|
||||||
|
const id = await tools.host.sleepy({ id: 5, ms: 5 })
|
||||||
|
resolve(id * 2)
|
||||||
|
})
|
||||||
|
return await result
|
||||||
|
`),
|
||||||
|
).toBe(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("a never-settled promise is abandoned silently at the return", async () => {
|
||||||
|
const result = await run(`
|
||||||
|
const forever = new Promise(() => {})
|
||||||
|
forever.then(() => {})
|
||||||
|
return "done"
|
||||||
|
`)
|
||||||
|
expect(result.ok).toBe(true)
|
||||||
|
if (!result.ok) return
|
||||||
|
expect(result.value).toBe("done")
|
||||||
|
expect(result.warnings).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("an un-awaited constructed rejection is reported like any unhandled rejection", async () => {
|
||||||
|
const result = await run(`
|
||||||
|
new Promise((_, reject) => reject(new Error("dropped")))
|
||||||
|
await Promise.resolve()
|
||||||
|
await Promise.resolve()
|
||||||
|
return "done"
|
||||||
|
`)
|
||||||
|
expect(result.ok).toBe(true)
|
||||||
|
if (!result.ok) return
|
||||||
|
expect(result.value).toBe("done")
|
||||||
|
expect(result.warnings).toHaveLength(1)
|
||||||
|
expect(result.warnings?.[0].message).toContain("Unhandled rejection")
|
||||||
|
expect(result.warnings?.[0].message).toContain("dropped")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("resolver functions cannot cross the data boundary", async () => {
|
||||||
|
const diagnostic = await error(`
|
||||||
|
let escaped
|
||||||
|
new Promise((resolve) => { escaped = resolve })
|
||||||
|
return { escaped }
|
||||||
|
`)
|
||||||
|
expect(diagnostic.kind).toBe("InvalidDataValue")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue