From a9ea71df7ddec01a9e0c9984fe3981f4d4b68cf9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 16:22:25 -0500 Subject: [PATCH] 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. --- .../frontend/lowering/expressions/binary.ts | 27 ++++++++++--------- .../union-promise-function-arm-widening.ts | 9 +++++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index 01b48554e..4e46b4d5f 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -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>`): - // 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>`, 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) ) { diff --git a/tests/corpus/union-promise-function-arm-widening.ts b/tests/corpus/union-promise-function-arm-widening.ts index 751af6f41..d3b8f618d 100644 --- a/tests/corpus/union-promise-function-arm-widening.ts +++ b/tests/corpus/union-promise-function-arm-widening.ts @@ -58,6 +58,15 @@ async function main(): Promise { 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));