diff --git a/packages/codemode/codemode.md b/packages/codemode/codemode.md
index 8fd1bc5af9..8763a2f121 100644
--- a/packages/codemode/codemode.md
+++ b/packages/codemode/codemode.md
@@ -64,10 +64,13 @@ path lookup, namespace browsing, deterministic ranking, and pagination.
### Tool execution
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`,
-`Promise.resolve`, and `Promise.reject`. Nested functions therefore cannot end the lifetime of work they started.
+async functions, chained `.then`/`.catch`/`.finally` reactions, `new Promise(executor)` constructions, and the
+`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.
-`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
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,
diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md
index 40d0ab6eee..da4f874e65 100644
--- a/packages/codemode/interpreter-support.md
+++ b/packages/codemode/interpreter-support.md
@@ -94,7 +94,7 @@ ultimate source of truth.
- [x] Sequence expressions (the comma operator).
- [x] `await` for sandbox promises; a plain value passes through unchanged, though every `await` still defers its
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] Equality and ordering: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, and `>=`.
- [x] Bitwise operators: `&`, `|`, `^`, `~`, `<<`, `>>`, and `>>>`.
@@ -103,15 +103,15 @@ ultimate source of truth.
- [x] Prefix and postfix `++` and `--`.
- [x] Plain, arithmetic, bitwise, and logical assignment operators.
- [ ] Unary `void` and `delete`.
-- [ ] Arbitrary constructors and `new Promise(...)`.
+- [ ] Arbitrary constructors.
## Promises and tools
- [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] `Promise.resolve` and `Promise.reject`.
-- [x] `Promise.all`, `Promise.allSettled`, and `Promise.race` over supported collections containing promises and plain
- values.
+- [x] `Promise.all`, `Promise.allSettled`, `Promise.race`, and `Promise.any` over supported collections 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.
@@ -130,9 +130,14 @@ ultimate source of truth.
diagnostics. A combinator abandoned inside its final settlement turn counts as pending and is interrupted
without a warning.
- [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).
-- [ ] Custom promise construction with `new Promise(...)`.
- [ ] Async iterables, host streams, and stream consumption.
## Objects and properties
@@ -163,7 +168,7 @@ ultimate source of truth.
- [ ] The mapper and `thisArg` forms of `Array.from`.
- [ ] `Array.prototype.toSpliced`.
- [ ] 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.
## Strings
@@ -268,6 +273,8 @@ 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`.
- [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 interpreter failures and awaited tool failures.
diff --git a/packages/codemode/src/interpreter/model.ts b/packages/codemode/src/interpreter/model.ts
index 212e271e8d..6171b5e008 100644
--- a/packages/codemode/src/interpreter/model.ts
+++ b/packages/codemode/src/interpreter/model.ts
@@ -61,7 +61,7 @@ export class ComputedValue {
export class PromiseNamespace {}
-export type PromiseMethodName = "all" | "allSettled" | "race" | "resolve" | "reject"
+export type PromiseMethodName = "all" | "allSettled" | "race" | "any" | "resolve" | "reject"
export class PromiseMethodReference {
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 =
| "Object"
| "Math"
@@ -131,7 +137,7 @@ export type DiagnosticKind =
export const OptionalShortCircuit: unique symbol = Symbol("codemode.optional-short-circuit")
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 {
readonly node?: AstNode
diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts
index 8e56849c5c..4c751d0142 100644
--- a/packages/codemode/src/interpreter/runtime.ts
+++ b/packages/codemode/src/interpreter/runtime.ts
@@ -1,5 +1,5 @@
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 {
copyIn,
@@ -42,6 +42,7 @@ import {
isRecord,
type MemberReference,
OptionalShortCircuit,
+ PromiseCapabilityFunction,
PromiseInstanceMethodReference,
PromiseMethodReference,
type PromiseMethodName,
@@ -94,6 +95,7 @@ import {
coerceToNumber,
coerceToString,
compoundOperators,
+ createAggregateErrorValue,
createErrorValue,
errorBrandName,
errorConstructors,
@@ -226,11 +228,22 @@ const settleAfterTurn = (body: Effect.Effect): Effect.Effect
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.
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
}
if (typeofValue(value) === "function") {
@@ -250,6 +263,21 @@ const caughtErrorValue = (thrown: unknown): unknown => {
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, 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 =>
value instanceof CodeModeFunction ||
value instanceof ToolReference ||
@@ -262,6 +290,7 @@ const isRuntimeReference = (value: unknown): boolean =>
value instanceof SandboxPromise ||
value instanceof CoercionFunction ||
value instanceof UriFunction ||
+ value instanceof PromiseCapabilityFunction ||
value instanceof ErrorConstructorReference ||
isSandboxValue(value)
@@ -305,6 +334,7 @@ const typeofValue = (value: unknown): string => {
value instanceof PromiseMethodReference ||
value instanceof PromiseInstanceMethodReference ||
value instanceof PromiseNamespace ||
+ value instanceof PromiseCapabilityFunction ||
value instanceof ErrorConstructorReference
)
return "function"
@@ -1584,19 +1614,10 @@ class Interpreter {
const argNodes = getArray(node, "arguments")
const self = this
if (name === "Promise") {
- throw new InterpreterRuntimeError(
- "new Promise(...) is not supported in CodeMode; tool calls already return promises - call the tool and await the result.",
- node,
- "UnsupportedSyntax",
- [supportedSyntaxMessage],
- )
+ return Effect.flatMap(this.evaluateCallArguments(argNodes), (args) => self.constructPromise(args[0], node))
}
if (errorConstructors.has(name)) {
- return Effect.gen(function* () {
- const arg =
- argNodes.length > 0 ? yield* self.evaluateExpression(asNode(argNodes[0], "arguments[0]")) : undefined
- return createErrorValue(name, arg === undefined ? "" : coerceToString(arg))
- })
+ return Effect.map(this.evaluateCallArguments(argNodes), (args) => constructErrorValue(name, args, node))
}
if (valueConstructors.has(name)) {
return Effect.gen(function* () {
@@ -2066,7 +2087,11 @@ class Interpreter {
}
// `Error("msg")` without `new` constructs an error exactly like `new Error("msg")`, as in JS.
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)
})
@@ -2255,8 +2280,8 @@ class Interpreter {
return this.createPromise(Effect.fail(new ProgramThrow(args[0])))
}
- const items = spreadItems(args[0])
- if (items === undefined) {
+ const spread = spreadItems(args[0])
+ if (spread === undefined) {
return this.createPromise(
Effect.fail(
new InterpreterRuntimeError(
@@ -2266,6 +2291,8 @@ class Interpreter {
),
)
}
+ // 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
// belong to the aggregate from this moment, even ones settling before it runs.
@@ -2331,6 +2358,29 @@ class Interpreter {
// and is interrupted at normal completion (already observed) or by teardown.
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 {
)
}
+ // 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 {
+ 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()
+ 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): Effect.Effect {
const invocation = new Interpreter(this.invokeTool, this.toolKeys, this.promises, this.logs, this.callPermits)
invocation.scopes = [...fn.capturedScopes, new Map()]
@@ -2568,7 +2655,8 @@ class Interpreter {
if (
!(callback instanceof CodeModeFunction) &&
!(callback instanceof CoercionFunction) &&
- !(callback instanceof UriFunction)
+ !(callback instanceof UriFunction) &&
+ !(callback instanceof PromiseCapabilityFunction)
) {
throw new InterpreterRuntimeError(`${name} expects a function callback.`, node)
}
@@ -2577,7 +2665,9 @@ class Interpreter {
? Effect.succeed(invokeCoercion(callback, callbackArgs, node))
: callback instanceof UriFunction
? 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(
@@ -3185,7 +3275,7 @@ class Interpreter {
return new PromiseMethodReference(key as PromiseMethodName)
}
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,
)
}
diff --git a/packages/codemode/src/stdlib/promise.ts b/packages/codemode/src/stdlib/promise.ts
index d0b442ccf7..5a7cc5974b 100644
--- a/packages/codemode/src/stdlib/promise.ts
+++ b/packages/codemode/src/stdlib/promise.ts
@@ -1,6 +1,6 @@
import type { PromiseMethodName } from "../interpreter/model.js"
-export const promiseStatics = new Set(["all", "allSettled", "race", "resolve", "reject"])
+export const promiseStatics = new Set(["all", "allSettled", "race", "any", "resolve", "reject"])
/** Maximum number of eagerly forked tool calls that may run concurrently. */
export const TOOL_CALL_CONCURRENCY = 8
diff --git a/packages/codemode/src/stdlib/value.ts b/packages/codemode/src/stdlib/value.ts
index d9b85ea52d..7cdd8cc7dc 100644
--- a/packages/codemode/src/stdlib/value.ts
+++ b/packages/codemode/src/stdlib/value.ts
@@ -6,6 +6,7 @@ export const errorConstructors = new Set([
"ReferenceError",
"EvalError",
"URIError",
+ "AggregateError",
])
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
}
+export const createAggregateErrorValue = (errors: Array, message: string): SafeObject =>
+ Object.assign(createErrorValue("AggregateError", message), { errors })
+
export const errorBrandName = (value: unknown): string | undefined =>
value !== null && typeof value === "object"
? ((value as Record)[ErrorBrand] as string | undefined)
diff --git a/packages/codemode/src/tool-runtime.ts b/packages/codemode/src/tool-runtime.ts
index 7ff0c76b79..ea74930011 100644
--- a/packages/codemode/src/tool-runtime.ts
+++ b/packages/codemode/src/tool-runtime.ts
@@ -611,7 +611,7 @@ export const prepare = (tools: HostTools, catalogBudget = defaultCatalogBu
"## 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, 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.",
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
]
diff --git a/packages/codemode/test/codemode.test.ts b/packages/codemode/test/codemode.test.ts
index b4c06b587c..c9ba85dab2 100644
--- a/packages/codemode/test/codemode.test.ts
+++ b/packages/codemode/test/codemode.test.ts
@@ -672,9 +672,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", "new Promise(...)"]) {
+ for (const missing of ["Modules/imports", "classes", "generators", "fetch"]) {
expect(instructions).toContain(missing)
}
+ expect(instructions).not.toContain("new Promise(...) are unavailable")
expect(instructions).not.toContain("promise chaining")
expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers")
expect(instructions).not.toContain("host globals")
diff --git a/packages/codemode/test/promise-test262.test.ts b/packages/codemode/test/promise-test262.test.ts
index fa0eb246c1..e416ec942c 100644
--- a/packages/codemode/test/promise-test262.test.ts
+++ b/packages/codemode/test/promise-test262.test.ts
@@ -573,13 +573,14 @@ describe("Test262 async functions and await", () => {
})
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 () => {
// 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_T2.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/any/iter-arg-is-number-reject.js
expect(
await value(`
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)
expect(
await value(`
@@ -607,7 +608,7 @@ describe("Test262 expected Promise conformance", () => {
).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)
expect(
await value(`
@@ -619,7 +620,7 @@ describe("Test262 expected Promise conformance", () => {
).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)
expect(
await value(`
@@ -630,6 +631,17 @@ describe("Test262 expected Promise conformance", () => {
).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 () => {
// Sources:
// test/built-ins/Promise/all/S25.4.4.1_A7.2_T1.js
@@ -1002,3 +1014,410 @@ describe("Test262 expected Promise conformance", () => {
).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")
+ })
+})
diff --git a/packages/codemode/test/promise.test.ts b/packages/codemode/test/promise.test.ts
index c74d6fd7df..5ce4d003d5 100644
--- a/packages/codemode/test/promise.test.ts
+++ b/packages/codemode/test/promise.test.ts
@@ -1062,15 +1062,179 @@ describe("unsupported promise surface", () => {
})
test("unknown Promise statics list what is available", async () => {
- const diagnostic = await error(`return await Promise.any([tools.host.sleepy({ id: 1 })])`)
- expect(diagnostic.message).toContain("Promise.any is not available")
- expect(diagnostic.message).toContain("Promise.allSettled")
+ const diagnostic = await error(`return await Promise.withResolvers()`)
+ expect(diagnostic.message).toContain("Promise.withResolvers is not available")
+ 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))`)
- expect(diagnostic.kind).toBe("UnsupportedSyntax")
- expect(diagnostic.message).toContain("new Promise(...) is not supported")
- expect(diagnostic.message).toContain("already return promises")
+describe("Promise.any", () => {
+ test("first tool success wins; failing and losing calls are handled silently", async () => {
+ const trace = makeTrace()
+ 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")
})
})