Compare unions that hold promises as untyped values

A promise converted into a union's promise arm is a new promise, so two
optional slots set to one promise each hold their own conversion, and a
value narrowed out of such a slot may be one too. Union equality compared
those arm payloads by pointer and answered false where Node answers true.

Any comparison between a union with a promise arm and a promise or another
such union now compares both sides as untyped values, where a converted
promise answers with its origin's identity.
This commit is contained in:
Chris Tate
2026-10-25 16:22:25 -05:00
parent 30e6ca9160
commit a9ea71df7d
2 changed files with 24 additions and 12 deletions
@@ -2388,19 +2388,22 @@ export function lowerStrictEquality(
loc,
};
}
// A plain promise against a union whose promise arm has a different
// payload (`chains.get(id) === tail` with a `Map<K, Promise<unknown>>`):
// wrapping would convert the promise into that arm, and the converted
// promise is a new object. Promises are objects to ===, so compare both
// sides as untyped values, where a converted promise answers with its
// origin's identity (the plain promise-vs-promise rule below).
const plainPromise =
left.type.kind === "promise" ? left : right.type.kind === "promise" ? right : null;
// A promise against a union with a promise arm, or two such unions:
// converting a promise into a union's promise arm of another payload
// (`chains.get(id) === tail` with a `Map<K, Promise<unknown>>`, or
// `a.pending === b.pending` after both were set to one promise) builds a
// new promise, so either side may hold a conversion of the other's
// promise, and a value narrowed out of the union may be one too.
// Promises are objects to ===, so compare both sides as untyped values,
// where a converted promise answers with its origin's identity (the plain
// promise-vs-promise rule below).
const holdsPromise = (type: IrType): boolean =>
type.kind === "promise" ||
(type.kind === "union" &&
(lowerer.unions.get(type.unionId)?.arms ?? []).some((arm) => arm.kind === "promise"));
if (
!bothUnion &&
plainPromise !== null &&
lowerer.armTag(ut.unionId, plainPromise.type) < 0 &&
unionArms.some((arm) => arm.kind === "promise") &&
holdsPromise(left.type) &&
holdsPromise(right.type) &&
lowerer.dynConvertible(left.type) &&
lowerer.dynConvertible(right.type)
) {
@@ -58,6 +58,15 @@ async function main(): Promise<void> {
console.log(store.pending === other);
console.log(await store.pending);
// One promise in two optional slots: each holds its own conversion.
const second: Store = {};
second.pending = work;
console.log(store.pending === second.pending, store.pending !== second.pending);
const slots: Store[] = [store, second, { pending: other }];
console.log(slots.map((slot) => slot.pending === slots[0]!.pending).join(","));
second.lookup = findName;
console.log(store.lookup === second.lookup);
const lookup = store.lookup;
if (lookup) console.log(await lookup("abcd"), await lookup("ab"));
console.log(store.scale?.(21));