mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-04 08:04:37 +08:00
Convert promise and function values into a union's arm of their kind (#2257)
This commit is contained in:
@@ -2388,6 +2388,34 @@ export function lowerStrictEquality(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// 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 (
|
||||
holdsPromise(left.type) &&
|
||||
holdsPromise(right.type) &&
|
||||
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 {
|
||||
|
||||
@@ -10695,6 +10695,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,77 @@
|
||||
// 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);
|
||||
|
||||
// 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));
|
||||
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