mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
Convert promise and function values into a union's arm of their kind
A promise or function value flowing into a union whose only arm of that kind has a wider payload or result (`Promise<T>` into `Promise<unknown> | undefined`, `(s) => Promise<string | null>` into an optional `(s) => Promise<string | null | undefined>` member) was refused with SC2003: an arm value wrapped into a union only when the union held an identical arm. It now takes the arm-level conversion (the checked payload widening, the function adapter) and wraps the result. `p === u` against such a union compares both sides as untyped values, so a promise keeps its identity although converting it into the union's arm builds a new promise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8a22e0f515
commit
30e6ca9160
@@ -2388,6 +2388,31 @@ 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;
|
||||
if (
|
||||
!bothUnion &&
|
||||
plainPromise !== null &&
|
||||
lowerer.armTag(ut.unionId, plainPromise.type) < 0 &&
|
||||
unionArms.some((arm) => arm.kind === "promise") &&
|
||||
lowerer.dynConvertible(left.type) &&
|
||||
lowerer.dynConvertible(right.type)
|
||||
) {
|
||||
return {
|
||||
kind: "dynScalarEq",
|
||||
left: lowerer.coerceToExpected(left, DYN),
|
||||
right: lowerer.coerceToExpected(right, DYN),
|
||||
...(negated ? { negated: true as const } : {}),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if ((sameUnion || !bothUnion) && lowerer.eqComparableUnion(ut.unionId)) {
|
||||
aliasRefusal(ut);
|
||||
return {
|
||||
|
||||
@@ -10397,6 +10397,31 @@ export class Lowerer {
|
||||
}
|
||||
}
|
||||
}
|
||||
// A promise or function value against a union whose ONLY arm of
|
||||
// that kind differs from it (`Promise<T>` into `Promise<unknown> |
|
||||
// undefined`, `() => Promise<string | null>` into an optional
|
||||
// `() => Promise<string | null | undefined>` member): convert into
|
||||
// that arm with the arm-level conversion (the checked payload
|
||||
// widening, the function adapter), then wrap. Several arms of the
|
||||
// kind stay ambiguous and fall through to the union fence.
|
||||
if (expr.type.kind === "promise" || expr.type.kind === "func") {
|
||||
const arms = this.unions.get(expected.unionId)?.arms ?? [];
|
||||
const candidates = arms.filter((arm) => arm.kind === expr.type.kind);
|
||||
const arm = candidates.length === 1 ? candidates[0]! : null;
|
||||
if (arm) {
|
||||
const converted = this.coerceToExpected(expr, arm);
|
||||
if (converted !== expr && typeEquals(converted.type, arm)) {
|
||||
return {
|
||||
kind: "unionWrap",
|
||||
unionId: expected.unionId,
|
||||
tag: arms.indexOf(arm),
|
||||
value: converted,
|
||||
type: expected,
|
||||
loc: expr.loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// A width-coercible value against a union: coerce into the SINGLE
|
||||
// width-liftable arm, then wrap like any arm value (widthLiftPlan's
|
||||
// liftWrap — several candidate arms are ambiguous and decline).
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/union-promise-function-arm-widening.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// A promise or function value flowing into a union whose only arm of that
|
||||
// kind has a wider payload or result: the value converts into that arm.
|
||||
// Promises keep their identity under === and !== against the union.
|
||||
|
||||
const chains = new Map<string, Promise<unknown>>();
|
||||
|
||||
async function serialize<T>(key: string, task: () => Promise<T>): Promise<T> {
|
||||
const previous = chains.get(key) ?? Promise.resolve();
|
||||
const queued = (async () => {
|
||||
await previous.catch(() => {});
|
||||
return task();
|
||||
})();
|
||||
const tail = queued.catch(() => {});
|
||||
chains.set(key, tail);
|
||||
void tail.then(() => {
|
||||
if (chains.get(key) === tail) chains.delete(key);
|
||||
console.log(`settled ${key}: ${chains.get(key) !== tail}`);
|
||||
});
|
||||
return queued;
|
||||
}
|
||||
|
||||
interface Store {
|
||||
pending?: Promise<unknown>;
|
||||
lookup?: (name: string) => Promise<string | null | undefined>;
|
||||
scale?: (n: number) => number | string;
|
||||
}
|
||||
|
||||
async function findName(name: string): Promise<string | null> {
|
||||
return name.length > 3 ? name.toUpperCase() : null;
|
||||
}
|
||||
|
||||
function double(n: number): number {
|
||||
return n * 2;
|
||||
}
|
||||
|
||||
async function failing(): Promise<number | undefined> {
|
||||
throw new Error("task failed");
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const a = await serialize("alpha", async () => 1);
|
||||
const b = await serialize("alpha", async () => "two");
|
||||
const c = await serialize("beta", async (): Promise<boolean | undefined> => true);
|
||||
console.log(a, b, c, chains.size);
|
||||
try {
|
||||
await serialize("alpha", failing);
|
||||
} catch (e) {
|
||||
console.log("caught", (e as Error).message);
|
||||
}
|
||||
for (let i = 0; i < 4; i++) await Promise.resolve();
|
||||
console.log("remaining", chains.size);
|
||||
|
||||
const store: Store = { lookup: findName, scale: double };
|
||||
const work: Promise<number> = Promise.resolve(41);
|
||||
store.pending = work;
|
||||
console.log(store.pending === work, store.pending !== work);
|
||||
const other: Promise<number> = Promise.resolve(41);
|
||||
console.log(store.pending === other);
|
||||
console.log(await store.pending);
|
||||
|
||||
const lookup = store.lookup;
|
||||
if (lookup) console.log(await lookup("abcd"), await lookup("ab"));
|
||||
console.log(store.scale?.(21));
|
||||
const empty: Store = {};
|
||||
console.log(empty.pending === work, empty.lookup === undefined, empty.scale?.(1));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 14002, "san": 16102 }
|
||||
Reference in New Issue
Block a user