mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-06 08:23:19 +08:00
Fill an async result from a returned Promise<never>
`return fatal(...)` where `fatal` is declared `Promise<never>` (a throw-only
async helper) is well-typed — `never` converts into anything — but the returned
value flattens through the returned-promise await into a void, which then
fails the async function's declared record result with SC2002 (`expected
'{ cancelled: boolean }', got 'void'`). The IR keeps `Promise<never>` as a
promise with a void payload, exactly like `Promise<void>`, so the two are
indistinguishable by type alone.
The return now asks the TS payload type: a `never` payload means the promise
never settles, so the awaited value is unreachable and fills the declared
result through divergentValue — the same unreachable-value path a
never-returning call already takes in a value position.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3c5b9eadd2
commit
e41fee0c4a
@@ -13894,6 +13894,19 @@ export class Lowerer {
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether `node`'s TS type is a `Promise<never>` (the throw-only async
|
||||
* function's declared result): the promise can never settle, so the value
|
||||
* it would resolve to is unreachable. The IR keeps `Promise<never>` as a
|
||||
* promise with a void payload, exactly like `Promise<void>`, so the TS
|
||||
* payload type is what separates the two. */
|
||||
private promisePayloadIsNever(node: ts.Expression): boolean {
|
||||
const t = this.typeOf(node);
|
||||
if ((t.flags & ts.TypeFlags.Object) === 0) return false;
|
||||
if (((t as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) === 0) return false;
|
||||
const args = this.checker.getTypeArguments(t as ts.TypeReference);
|
||||
return args.length === 1 && (args[0]!.flags & ts.TypeFlags.Never) !== 0;
|
||||
}
|
||||
|
||||
/** The value of `return <e>` (already lowered untyped) in an async or
|
||||
* void context: a promise flattens through the returned-promise await,
|
||||
* everything else converts into the declared result. */
|
||||
@@ -13901,6 +13914,15 @@ export class Lowerer {
|
||||
if (expected.kind === "void" && e.kind === "unitLit") return null;
|
||||
if (this.ctx.isAsync && e.type.kind === "promise" && expected.kind !== "promise") {
|
||||
const awaited = this.returnedPromiseAwait(e);
|
||||
// A `Promise<never>` never settles, so the awaited value is
|
||||
// unreachable: it fills any result shape instead of failing the
|
||||
// void-to-record conversion.
|
||||
if (
|
||||
expected.kind !== "void" &&
|
||||
e.type.inner.kind === "void" &&
|
||||
this.promisePayloadIsNever(node)
|
||||
)
|
||||
return this.divergentValue(awaited, expected);
|
||||
return this.coerceInto(node, awaited, expected);
|
||||
}
|
||||
return this.coerceInto(node, e, expected);
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/promise-never-return.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// `return fatal(...)` where `fatal` is declared `Promise<never>`: the promise
|
||||
// can never settle, so the awaited value is unreachable and fills the async
|
||||
// function's record result instead of failing a void-to-record conversion.
|
||||
|
||||
interface Outcome {
|
||||
cancelled: boolean;
|
||||
}
|
||||
|
||||
async function fatal(message: string): Promise<never> {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
async function run(flag: boolean): Promise<Outcome> {
|
||||
try {
|
||||
if (flag) throw new Error(`zap ${flag}`);
|
||||
return { cancelled: false };
|
||||
} catch (error: unknown) {
|
||||
return fatal(error instanceof Error ? error.message : "unknown");
|
||||
}
|
||||
}
|
||||
|
||||
async function passthrough(): Promise<number> {
|
||||
try {
|
||||
throw new Error("always");
|
||||
} catch {
|
||||
return fatal("passthrough");
|
||||
}
|
||||
}
|
||||
|
||||
run(false)
|
||||
.then((outcome) => console.log("resolved:", outcome.cancelled))
|
||||
.catch((error) => console.log("rejected:", error instanceof Error ? error.message : error));
|
||||
run(true)
|
||||
.then((outcome) => console.log("resolved:", outcome.cancelled))
|
||||
.catch((error) => console.log("rejected:", error instanceof Error ? error.message : error));
|
||||
passthrough()
|
||||
.then((value) => console.log("value:", value))
|
||||
.catch((error) => console.log("rejected:", error instanceof Error ? error.message : error));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2900, "san": 4100 }
|
||||
Reference in New Issue
Block a user