From a4a84f59da234c7401eed409363ccf19d3d02301 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 27 Oct 2026 21:12:28 -0700 Subject: [PATCH] Resolve a promise-or-unit operand in `Promise.resolve` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Promise.resolve(p)` over `Promise | void` — an async callback's declared result — was refused: the maybe-promise detection required every non-promise arm to carry a value, while a nullish arm is exactly what `Promise.resolve` fulfills as undefined. `promiseOrUnitUnionOf` names that shape, and the resolution reuses the existing adopt-or- fulfill path, so the promise arm passes through unchanged (`Promise.resolve(p) === p`). Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/builtins/promises.ts | 16 +++++++-- .../corpus/promise-resolve-promise-or-unit.ts | 34 +++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 tests/corpus/promise-resolve-promise-or-unit.ts diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index c3b2cd4792..fdd16da90e 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -5,7 +5,11 @@ import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; import { countedFor, varRef } from "../../../ir/build.js"; import { arrayValueStore, optionalElementValue, primitiveElement } from "../array-values.js"; -import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js"; +import { + maybePromiseResolve, + maybePromiseUnionOf, + promiseOrUnitUnionOf, +} from "../maybe-promise.js"; import { isJsSourceFile, locOf } from "../../program.js"; import { BOOL, @@ -670,11 +674,17 @@ export function lowerPromiseStaticCall( if (v.type.kind !== "promise") lowerer.badType(argNode, argT); return v; } - const maybePromise = argIr ? maybePromiseUnionOf(lowerer, argIr) : null; + // A promise-or-unit union (`Promise | void`, + // `Promise | undefined`) resolves exactly like a maybe-promise + // one: Node adopts a promise operand and fulfills every other value + // as itself (a nullish arm fulfills undefined). + const maybePromise = argIr + ? (maybePromiseUnionOf(lowerer, argIr) ?? promiseOrUnitUnionOf(lowerer, argIr)) + : null; if (maybePromise) { const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); const v = lowerer.lowerExpr(argNode); - const mp = maybePromiseUnionOf(lowerer, v.type); + const mp = maybePromiseUnionOf(lowerer, v.type) ?? promiseOrUnitUnionOf(lowerer, v.type); const resolved = resultT?.kind === "promise" && mp ? maybePromiseResolve(lowerer, v, mp, resultT, loc) diff --git a/tests/corpus/promise-resolve-promise-or-unit.ts b/tests/corpus/promise-resolve-promise-or-unit.ts new file mode 100644 index 0000000000..209021ab0d --- /dev/null +++ b/tests/corpus/promise-resolve-promise-or-unit.ts @@ -0,0 +1,34 @@ +// `Promise.resolve(u)` over a promise-or-unit union (`Promise | +// void`): Node adopts a promise operand — the SAME promise comes +// back — and fulfills a nullish one as undefined. The settled turn of the +// adopted promise is unchanged, so a handler chain observes the same +// microtask order either way. + +let runs = 0; + +async function step(label: string): Promise { + runs++; + await Promise.resolve(); + console.log("step", label, runs); +} + +// A callback whose declared result is `Promise | void`: the +// async path answers a promise, the sync one answers undefined. +function handler(flag: boolean): Promise | void { + if (flag) return step("async"); + runs++; + console.log("sync handler", runs); +} + +function call(flag: boolean): Promise { + return Promise.resolve(handler(flag)).then(() => console.log("call done", flag)); +} + +const pending: Promise | void = Promise.resolve(); +console.log("identity", Promise.resolve(pending) === pending); + +console.log("start"); +call(true) + .then(() => call(false)) + .then(() => call(true)) + .then(() => console.log("runs", runs));