Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5245,7 +5245,15 @@ export class ClassDynamicDispatch {
if (methodInfo) {
owner = methodInfo.declarer;
params = methodInfo.sig.params;
result = methodInfo.sig.ret;
// An untyped async method's `dyn` result is its promise's payload; the
// call answers the promise. An async generator method answers its
// generator object, which its signature already returns.
result =
methodInfo.sig.async === true &&
!methodInfo.sig.gen &&
methodInfo.sig.ret.kind !== "promise"
? { kind: "promise", inner: methodInfo.sig.ret }
: methodInfo.sig.ret;
Comment thread
vercel[bot] marked this conversation as resolved.
callee = `%${owner.def.name}.${method}`;
virtual = lowerer.overrideBelow(owner, method) || methodInfo.sig.abstract === true;
} else if (
Expand Down
6 changes: 5 additions & 1 deletion packages/compiler/src/frontend/lowering/class-prototypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,16 @@ export function classPrototypeData(
continue;
const compiled = classMethodValue(lowerer, access, info, method, locOf(access));
if (!compiled) continue;
// A method value without a checked-dynamic form (an async method's
// promise-returning signature) gets no data slot.
const boxedValue = lowerer.coerceToExpected(compiled, DYN);
if (boxedValue.type.kind !== "dyn") continue;
const descriptor: IrExpr = {
kind: "dynObjLit",
fields: [
{
key: { kind: "strLit", value: "value", type: STRING, loc },
value: lowerer.coerceToExpected(compiled, DYN),
value: boxedValue,
},
{
key: { kind: "strLit", value: "writable", type: STRING, loc },
Expand Down
10 changes: 9 additions & 1 deletion packages/compiler/src/frontend/lowering/lower-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6723,14 +6723,22 @@ function thunkAsyncSlot(
): void {
if (overridden.sig.async !== true) return;
const declarer = overridden.declarer;
// The slot answers the body's promise: an untyped async method's `dyn`
// result is the promise's payload, so the slot's ABI is that promise. An
// async generator method's slot keeps its generator object.
const ret: IrType =
overridden.sig.gen || overridden.sig.ret.kind === "promise"
? overridden.sig.ret
: { kind: "promise", inner: overridden.sig.ret };
declarer.returnAdapters ??= new Map();
if (!declarer.returnAdapters.has(mName)) {
declarer.returnAdapters.set(mName, {
params: [...overridden.sig.params],
ret: overridden.sig.ret,
ret,
async: true,
});
}
overridden.sig.ret = ret;
delete overridden.sig.async;
}

Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/src/ir/ir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4214,6 +4214,10 @@ export function canConvertToDyn(
(a) =>
a.kind === "undefinedT" ||
isJsonSafeType(a, getRecord, getUnion) ||
// Composite arms (records holding dyn, Map or Set members) box
// through the walker beside the promise arm: a method answering
// either its result or a promise of it.
canBoxDynComposite(a, getRecord, getUnion, visiting) ||
isDynTypedRefType(a) ||
a.kind === "classval" ||
DYN_HANDLE_KINDS.has(a.kind) ||
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6419-untyped-async-package/main.ts"
],
"diags": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6592-promise-armed-method-values/main.ts"
],
"diags": []
}
38 changes: 38 additions & 0 deletions tests/corpus/6419-untyped-async-package/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Untyped async methods of a package, reached through the package's own
// untyped callers and prototype reflection, and overridden by a typed
// subclass. An async generator method called from the package answers its
// generator object.
import { Pool, Schema, limiter, listIssues, prototypeNames, validateAll } from "token-pool";

class CountingPool extends Pool {
acquired = 0;
async acquire(): Promise<void> {
await super.acquire();
this.acquired++;
}
}

async function main(): Promise<void> {
const pool = new CountingPool(1);
await pool.acquire();
const waiting = pool.acquire();
console.log("waiting", pool.nrWaiting());
pool.release("t");
await waiting;
console.log("acquired", pool.acquired, pool.nrWaiting());
const base = new Pool(1);
console.log("token", await base.acquire());
const run = limiter(1);
console.log(await Promise.all([run(async () => 1), run(async () => 2)]));
const schema = new Schema((value) => typeof value === "number");
console.log(await validateAll(schema, [1, "x", 3]));
console.log(await listIssues(schema, ["p", 5, "q"]));
console.log(await schema.parseAsync(4));
try {
await schema.parseAsync("y");
} catch (e) {
console.log("rejected", e instanceof Error ? e.message : String(e));
}
console.log(prototypeNames());
}
main();

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions tests/corpus/6592-promise-armed-method-values/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Parse steps that answer either a status record or a promise of one keep
// their method values: reads through untyped code, detached calls, and the
// per-instance rebinding of the base constructor all see the function.
import { map, number, object, record, set, stepDetached, stepKinds } from "status-schema";
async function main(): Promise<void> {
const shaped = object({ count: number(), size: number() });
const counts = record(number());
const byName = map(number());
const seen = set(number());
console.log(stepKinds(shaped), "|", stepKinds(counts), "|", stepKinds(byName), "|", stepKinds(seen));
console.log(shaped.safeParse({ count: 1, size: 2 }).success, shaped.safeParse({ count: "x", size: 2 }).success);
console.log(JSON.stringify(shaped.parse({ count: 3, size: 4 })));
console.log(JSON.stringify(await shaped.parseAsync({ count: 5, size: 6 })), (await shaped.spa(7)).success);
console.log(counts.safeParse({ a: 1, b: 2 }).success, (await counts.safeParseAsync({ a: "x" })).success);
console.log(byName.safeParse(new Map([["a", 1]])).success, (await byName.safeParseAsync(new Map([["b", "x"]]))).success);
console.log(seen.safeParse(new Set([1, 2])).success, (await seen.spa(new Set([3]))).success);
console.log(await stepDetached(shaped, { count: 1, size: 1 }), "|", await stepDetached(counts, { a: "x" }));
console.log(await stepDetached(byName, new Map([["c", 2]])), "|", await stepDetached(seen, [1]));
try {
await counts.parseAsync(null);
} catch (e) {
console.log("rejected", e instanceof Error ? e.message : String(e));
}
const nested = object({ inner: record(number()), tags: set(number()) });
console.log(JSON.stringify(await nested.parseAsync({ inner: { q: 1 }, tags: new Set([1]) })));
}
main();

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading