Answer promises from thunked and dispatched untyped async methods (#1534)

This commit is contained in:
Chris Tate
2026-10-20 18:11:10 -05:00
committed by GitHub
14 changed files with 382 additions and 3 deletions
@@ -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;
callee = `%${owner.def.name}.${method}`;
virtual = lowerer.overrideBelow(owner, method) || methodInfo.sig.abstract === true;
} else if (
@@ -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 },
@@ -6770,14 +6770,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;
}
+4
View File
@@ -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) ||
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6419-untyped-async-package/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6592-promise-armed-method-values/main.ts"
],
"diags": []
}
@@ -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();
@@ -0,0 +1,17 @@
export declare class Pool {
constructor(size: number);
tryAcquire(): any;
acquire(): Promise<any>;
release(token?: any): void;
nrWaiting(): number;
}
export declare function limiter(size: number): <T>(work: () => Promise<T>) => Promise<T>;
export declare class Schema {
constructor(check: (value: unknown) => boolean);
safeParseAsync(data: unknown): Promise<any>;
parseAsync(data: unknown): Promise<any>;
issues(values: unknown[]): AsyncGenerator<any>;
}
export declare function validateAll(schema: Schema, values: unknown[]): Promise<string>;
export declare function listIssues(schema: Schema, values: unknown[]): Promise<string>;
export declare function prototypeNames(): string;
@@ -0,0 +1,123 @@
"use strict";
// An untyped token pool and schema in the shape of a semaphore and a
// validation library: async methods whose results come from storage that
// passes through untyped helpers.
Object.defineProperty(exports, "__esModule", { value: true });
function moveItems(src, srcIndex, dst, dstIndex, len) {
for (let j = 0; j < len; ++j) {
dst[j + dstIndex] = src[j + srcIndex];
src[j + srcIndex] = void 0;
}
}
class Queue {
constructor() {
this._length = 0;
this.arr = [];
}
push(item) {
if (this._length > 64) moveItems(this.arr, 1, this.arr, 0, this._length - 1);
this.arr[this._length] = item;
this._length = this._length + 1;
}
pop() {
const length = this._length;
if (length === 0) return void 0;
const ret = this.arr[length - 1];
this.arr[length - 1] = void 0;
this._length = length - 1;
return ret;
}
shift() {
const length = this._length;
if (length === 0) return void 0;
const ret = this.arr[0];
for (let i = 1; i < length; i++) this.arr[i - 1] = this.arr[i];
this.arr[length - 1] = void 0;
this._length = length - 1;
return ret;
}
get length() {
return this._length;
}
}
class Pool {
constructor(size) {
this.free = new Queue();
this.waiting = new Queue();
for (let i = 0; i < size; i++) this.free.push("token" + i);
}
tryAcquire() {
return this.free.pop();
}
async acquire() {
const token = this.tryAcquire();
if (token !== void 0) return token;
return new Promise((resolve) => {
this.waiting.push(resolve);
});
}
release(token) {
const next = this.waiting.shift();
if (next) next(token);
else this.free.push(token);
}
nrWaiting() {
return this.waiting.length;
}
}
exports.Pool = Pool;
function limiter(size) {
const pool = new Pool(size);
return async function limited(work) {
const token = await pool.acquire();
try {
return await work();
} finally {
pool.release(token);
}
};
}
exports.limiter = limiter;
class Schema {
constructor(check) {
this.check = check;
this.results = new Queue();
}
async safeParseAsync(data) {
await null;
this.results.push(this.check(data) ? { success: true, data } : { success: false, issue: "invalid " + data });
return this.results.pop();
}
async *issues(values) {
for (const value of values) {
const result = await this.safeParseAsync(value);
if (!result.success) yield result.issue;
}
}
async parseAsync(data) {
const result = await this.safeParseAsync(data);
if (result.success) return result.data;
throw new Error(result.issue);
}
}
exports.Schema = Schema;
async function validateAll(schema, values) {
const out = [];
for (const value of values) {
const result = await schema.safeParseAsync(value);
out.push(result.success ? "ok " + result.data : result.issue);
}
return out.join(", ");
}
exports.validateAll = validateAll;
async function listIssues(schema, values) {
const out = [];
const it = schema.issues(values);
for (let step = await it.next(); !step.done; step = await it.next()) out.push(step.value);
return out.join(", ");
}
exports.listIssues = listIssues;
function prototypeNames() {
return typeof Schema.prototype.parseAsync + " " + typeof Schema.prototype.safeParseAsync;
}
exports.prototypeNames = prototypeNames;
@@ -0,0 +1 @@
{ "name": "token-pool", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" }
@@ -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();
@@ -0,0 +1,18 @@
export interface SafeResult {
success: boolean;
data?: unknown;
}
export declare class Schema {
safeParse(data: unknown): SafeResult;
parse(data: unknown): unknown;
safeParseAsync(data: unknown): Promise<SafeResult>;
spa(data: unknown): Promise<SafeResult>;
parseAsync(data: unknown): Promise<unknown>;
}
export declare function number(): Schema;
export declare function object(shape: { [key: string]: Schema }): Schema;
export declare function record(valueType: Schema): Schema;
export declare function map(valueType: Schema): Schema;
export declare function set(valueType: Schema): Schema;
export declare function stepKinds(schema: Schema): string;
export declare function stepDetached(schema: Schema, data: unknown): Promise<string>;
@@ -0,0 +1,118 @@
// A schema hierarchy in the shape of a validation library's: each parse
// step answers a status record or, in async mode, a promise of one built
// by a static async merge helper. The base constructor rebinds its
// methods per instance, async ones included.
export const INVALID = Object.freeze({ status: "aborted" });
export class Status {
static async mergeAsync(pairs) {
const settled = [];
for (const pair of pairs) {
settled.push({ key: pair.key, value: await pair.value });
}
return Status.mergeSync(settled);
}
static mergeSync(pairs) {
const value = {};
for (const pair of pairs) {
if (pair.value.status === "aborted") return INVALID;
value[pair.key] = pair.value.value;
}
return { status: "valid", value };
}
}
export class Schema {
constructor(def) {
this.spa = this.safeParseAsync;
this._def = def;
this.parse = this.parse.bind(this);
this.safeParse = this.safeParse.bind(this);
this.parseAsync = this.parseAsync.bind(this);
this.safeParseAsync = this.safeParseAsync.bind(this);
this.spa = this.spa.bind(this);
}
safeParse(data) {
const result = this._parse({ data, async: false });
if (result instanceof Promise) throw new Error("async parse step in safeParse");
return result.status === "valid" ? { success: true, data: result.value } : { success: false };
}
parse(data) {
const result = this.safeParse(data);
if (result.success) return result.data;
throw new Error(this._def.name + ": invalid input");
}
async safeParseAsync(data) {
const result = await this._parse({ data, async: true });
return result.status === "valid" ? { success: true, data: result.value } : { success: false };
}
async parseAsync(data) {
const result = await this.safeParseAsync(data);
if (result.success) return result.data;
throw new Error(this._def.name + ": invalid input");
}
}
export class NumberSchema extends Schema {
_parse(input) {
return typeof input.data === "number" ? { status: "valid", value: input.data } : INVALID;
}
}
export class ObjectSchema extends Schema {
_parse(input) {
if (input.data === null || typeof input.data !== "object") return INVALID;
const pairs = [];
for (const key of Object.keys(this._def.shape)) {
pairs.push({ key, value: this._def.shape[key]._parse({ data: input.data[key], async: input.async }) });
}
return input.async ? Status.mergeAsync(pairs) : Status.mergeSync(pairs);
}
}
export class RecordSchema extends Schema {
_parse(input) {
if (input.data === null || typeof input.data !== "object") return INVALID;
const pairs = [];
for (const key in input.data) {
pairs.push({ key, value: this._def.valueType._parse({ data: input.data[key], async: input.async }) });
}
return input.async ? Status.mergeAsync(pairs) : Status.mergeSync(pairs);
}
}
export class MapSchema extends Schema {
_parse(input) {
if (!(input.data instanceof Map)) return INVALID;
const finish = () => {
const out = new Map();
for (const [k, v] of input.data) {
if (this._def.valueType._parse({ data: v, async: false }).status !== "valid") return INVALID;
out.set(k, v);
}
return { status: "valid", value: out };
};
return input.async ? Promise.resolve().then(finish) : finish();
}
}
export class SetSchema extends Schema {
_parse(input) {
if (!(input.data instanceof Set)) return INVALID;
const finish = () => {
const out = new Set();
for (const v of input.data) {
if (this._def.valueType._parse({ data: v, async: false }).status !== "valid") return INVALID;
out.add(v);
}
return { status: "valid", value: out };
};
return input.async ? Promise.resolve().then(finish) : finish();
}
}
export const number = () => new NumberSchema({ name: "number" });
export const object = (shape) => new ObjectSchema({ name: "object", shape });
export const record = (valueType) => new RecordSchema({ name: "record", valueType });
export const map = (valueType) => new MapSchema({ name: "map", valueType });
export const set = (valueType) => new SetSchema({ name: "set", valueType });
// Untyped readers of the parse step's method value.
export const stepKinds = (schema) => typeof schema._parse + " " + typeof Object.getPrototypeOf(schema)._parse;
export const stepDetached = async (schema, data) => {
const step = schema._parse;
const sync = step.call(schema, { data, async: false });
const later = await step.call(schema, { data, async: true });
return sync.status + " " + later.status;
};
@@ -0,0 +1 @@
{ "name": "status-schema", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }