mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Answer promises from thunked and dispatched untyped async methods (#1534)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) ||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6419-untyped-async-package/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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();
|
||||
+17
@@ -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;
|
||||
+123
@@ -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;
|
||||
+1
@@ -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();
|
||||
Generated
Vendored
+18
@@ -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>;
|
||||
Generated
Vendored
+118
@@ -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;
|
||||
};
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "name": "status-schema", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
|
||||
Reference in New Issue
Block a user