From c9c19d8fdc76ddbd4855ce2cd6e58d545331a3b9 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 15:39:16 -0500 Subject: [PATCH] Keep the generator result of untyped async generator methods - `%dyn.class.call` dispatch answers a promise for an untyped async method whose signature keeps the promise's payload. An async generator method also has the async flag, but its signature already returns the generator object, so the call now keeps that type. Before, a call from untyped code (`schema.issues(values)` over an `async *issues` method) failed IR validation with "type promise != return generator". - thunkAsyncSlot keeps an async generator method's slot type the same way. - Corpus 6419 adds an async generator method that the package's own untyped helper iterates with `next()`. --- .../src/frontend/lowering/class-dynamic-dispatch.ts | 7 +++++-- .../compiler/src/frontend/lowering/lower-classes.ts | 5 +++-- tests/corpus/6419-untyped-async-package/main.ts | 6 ++++-- .../node_modules/token-pool/index.d.ts | 2 ++ .../node_modules/token-pool/index.js | 13 +++++++++++++ 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index d845bfc54..6c69a80cb 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -5246,9 +5246,12 @@ export class ClassDynamicDispatch { owner = methodInfo.declarer; params = methodInfo.sig.params; // An untyped async method's `dyn` result is its promise's payload; the - // call answers the promise. + // call answers the promise. An async generator method answers its + // generator object, which its signature already returns. result = - methodInfo.sig.async === true && methodInfo.sig.ret.kind !== "promise" + 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}`; diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index a6dd6e3dd..10927e49b 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -6724,9 +6724,10 @@ function thunkAsyncSlot( 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. + // 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.ret.kind === "promise" + overridden.sig.gen || overridden.sig.ret.kind === "promise" ? overridden.sig.ret : { kind: "promise", inner: overridden.sig.ret }; declarer.returnAdapters ??= new Map(); diff --git a/tests/corpus/6419-untyped-async-package/main.ts b/tests/corpus/6419-untyped-async-package/main.ts index e118adba4..9ec10f7c5 100644 --- a/tests/corpus/6419-untyped-async-package/main.ts +++ b/tests/corpus/6419-untyped-async-package/main.ts @@ -1,7 +1,8 @@ // Untyped async methods of a package, reached through the package's own // untyped callers and prototype reflection, and overridden by a typed -// subclass. -import { Pool, Schema, limiter, prototypeNames, validateAll } from "token-pool"; +// 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; @@ -25,6 +26,7 @@ async function main(): Promise { 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"); diff --git a/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.d.ts b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.d.ts index a7a8459ac..ed0c82579 100644 --- a/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.d.ts +++ b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.d.ts @@ -10,6 +10,8 @@ export declare class Schema { constructor(check: (value: unknown) => boolean); safeParseAsync(data: unknown): Promise; parseAsync(data: unknown): Promise; + issues(values: unknown[]): AsyncGenerator; } export declare function validateAll(schema: Schema, values: unknown[]): Promise; +export declare function listIssues(schema: Schema, values: unknown[]): Promise; export declare function prototypeNames(): string; diff --git a/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.js b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.js index 9140e50af..04935ff29 100644 --- a/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.js +++ b/tests/corpus/6419-untyped-async-package/node_modules/token-pool/index.js @@ -88,6 +88,12 @@ class Schema { 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; @@ -104,6 +110,13 @@ async function validateAll(schema, values) { 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; }