From 99ebf7501d891d2e04463dd0d92bde3786180e75 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 27 Oct 2026 21:17:03 -0700 Subject: [PATCH] Await a union whose promise arm settles a union `Promise | undefined` awaits as Awaited of the operand, which the maybe-promise lowering already computes: it awaits the narrowed promise arm and coerces its settled union, and the non-promise arms take JS's one microtask hop. The await lowering refused any union inner instead, so the promise-or-unit shape now goes through that same path; a union whose promise arm has no combined result keeps the SC1090 fence. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/lower-exprs.ts | 26 ++++++++++++++++--- .../corpus/union-await-promise-arm.ts.json | 6 +++++ tests/corpus/union-await-promise-arm.ts | 24 +++++++++++++++++ .../union-await-promise-arm.ts.json | 1 + 4 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-await-promise-arm.ts.json create mode 100644 tests/corpus/union-await-promise-arm.ts create mode 100644 tests/harness/corpus-costs/union-await-promise-arm.ts.json diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 89ea52bc5..cc380243c 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -26,7 +26,11 @@ import { checkedPrivateReceiver, localPrivateBrandClass } from "./private-brands import { staticCondition } from "./static-typeof.js"; import { constituentTypes } from "../ts7/checker.js"; import type { Lowerer } from "./lowerer.js"; -import { lowerMaybePromiseAwait, maybePromiseUnionOf } from "./maybe-promise.js"; +import { + lowerMaybePromiseAwait, + maybePromiseUnionOf, + promiseArmedUnionOf, +} from "./maybe-promise.js"; import { checkedClassUnionAssertion } from "./class-assertions.js"; import { BIGINT_T, @@ -668,9 +672,23 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { loc, }; } - if (inner.kind === "union" || inner.kind === "dyn") { - // A union inner would need an arm-wise re-tag into the result - + if (inner.kind === "union") { + // A UNION inner (`Promise | undefined`): the awaited + // result is the checker's own `Awaited` of the operand, which + // the maybe-promise lowering already produces — it awaits the + // narrowed promise arm and coerces its settled union, and the + // unit arms keep JS's one-microtask hop. Only the promise-or- + // unit shape reaches here, so the maybe-promise union is the + // same union with its only data arms being units. + const armed = promiseArmedUnionOf(lowerer, value.type); + if (armed) return lowerMaybePromiseAwait(lowerer, expr, value, armed, loc); + lowerer.unsupported( + "SC1090", + expr, + `awaiting '${lowerer.fmt(value.type)}' (the promise's inner type has no combined result union — await the promise arm after narrowing instead)`, + ); + } + if (inner.kind === "dyn") { lowerer.unsupported( "SC1090", expr, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-await-promise-arm.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-await-promise-arm.ts.json new file mode 100644 index 000000000..f62e66a8a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/union-await-promise-arm.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/union-await-promise-arm.ts" + ], + "diags": [] +} diff --git a/tests/corpus/union-await-promise-arm.ts b/tests/corpus/union-await-promise-arm.ts new file mode 100644 index 000000000..91d9f4485 --- /dev/null +++ b/tests/corpus/union-await-promise-arm.ts @@ -0,0 +1,24 @@ +// Awaiting a union whose promise arm settles a union +// (`Promise | undefined`): the promise arm is awaited, and a +// non-promise arm answers its own value after one microtask turn. +function maybe(flag: boolean): Promise | undefined { + if (!flag) return undefined; + return Promise.resolve(flag ? "value" : null); +} + +async function main(): Promise { + const absent = await maybe(false); + console.log(String(absent)); + const settled = await maybe(true); + console.log(String(settled)); + const nil = await maybe(true); + console.log(nil === null); + + // Awaiting a non-promise arm takes exactly one microtask turn. + const order: string[] = []; + Promise.resolve().then(() => order.push("microtask")); + const hopped = await maybe(false); + order.push(`after:${String(hopped)}`); + console.log(order.join(",")); +} +main(); diff --git a/tests/harness/corpus-costs/union-await-promise-arm.ts.json b/tests/harness/corpus-costs/union-await-promise-arm.ts.json new file mode 100644 index 000000000..6eebf7e03 --- /dev/null +++ b/tests/harness/corpus-costs/union-await-promise-arm.ts.json @@ -0,0 +1 @@ +{ "plain": 3150, "san": 4400 }