Await a union whose promise arm settles a union

`Promise<A | B> | 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 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-28 05:37:59 -07:00
co-authored by Claude Opus 5.5
parent a0a6910e4e
commit 99ebf7501d
4 changed files with 53 additions and 4 deletions
@@ -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<A | B> | 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,
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/union-await-promise-arm.ts"
],
"diags": []
}
+24
View File
@@ -0,0 +1,24 @@
// Awaiting a union whose promise arm settles a union
// (`Promise<A | B> | undefined`): the promise arm is awaited, and a
// non-promise arm answers its own value after one microtask turn.
function maybe(flag: boolean): Promise<string | null> | undefined {
if (!flag) return undefined;
return Promise.resolve(flag ? "value" : null);
}
async function main(): Promise<void> {
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();
@@ -0,0 +1 @@
{ "plain": 3150, "san": 4400 }