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));