From a4f743f78167028304f02dc75893fcf00ab7b9c0 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 14 Sep 2026 00:33:35 -0500 Subject: [PATCH] Implement typed async generators --- docs/src/app/limitations/page.mdx | 1 + .../node-v24-compatibility-meta.json | 2 +- .../src/generated/node-v24-compatibility.json | 10 +- .../generated/node-v24-backlog.json | 84 +++-- .../generated/node-v24-internal.json | 81 +++-- internal/compatibility/static-support.json | 2 +- .../ambient/scriptc-node-fallback.d.ts | 7 +- packages/compiler/src/backend/c/async.ts | 33 +- packages/compiler/src/backend/c/c-emitter.ts | 23 +- packages/compiler/src/backend/c/exprs.ts | 62 +++- packages/compiler/src/backend/llvm/emitter.ts | 42 ++- .../compiler/src/backend/llvm/expr-async.ts | 73 +++- .../compiler/src/coverage/surface-manifest.ts | 2 + .../src/frontend/lowering/lower-builtins.ts | 79 ++++- .../src/frontend/lowering/lower-calls.ts | 64 ++-- .../src/frontend/lowering/lower-classes.ts | 50 ++- .../src/frontend/lowering/lower-generators.ts | 166 ++++++++- .../src/frontend/lowering/lower-stmts.ts | 21 +- .../compiler/src/frontend/lowering/lowerer.ts | 14 +- packages/compiler/src/frontend/type-mapper.ts | 18 +- packages/compiler/src/ir/ir.ts | 25 +- packages/compiler/src/ir/validate.ts | 18 +- packages/compiler/surface-manifest.json | 14 + .../test/ts7/baselines/order-parity.json | 60 ++++ packages/runtime/src/scr_async.c | 328 +++++++++++++++++- packages/runtime/src/scr_runtime.h | 20 +- .../corpus/2850-async-generators-protocol.ts | 146 ++++++++ tests/corpus/2851-async-generators-values.ts | 53 +++ tests/corpus/2852-timers-promises-interval.ts | 18 + tests/coverage-fixtures/unreached.ts | 7 +- tests/diagnostics/node-fallback-fence.ts | 2 + tests/diagnostics/private-members.ts | 3 +- .../__snapshots__/coverage-unreached.txt | 7 +- .../__snapshots__/node-fallback-fence.ts.txt | 25 +- .../__snapshots__/private-members.ts.txt | 23 +- 35 files changed, 1357 insertions(+), 226 deletions(-) create mode 100644 tests/corpus/2850-async-generators-protocol.ts create mode 100644 tests/corpus/2851-async-generators-values.ts create mode 100644 tests/corpus/2852-timers-promises-interval.ts diff --git a/docs/src/app/limitations/page.mdx b/docs/src/app/limitations/page.mdx index 1f954c536..bccdcc09f 100644 --- a/docs/src/app/limitations/page.mdx +++ b/docs/src/app/limitations/page.mdx @@ -10,6 +10,7 @@ These are rejected at compile time with an `SC` code, a code frame, and usually - Loose `==`/`!=` comparisons compile between two numbers, two strings, or two booleans, and for the `x == null`/`x != null` nullish idiom. Other coercing comparisons are rejected — use `===`/`!==`. - Block- and function-scoped `using` and `await using` compile for statically represented classes with zero-parameter disposal methods and for the supported FileHandle, timer, immediate, child-process, and readline handles. Array `for (using ... of ...)` loops compile. Top-level and switch-clause declarations, disposal through an engine-held package value, and the `DisposableStack` constructors remain fenced; a caught `SuppressedError` exposes its Node-compatible `name` and `message`, while its `error` and `suppressed` payload properties remain outside the static catch surface. +- Typed async generator functions and methods compile with lazy execution, `await`, direct `yield`, queued `.next()`/`.return()`/`.throw()` requests, and `for await`. Async `yield*` delegation and arbitrary structural async-iterator objects remain fenced; write an explicit `for await` loop for delegation. The `node:timers/promises` `setInterval(delay, value)` async iterator compiles, while its AbortSignal options remain fenced. - Generics monomorphize when the target resolves statically. The remaining edges include generic functions declared inside another function, generic class expressions, generic classes whose base depends on their own type parameters, and generic methods that would require dynamic dispatch. - Generic function values must be pinned at use to a concrete signature and come from a never-reassigned binding; unpinned or rebound values remain fenced. Built-in and standard-library functions often have call-only lowerings, so values such as `const g = Math.floor` are rejected — call them directly. diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index e8d49145d..3ba1d7228 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.15.0", "nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119", - "artifactVersion": "a8a777761f1b2e39b04c", + "artifactVersion": "981ad58e8b022358310b", "rowCount": 3662 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 459b97b66..b649810e3 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -1058,9 +1058,9 @@ "apiEntries": 25, "static": { "supported": 0, - "partial": 19, + "partial": 20, "refused": 0, - "not-implemented": 6, + "not-implemented": 5, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -78290,9 +78290,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" }, "dynamic": { "status": "partial", diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index f0844b634..301cfe30b 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -8,8 +8,8 @@ "tiers": { "static": { "replace-refusal": 73, - "verify-gap": 2760, - "audit-partial": 375, + "verify-gap": 2759, + "audit-partial": 376, "classify": 3, "implement": 93 }, @@ -45352,7 +45352,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -45391,7 +45392,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -45430,7 +45432,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -45537,7 +45540,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -45576,7 +45580,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -45615,7 +45620,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -85484,7 +85490,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -85523,7 +85530,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -85562,7 +85570,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -85601,7 +85610,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -85640,7 +85650,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -85679,7 +85690,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -85718,7 +85730,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -85757,7 +85770,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -85788,12 +85802,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:timers", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-dedicated:timersPromises.setInterval", + "tests": [ + "tests/corpus/1804-timers-module.ts", + "tests/corpus/1805-timer-callback-args.ts", + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" + ] }, "dynamic": { "status": "partial", @@ -85930,7 +85949,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -85967,7 +85987,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -86004,7 +86025,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -86142,7 +86164,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -86179,7 +86202,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -86216,7 +86240,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -86253,7 +86278,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index f95f647f3..f14284d57 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -1179,9 +1179,9 @@ "apiEntries": 25, "static": { "supported": 0, - "partial": 19, + "partial": 20, "refused": 0, - "not-implemented": 6, + "not-implemented": 5, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -59091,6 +59091,7 @@ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts", "tests/harness/fetch-conformance.test.ts" ] }, @@ -60968,7 +60969,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -61003,7 +61005,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -61038,7 +61041,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -61130,7 +61134,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -61165,7 +61170,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -61200,7 +61206,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -102335,7 +102342,8 @@ "tests/corpus/2093-timers-promises.ts", "tests/corpus/2672-http-request-response-callback.ts", "tests/corpus/2689-http-server-timeout-properties.ts", - "tests/corpus/2844-node-disposable-resources.ts" + "tests/corpus/2844-node-disposable-resources.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -102397,7 +102405,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -102432,7 +102441,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -102467,7 +102477,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -102529,7 +102540,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -102564,7 +102576,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -102599,7 +102612,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -102661,7 +102675,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -102696,7 +102711,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -102726,8 +102742,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:timers" + "status": "partial", + "evidence": "compiler-dedicated:timersPromises.setInterval", + "tests": [ + "tests/corpus/1804-timers-module.ts", + "tests/corpus/1805-timer-callback-args.ts", + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" + ] }, "dynamic": { "status": "partial", @@ -102842,7 +102864,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -102874,7 +102897,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -102906,7 +102930,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -103022,7 +103047,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -103054,7 +103080,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -103086,7 +103113,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { @@ -103118,7 +103146,8 @@ "tests": [ "tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", - "tests/corpus/2093-timers-promises.ts" + "tests/corpus/2093-timers-promises.ts", + "tests/corpus/2852-timers-promises-interval.ts" ] }, "dynamic": { diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 121046777..27575b98a 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -94,7 +94,7 @@ { "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.getEventListeners", "events.listenerCount", "events.once", "events.on", "events.setMaxListeners"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] }, { "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs"] }, { "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts"] }, - { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts"] }, + { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts"] }, { "symbols": ["stream.finished", "stream.pipeline", "stream.getDefaultHighWaterMark", "readable.push", "readable.unshift", "readable.read", "readable.pause", "readable.resume", "readable.isPaused", "readable.setEncoding", "readable.pipe", "readable.unpipe", "readable.destroy", "writable.write", "writable.end", "writable.cork", "writable.uncork", "writable.setDefaultEncoding"], "status": "partial", "evidence": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", "tests/corpus/1814-stream-pipeline.cjs"] }, { "symbols": ["net.createServer", "net.connect", "net.createConnection", "net.getDefaultAutoSelectFamilyAttemptTimeout", "net.setDefaultAutoSelectFamilyAttemptTimeout", "server.address", "server.close", "server.listen", "socket.destroy", "socket.end", "socket.pause", "socket.resume", "socket.setEncoding", "socket.setTimeout", "socket.write"], "status": "partial", "evidence": ["tests/corpus/2500-net-autosel-timeout.ts", "tests/corpus/2596-net-arg-ladders.cjs"] }, { "symbols": ["dgram.createSocket", "socket.address", "socket.bind", "socket.close", "socket.connect", "socket.ref", "socket.remoteAddress", "socket.send", "socket.unref"], "status": "partial", "evidence": ["tests/corpus/2597-dgram-send-ladders.cjs"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index ed7eb2285..98dd50121 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -777,12 +777,13 @@ declare module "timers" { /* node:timers/promises — the promisified pair the Node test harness leans * on (`await setTimeout(ms)` as a sleep). setTimeout's delay-only form and * setImmediate's bare form lower (a void promise the shared timer heap - * settles); the resolve-value, options (AbortSignal), and setInterval - * async-iterator forms are declared surface that fences per site. */ + * settles); setInterval(delay, value) lowers to a typed async iterator. + * Resolve values on the one-shot pair and AbortSignal options remain + * declared surface that fences per site. */ declare module "node:timers/promises" { export function setTimeout(delay?: number): Promise; export function setImmediate(): Promise; - export function setInterval(delay?: number, value?: unknown, options?: unknown): unknown; + export function setInterval(delay?: number, value?: T, options?: unknown): AsyncGenerator; export const scheduler: { wait(delay?: number): Promise; yield(): Promise; diff --git a/packages/compiler/src/backend/c/async.ts b/packages/compiler/src/backend/c/async.ts index 0b3949fba..2a677b0f9 100644 --- a/packages/compiler/src/backend/c/async.ts +++ b/packages/compiler/src/backend/c/async.ts @@ -71,7 +71,7 @@ function emitArgPackAndTrampolinePrologue( * their closure env through the pack (+1, released after the body). */ export function emitAsyncScaffolding(emitter: CEmitter, out: string[]): void { for (const fn of emitter.mod.functions) { - if (!fn.async) continue; + if (!fn.async || fn.generator !== undefined) continue; const { definitions, ret, lines, spawnParams, argPackLines } = emitArgPackAndTrampolinePrologue(fn); out.push(...definitions); @@ -199,6 +199,33 @@ function emitArgPackAndTrampolinePrologue( ); out.push(...lines); + let settleAsync: string | null = null; + if (fn.async) { + const genT: IrType & { kind: "generator" } = { + kind: "generator", + async: true, + yieldT: fn.generator.yieldT, + retT: ret, + nextT: fn.generator.nextT, + }; + const resultT = fn.generator.resultType; + const build = genResultThunkFor(emitter, genT, resultT); + settleAsync = `${build}_async`; + const settleKey = `async:${typeKey(genT)}`; + if (!emitter.genResThunks.has(settleKey)) { + emitter.genResThunks.set(settleKey, settleAsync); + const adapters = vAdapters(resultT); + out.push(`static void ${settleAsync}(ScrGen *sc_g, ScrPromise *sc_p);`); + emitter.walkerDefs.push( + `static void ${settleAsync}(ScrGen *sc_g, ScrPromise *sc_p) {`, + ` ${mangleRecordStruct(resultT.shapeId)} *sc_r = ${build}(sc_g);`, + ` scr_promise_fulfill_ref(sc_p, sc_r, ${adapters.retain}, ${adapters.release}, ${emitter.traceArgC(resultT)});`, + `}`, + ``, + ); + } + } + out.push( `static void ${mangleGenDrop(fn.name)}(void *sc_ap0) {`, ` ${pack} sc_a = *(${pack} *)sc_ap0;`, @@ -211,7 +238,9 @@ function emitArgPackAndTrampolinePrologue( `}`, `static ScrGen *${mangleGenSpawn(fn.name)}(${spawnParams.join(", ") || "void"}) {`, ...argPackLines, - ` return scr_gen_new(&${mangleTrampoline(fn.name)}, sc_ap, &${mangleGenDrop(fn.name)});`, + settleAsync === null + ? ` return scr_gen_new(&${mangleTrampoline(fn.name)}, sc_ap, &${mangleGenDrop(fn.name)});` + : ` return scr_async_gen_new(&${mangleTrampoline(fn.name)}, sc_ap, &${mangleGenDrop(fn.name)}, &${settleAsync});`, `}`, ); } diff --git a/packages/compiler/src/backend/c/c-emitter.ts b/packages/compiler/src/backend/c/c-emitter.ts index f67c3b40f..4ea94f7a4 100644 --- a/packages/compiler/src/backend/c/c-emitter.ts +++ b/packages/compiler/src/backend/c/c-emitter.ts @@ -60,7 +60,7 @@ import { cFnPtrCast, cType, releaseCallC, cStringLiteral, cDecl } from "./types. import { computeMayThrow } from "./may-throw.js"; import { unionTruthyHelper, unionEqHelper, unionToStrHelper, unionJoinHelper, jsonWriteHelper, jsonIndentHelper, dynMatchHelper, dynCheckHelper, dynFuncBoxHelper, dynToStrHelper, caughtToDynHelper, toDynHelper, recordKeyGetHelper, recordKeySetHelper } from "./walkers.js"; import { VtSlot, ClassMeta, emitStructDefs, vtEntriesFor, vtSlotParams, emitVtableDecls, emitVtableInstances, emitVtAdapterDefs, emitHierarchyClassHelpers, emitClassObjs, emitCtorThunkDefs, errorVtStampLines, emitterVtStampLines, streamVtStampLines, traceAdapterC, traceArgC, boxNewC, arrNewC } from "./shapes.js"; -import { emitAsyncScaffolding, childDataThunkFor, childExitThunkFor, childExitSignalThunkFor, closeBindThunkFor, connectResThunkFor, connectSockThunkFor, closeOverrideWrapFor, dgramMsgThunkFor, dnsLookupThunkFor, fsRenameThunkFor, netLookupAnswerThunkFor, emitterInvokeThunkFor, streamCbThunkFor, streamDataThunkFor, raceAdapterFor, resolveThunkFor, sniAnswerThunkFor } from "./async.js"; +import { emitAsyncScaffolding, childDataThunkFor, childExitThunkFor, childExitSignalThunkFor, closeBindThunkFor, connectResThunkFor, connectSockThunkFor, closeOverrideWrapFor, dgramMsgThunkFor, dnsLookupThunkFor, fsRenameThunkFor, genResultThunkFor, netLookupAnswerThunkFor, emitterInvokeThunkFor, streamCbThunkFor, streamDataThunkFor, raceAdapterFor, resolveThunkFor, sniAnswerThunkFor } from "./async.js"; import { emitNpmEmbedding, islandAdapter, islandTypedAdapter } from "./island.js"; import { emitFunction, emitBlock, emitStmts, emitStmt, emitTryCatch, emitSwitch, mergeBrace, emitBranchInto, emitCondition } from "./stmts.js"; import { emitExpr } from "./exprs.js"; @@ -649,6 +649,23 @@ export class CEmitter { emit(): string { const body: string[] = []; + // Async-generator spawn wrappers need their type-directed result + // builders even when user code only creates and drops the iterator. + // Register them before the unit-instance table flushes below. + for (const fn of this.mod.functions) { + if (!fn.async || fn.generator === undefined) continue; + genResultThunkFor( + this, + { + kind: "generator", + async: true, + yieldT: fn.generator.yieldT, + retT: fn.returnType, + nextT: fn.generator.nextT, + }, + fn.generator.resultType, + ); + } // Function bodies are emitted first (into this.lines) so the literal // table is complete; the file is then assembled around them. for (const fn of this.mod.functions) { @@ -797,7 +814,7 @@ export class CEmitter { const fn = this.fnByName.get(name)!; const params = ["ScrClosure *sc_env", ...fn.params.map((p) => cDecl(p.type, mangleLocal(p.localId)))]; const call = `${this.callTargetC(name)}(${fn.params.map((p) => mangleLocal(p.localId)).join(", ")})`; - const retType = fn.async ? "ScrPromise *" : fn.generator ? "ScrGen *" : cType(fn.returnType); + const retType = fn.generator ? "ScrGen *" : fn.async ? "ScrPromise *" : cType(fn.returnType); out.push( ``, `static ${retType}${retType.endsWith("*") ? "" : " "}${mangleWrapper(name)}(${params.join(", ")}) {`, @@ -1684,8 +1701,8 @@ export class CEmitter { * fiber and returns the generator object). */ callTargetC(fnName: string): string { const fn = this.fnByName.get(fnName); - if (fn?.async === true) return mangleAsyncSpawn(fnName); if (fn?.generator !== undefined) return mangleGenSpawn(fnName); + if (fn?.async === true) return mangleAsyncSpawn(fnName); return mangleFunction(fnName); } diff --git a/packages/compiler/src/backend/c/exprs.ts b/packages/compiler/src/backend/c/exprs.ts index 8713ad7f4..04e0eca0f 100644 --- a/packages/compiler/src/backend/c/exprs.ts +++ b/packages/compiler/src/backend/c/exprs.ts @@ -3375,6 +3375,7 @@ function emitAsyncExpr( if (e.value === null) throw new InternalCompilerError("emitter bug: yieldExpr with no operand (frontend fills undefined)"); const v = emitter.emitExpr(e.value); const yt = e.value.type; + if (e.awaited) emitter.line(`scr_async_gen_hop_done();`); if (yt.kind === "f64" || yt.kind === "date") { emitter.line(`scr_gen_yield_f64(${v.name});${emitter.srcComment(e.loc)}`); } else if (yt.kind === "bool") { @@ -3405,13 +3406,70 @@ function emitAsyncExpr( // build the IteratorResult record through the interned helper. const genT = e.gen.type; if (genT.kind !== "generator") throw new InternalCompilerError("emitter bug: genResume on a non-generator"); - if (e.type.kind !== "record") throw new InternalCompilerError("emitter bug: genResume result is not a record"); + const resultT = genT.async ? (e.type.kind === "promise" ? e.type.inner : null) : e.type; + if (resultT?.kind !== "record") throw new InternalCompilerError("emitter bug: genResume result is not an IteratorResult record"); const g = emitter.emitExpr(e.gen); // borrowed for the calls below const sendArg = (store: (a: Temp) => string): void => { const a = emitter.emitExpr(e.arg!); if (isRefCounted(e.arg!.type)) emitter.moveTemp(a); // the slot takes ownership emitter.line(store(a)); }; + if (genT.async) { + let call: string; + if (e.mode === "next") { + if (e.arg === null) { + if (genT.nextT.kind === "dyn") { + call = `scr_async_gen_next_ref(${g.name}, scr_dyn_retain(scr_dyn_undefined()), scr_dyn_release_v)`; + } else { + call = `scr_async_gen_next_none(${g.name})`; + } + } else { + const a = emitter.emitExpr(e.arg); + const t = e.arg.type; + if (isRefCounted(t)) emitter.moveTemp(a); + call = t.kind === "f64" || t.kind === "date" + ? `scr_async_gen_next_f64(${g.name}, ${a.name})` + : t.kind === "bool" + ? `scr_async_gen_next_bool(${g.name}, ${a.name})` + : `scr_async_gen_next_ref(${g.name}, ${a.name}, ${vAdapters(t).release})`; + } + } else if (e.mode === "return") { + if (e.arg === null) { + call = `scr_async_gen_return_none(${g.name})`; + } else { + const a = emitter.emitExpr(e.arg); + const t = e.arg.type; + if (isRefCounted(t)) emitter.moveTemp(a); + call = t.kind === "f64" || t.kind === "date" + ? `scr_async_gen_return_f64(${g.name}, ${a.name})` + : t.kind === "bool" + ? `scr_async_gen_return_bool(${g.name}, ${a.name})` + : `scr_async_gen_return_ref(${g.name}, ${a.name}, ${vAdapters(t).release})`; + } + } else { + if (e.arg === null) throw new InternalCompilerError("emitter bug: async genResume throw with no payload"); + const a = emitter.emitExpr(e.arg); + const t = e.arg.type; + if (isRefCounted(t)) emitter.moveTemp(a); + if (t.kind === "date") { + throw new InternalCompilerError("emitter bug: Date async-generator throw reached backend"); + } else if (t.kind === "f64") { + emitter.line(`scr_throw_f64(${a.name});${emitter.srcComment(e.loc)}`); + } else if (t.kind === "bool") { + emitter.line(`scr_throw_bool(${a.name});${emitter.srcComment(e.loc)}`); + } else if (t.kind === "string") { + emitter.line(`scr_throw_str(${a.name});${emitter.srcComment(e.loc)}`); + } else if (t.kind === "object" && emitter.classMeta.get(t.className)?.hierarchy) { + const rc = vAdapters(t); + emitter.line(`scr_throw_obj(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`); + } else { + const rc = vAdapters(t); + emitter.line(`scr_throw_ref(${a.name}, &${rc.retain}, &${rc.release}, ${emitter.traceArgC(t)});${emitter.srcComment(e.loc)}`); + } + call = `scr_async_gen_throw(${g.name})`; + } + return emitter.newTemp(e.type, call); + } if (e.mode === "next") { if (e.arg === null) { // Valueless resume: dyn channels read JS's undefined; unit @@ -3466,7 +3524,7 @@ function emitAsyncExpr( } emitter.line(`scr_gen_resume_throw(${g.name});`); } - const helper = genResultThunkFor(emitter, genT, e.type); + const helper = genResultThunkFor(emitter, genT, resultT); // The record builds before the check so an unwind (a propagated // body exception) releases it as the frame's never-read dummy. return emitter.fallibleTemp(e.type, `${helper}(${g.name})`); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index b3c8a001e..455f0aab2 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -79,7 +79,7 @@ import type { IrUnionDef, SrcLoc, } from "../../ir/ir.js"; -import { CAUGHT, ffiCallbackType, isFfiContextParam, isRefCounted, isUnitType, moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, moduleUsesDynInvoke, moduleUsesFetch, moduleUsesFsWatch, moduleUsesHttpServer, moduleUsesNet, moduleUsesNodeTest, moduleUsesProcessEvents, moduleUsesStream, moduleUsesTls, moduleUsesTlsCa, NPM_COMPRESS_MIN, POINTER_KINDS, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, VOID } from "../../ir/ir.js"; +import { CAUGHT, ffiCallbackType, isFfiContextParam, isRefCounted, isUnitType, moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, moduleUsesDynInvoke, moduleUsesFetch, moduleUsesFsWatch, moduleUsesHttpServer, moduleUsesNet, moduleUsesNodeTest, moduleUsesProcessEvents, moduleUsesStream, moduleUsesTls, moduleUsesTlsCa, NPM_COMPRESS_MIN, POINTER_KINDS, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, typeKey, VOID } from "../../ir/ir.js"; import { matchIntegerBytesForLoop } from "../../ir/integer-loops.js"; import { allocateFfiCallbackAdapters, hasForeignFfiCallback, hasRetainedFfiCallback, type FfiCallbackAdapter } from "../ffi-callbacks.js"; import { RUNTIME_ABI_MARKER } from "../runtime-abi.js"; @@ -1885,7 +1885,7 @@ class LlEmitter { ); } for (const fn of this.mod.functions) { - if (fn.async !== true) continue; + if (fn.async !== true || fn.generator !== undefined) continue; const { definitions, ret, tr, spawnParams, argPackLines } = this.emitArgPackAndTrampolinePrologue(fn); out.push(...definitions); @@ -2095,6 +2095,36 @@ class LlEmitter { } out.push(...tr); + let settleAsync: string | null = null; + if (fn.async) { + this.declare(`declare ptr @scr_async_gen_new(ptr, ptr, ptr, ptr)`); + const genT: IrType & { kind: "generator" } = { + kind: "generator", + async: true, + yieldT: fn.generator.yieldT, + retT: ret, + nextT: fn.generator.nextT, + }; + const resultT = fn.generator.resultType; + const build = this.genResultThunkFor(genT, resultT); + settleAsync = `${build}_async`; + const settleKey = `ags:${typeKey(genT)}`; + if (!this.resolveThunks.has(settleKey)) { + this.resolveThunks.set(settleKey, settleAsync); + const adapters = vAdapters(this, resultT); + this.resolveThunkDefs.push( + `define internal void @${settleAsync}(ptr %g, ptr %p) ${FN_ATTRS} {`, + `entry:`, + ` %r = call ptr @${build}(ptr %g)`, + ` call void @scr_promise_fulfill_ref(ptr %p, ptr %r, ptr ${adapters.retain}, ptr ${adapters.release}, ptr ${traceArg(this, resultT)})`, + ` ret void`, + `}`, + ``, + ); + } + this.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`); + } + // The never-started teardown: drop the packed (+1) arguments. const dr: string[] = [ `define internal void @${mangleGenDrop(fn.name)}(ptr %ap) ${FN_ATTRS} {`, @@ -2129,7 +2159,9 @@ class LlEmitter { ...argPackLines, ]; sp.push( - ` %gg = call ptr @scr_gen_new(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)})`, + settleAsync === null + ? ` %gg = call ptr @scr_gen_new(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)})` + : ` %gg = call ptr @scr_async_gen_new(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)}, ptr @${settleAsync})`, ` ret ptr %gg`, `}`, ``, @@ -2789,8 +2821,8 @@ class LlEmitter { * fiber and returns the generator object) — CEmitter.callTargetC. */ private callTarget(fnName: string): string { const fn = this.fnByName.get(fnName); - if (fn?.async === true) return mangleAsyncSpawn(fnName); if (fn?.generator !== undefined) return mangleGenSpawn(fnName); + if (fn?.async === true) return mangleAsyncSpawn(fnName); return mangleFunction(fnName); } @@ -2925,7 +2957,7 @@ class LlEmitter { B.line(`call void @scr_wasi_coro_started(ptr ${handle})`); B.line(`${self} = call ptr @scr_fiber_self()`); this.currentWasiCoro = { - kind: fn.async === true ? "async" : "generator", + kind: fn.generator !== undefined ? "generator" : "async", id, handle, self, diff --git a/packages/compiler/src/backend/llvm/expr-async.ts b/packages/compiler/src/backend/llvm/expr-async.ts index c5c53bf63..4ee85730b 100644 --- a/packages/compiler/src/backend/llvm/expr-async.ts +++ b/packages/compiler/src/backend/llvm/expr-async.ts @@ -282,6 +282,10 @@ export function emitAsyncExpr(host: LlvmEmitterContext, e: ExprOf<"yieldExpr" | if (e.value === null) throw new InternalCompilerError("llvm emitter bug: yieldExpr with no operand (frontend fills undefined)"); const v = host.emitExpr(e.value); const yt = e.value.type; + if (e.awaited) { + host.declare(`declare void @scr_async_gen_hop_done()`); + B.line(`call void @scr_async_gen_hop_done()`); + } if (yt.kind === "f64" || yt.kind === "date") { if (host.wasi) { const coro = host.currentWasiCoro!; @@ -349,7 +353,8 @@ export function emitAsyncExpr(host: LlvmEmitterContext, e: ExprOf<"yieldExpr" | // build the IteratorResult record through the interned helper. const genT = e.gen.type; if (genT.kind !== "generator") throw new InternalCompilerError("llvm emitter bug: genResume on a non-generator"); - if (e.type.kind !== "record") throw new InternalCompilerError("llvm emitter bug: genResume result is not a record"); + const resultT = genT.async ? (e.type.kind === "promise" ? e.type.inner : null) : e.type; + if (resultT?.kind !== "record") throw new InternalCompilerError("llvm emitter bug: genResume result is not an IteratorResult record"); const g = host.emitExpr(e.gen); // borrowed for the calls below const sendArg = (store: (aName: string, t: IrType) => void): void => { const a = host.emitExpr(e.arg!); @@ -368,6 +373,70 @@ export function emitAsyncExpr(host: LlvmEmitterContext, e: ExprOf<"yieldExpr" | B.line(`call void @scr_gen_in_ref(ptr ${g.name}, ptr ${name}, ptr ${vAdapters(host, t).release})`); } }; + if (genT.async) { + let call: string; + if (e.mode === "next") { + if (e.arg === null) { + if (genT.nextT.kind === "dyn") { + host.declare(`declare ptr @scr_dyn_undefined()`); + host.declare(`declare ptr @scr_dyn_retain_v(ptr)`); + host.declare(`declare void @scr_dyn_release_v(ptr)`); + host.declare(`declare ptr @scr_async_gen_next_ref(ptr, ptr, ptr)`); + const u = B.tmp(); + const owned = B.tmp(); + B.line(`${u} = call ptr @scr_dyn_undefined()`); + B.line(`${owned} = call ptr @scr_dyn_retain_v(ptr ${u})`); + call = `call ptr @scr_async_gen_next_ref(ptr ${g.name}, ptr ${owned}, ptr @scr_dyn_release_v)`; + } else { + host.declare(`declare ptr @scr_async_gen_next_none(ptr)`); + call = `call ptr @scr_async_gen_next_none(ptr ${g.name})`; + } + } else { + const a = host.emitExpr(e.arg); + const t = e.arg.type; + if (isRefCounted(t)) host.moveTemp(a); + if (t.kind === "f64" || t.kind === "date") { + host.declare(`declare ptr @scr_async_gen_next_f64(ptr, double)`); + call = `call ptr @scr_async_gen_next_f64(ptr ${g.name}, double ${a.name})`; + } else if (t.kind === "bool") { + host.declare(`declare ptr @scr_async_gen_next_bool(ptr, i1 zeroext)`); + call = `call ptr @scr_async_gen_next_bool(ptr ${g.name}, i1 ${a.name})`; + } else { + host.declare(`declare ptr @scr_async_gen_next_ref(ptr, ptr, ptr)`); + call = `call ptr @scr_async_gen_next_ref(ptr ${g.name}, ptr ${a.name}, ptr ${vAdapters(host, t).release})`; + } + } + } else if (e.mode === "return") { + if (e.arg === null) { + host.declare(`declare ptr @scr_async_gen_return_none(ptr)`); + call = `call ptr @scr_async_gen_return_none(ptr ${g.name})`; + } else { + const a = host.emitExpr(e.arg); + const t = e.arg.type; + if (isRefCounted(t)) host.moveTemp(a); + if (t.kind === "f64" || t.kind === "date") { + host.declare(`declare ptr @scr_async_gen_return_f64(ptr, double)`); + call = `call ptr @scr_async_gen_return_f64(ptr ${g.name}, double ${a.name})`; + } else if (t.kind === "bool") { + host.declare(`declare ptr @scr_async_gen_return_bool(ptr, i1 zeroext)`); + call = `call ptr @scr_async_gen_return_bool(ptr ${g.name}, i1 ${a.name})`; + } else { + host.declare(`declare ptr @scr_async_gen_return_ref(ptr, ptr, ptr)`); + call = `call ptr @scr_async_gen_return_ref(ptr ${g.name}, ptr ${a.name}, ptr ${vAdapters(host, t).release})`; + } + } + } else { + if (e.arg === null) throw new InternalCompilerError("llvm emitter bug: async genResume throw with no payload"); + const a = host.emitExpr(e.arg); + if (isRefCounted(e.arg.type)) host.moveTemp(a); + host.emitThrowValue({ name: a.name, type: e.arg.type }); + host.declare(`declare ptr @scr_async_gen_throw(ptr)`); + call = `call ptr @scr_async_gen_throw(ptr ${g.name})`; + } + const p = B.tmp(); + B.line(`${p} = ${call}`); + return host.own({ name: p, type: e.type }); + } if (e.mode === "next") { if (e.arg === null) { if (genT.nextT.kind === "dyn") { @@ -423,7 +492,7 @@ export function emitAsyncExpr(host: LlvmEmitterContext, e: ExprOf<"yieldExpr" | host.declare(`declare void @scr_gen_resume_throw(ptr)`); B.line(`call void @scr_gen_resume_throw(ptr ${g.name})`); } - const helper = host.genResultThunkFor(genT, e.type); + const helper = host.genResultThunkFor(genT, resultT); // The record builds before the check so an unwind (a propagated // body exception) releases it as the frame's never-read dummy. const t = B.tmp(); diff --git a/packages/compiler/src/coverage/surface-manifest.ts b/packages/compiler/src/coverage/surface-manifest.ts index 583066876..b34179857 100644 --- a/packages/compiler/src/coverage/surface-manifest.ts +++ b/packages/compiler/src/coverage/surface-manifest.ts @@ -169,6 +169,8 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes }); } for (const [id, name, note] of [ + ["async-generators", "typed async generator functions and methods", "lazy bodies with queued next/return/throw requests, await, direct yield, and Node-compatible completion promises"], + ["for-await-async-generators", "for await over typed async generators", "awaits each IteratorResult and closes the generator on early break"], ["using-declarations", "block- and function-scoped using declarations", "LIFO disposal on normal and abrupt scope exit through [Symbol.dispose]"], ["await-using-declarations", "block- and function-scoped await using declarations", "LIFO asynchronous disposal through [Symbol.asyncDispose], with synchronous fallback"], ["for-using-of", "for (using ... of ...) over arrays", "each array element is disposed at the end of its iteration, including break and continue paths"], diff --git a/packages/compiler/src/frontend/lowering/lower-builtins.ts b/packages/compiler/src/frontend/lowering/lower-builtins.ts index ff85f69b9..cfefdc955 100644 --- a/packages/compiler/src/frontend/lowering/lower-builtins.ts +++ b/packages/compiler/src/frontend/lowering/lower-builtins.ts @@ -30,7 +30,7 @@ import { import { conditionalSpreadOf, droppableStatic, lowerAbsenceProbe, lowerDynObjectLiteral } from "./lower-exprs.js"; import { HTTP2_CONSTANTS } from "./http2-constants.js"; import { CRYPTO_CIPHERS, CRYPTO_CONSTANTS, CRYPTO_CURVES, CRYPTO_HASHES } from "./crypto-tables.js"; -import { timerStyleCallback } from "./lower-calls.js"; +import { generatorMeta, timerStyleCallback } from "./lower-calls.js"; import { registerHttpClientFnBinding, voidizedCallback } from "./lower-server.js"; import { pairsSnapshotHelper } from "./pairs-snapshot.js"; import { BOOL, BYTES_U8, CHILD_T, CHILDSTREAM_T, DYN, F64, FILEHANDLE_T, FSWATCHER_T, PROCSTREAM_T, IrExpr, IrFunction, IrLibFn, IrLocal, IrStmt, IrType, JSVAL, NULL_T, SEARCH_PARAMS_T, SPAWNRES_T, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, funcOf, isUnitType, typeEquals, typeKey } from "../../ir/ir.js"; @@ -45,6 +45,83 @@ function optionalStringTags(lowerer: Lowerer, type: IrType): { stringTag: number return stringTag >= 0 && undefinedTag >= 0 ? { stringTag, undefinedTag } : null; } +/** timers/promises.setInterval(delay, value), the first Node API built on + * the generic async-generator protocol. The supported form has an explicit + * value and no AbortSignal options. It lowers to a generated typed async + * generator whose loop awaits the existing promise timeout then yields the + * retained value; creating the iterator remains lazy. */ +export function lowerTimersPromisesSetInterval( + lowerer: Lowerer, + expr: ts.CallExpression, + bi: { module: string; member: string }, + loc: SrcLoc, +): IrExpr | null { + if (bi.module !== "timers/promises" || bi.member !== "setInterval") return null; + if (expr.arguments.some(ts.isSpreadElement)) { + lowerer.unsupported("SC1090", expr, "spread arguments"); + } + if (expr.arguments.length !== 2) { + lowerer.noLowering( + `timers/promises.setInterval with ${expr.arguments.length} arguments`, + expr, + "the lowered form is setInterval(delay, value) with an explicit yielded value; AbortSignal options are not supported yet", + ); + } + const delayNode = expr.arguments[0]!; + const valueNode = expr.arguments[1]!; + const delay = lowerer.lowerExpr(delayNode); + const ms = delay.kind === "unitLit" + ? { kind: "numLit", value: 1, type: F64, loc } satisfies IrExpr + : lowerer.coerceInto(delayNode, delay, F64); + const value = lowerer.lowerExpr(valueNode); + const callType = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (callType?.kind !== "generator" || !callType.async) { + lowerer.badType(expr, lowerer.typeOf(expr)); + } + const genT = callType; + const yielded = lowerer.coerceInto(valueNode, value, genT.yieldT); + const fnName = `%fn${lowerer.lambdaCounter++}_tpInterval`; + const msParam: IrLocal = { id: "%tp.ms", name: "delay", type: F64, mutable: false }; + const valueParam: IrLocal = { id: "%tp.value", name: "value", type: genT.yieldT, mutable: false }; + const promiseVoid: IrType = { kind: "promise", inner: VOID }; + const msRef = (): IrExpr => ({ kind: "varRef", localId: msParam.id, type: F64, loc }); + const valueRef = (): IrExpr => ({ kind: "varRef", localId: valueParam.id, type: genT.yieldT, loc }); + const fn: IrFunction = { + name: fnName, + params: [ + { localId: msParam.id, name: msParam.name, type: msParam.type }, + { localId: valueParam.id, name: valueParam.name, type: valueParam.type }, + ], + returnType: VOID, + locals: [msParam, valueParam], + async: true, + generator: generatorMeta(lowerer, genT), + body: [ + { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [ + { + kind: "exprStmt", + expr: { + kind: "awaitExpr", + value: { kind: "libCall", fn: "tp.setTimeout", args: [msRef()], type: promiseVoid, loc }, + type: VOID, + loc, + }, + loc, + }, + { kind: "exprStmt", expr: { kind: "yieldExpr", value: valueRef(), type: VOID, loc }, loc }, + ], + loc, + }, + ], + loc, + }; + lowerer.liftedFns.push(fn); + return { kind: "call", callee: fnName, args: [ms, yielded], type: genT, loc }; +} + function lowerBuiltinValuePreservingUndefined(lowerer: Lowerer, node: ts.Expression): IrExpr { return lowerer.runtimeOptionalIdentifierValue(node)?.value ?? lowerAbsenceProbe(lowerer, node) ?? diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 141a90866..1da534a4b 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,3 +1,4 @@ +import { InternalCompilerError } from "../../errors.js"; /* Call lowering: the lowerCall dispatch chain, parameter-shape analysis and * argument completion (optional/default/rest, explicit-undefined ≡ omission), * function/lambda lowering and signature collection, and monomorphizing @@ -8,7 +9,7 @@ import { lowerGenMethodCall } from "./lower-generators.js"; import { BOOL, CAUGHT, DYN, F64, IrExpr, IrFunction, IrLocal, IrParam, IrStmt, IrType, JSVAL, STRING, SYMBOL_T, SrcLoc, UNDEFINED_T, VOID, arrayOf, canBoxFuncIntoDyn, canConvertToDyn, canDynCheckTo, canMarshalTypedFuncIntoIsland, ffiClassType, ffiSourceParamTypes, funcOf, isFfiCallbackParam, isFfiContextParam, isFfiReleaseParam, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/ir.js"; import type { IrFfiCallbackParam, IrFfiCallbackParamClass, IrFfiImport, IrFfiReleaseParam } from "../../ir/ir.js"; import { isJsSourceFile, locOf } from "../program.js"; -import { isGenericCallableMemberType, typeKey } from "../type-mapper.js"; +import { genResultRecord, isGenericCallableMemberType, typeKey } from "../type-mapper.js"; import { PoisonError, dynFallbackType, dynUndefinedExpr, importCallHandleType, jsFuncNameOf, newFnCtx, nodeThrowExpr } from "./lowerer.js"; import { enforceLibBoundary } from "./lib-boundary.js"; import { NARROW_FIRST, builtinFenceHintOf, builtinModuleFnOf } from "./surfaces.js"; @@ -57,11 +58,23 @@ export interface FnSig { /** Call-site result type — Promise for async functions, the * generator type for generator functions. */ returnType: IrType; - /** Async: the IrFunction's returnType is the promise's INNER type. */ + /** Async: the IrFunction's returnType is the promise's INNER type, or + * the generator's TReturn when generator is also present. */ isAsync?: boolean; /** Generator: the IrFunction's returnType is the TReturn channel; the * yield/next channels ride here (IrFunction.generator's exact shape). */ - generator?: { yieldT: IrType; nextT: IrType }; + generator?: { yieldT: IrType; nextT: IrType; resultType: IrType & { kind: "record" } }; +} + +export function generatorMeta( + lowerer: Lowerer, + type: IrType & { kind: "generator" }, +): NonNullable { + const resultType = genResultRecord(type.yieldT, type.retT, lowerer.shapes, lowerer.unions); + if (resultType === null) { + throw new InternalCompilerError("lowerer bug: mapped generator without an IteratorResult record"); + } + return { yieldT: type.yieldT, nextT: type.nextT, resultType }; } /** Instantiation cap per generic function: same-key recursion (`len` @@ -870,9 +883,6 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara if (!decl.typeParameters && mixinFnShapeOf(lowerer, decl)) return; const isAsync = decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; const isGenerator = decl.asteriskToken !== undefined; - if (isGenerator && isAsync) { - lowerer.unsupported("SC1071", decl, "async generators (async function*)"); - } if (decl.typeParameters) { // Generic async composes: each monomorphized instance is an async // IrFunction like any other — its own spawn wrapper, its body @@ -927,10 +937,13 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara } const nameBlame: ts.Node = decl.name ?? decl; const returnType = lowerer.declaredReturnType(decl, nameBlame); - if (isAsync && returnType.kind !== "promise") { + if (isAsync && !isGenerator && returnType.kind !== "promise") { lowerer.badType(nameBlame, lowerer.typeOf(nameBlame)); } - if (isGenerator && returnType.kind !== "generator") { + if ( + isGenerator && + (returnType.kind !== "generator" || (returnType.async === true) !== isAsync) + ) { lowerer.badType(nameBlame, lowerer.typeOf(nameBlame)); } @@ -946,7 +959,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara returnType, isAsync, ...(isGenerator && returnType.kind === "generator" - ? { generator: { yieldT: returnType.yieldT, nextT: returnType.nextT } } + ? { generator: generatorMeta(lowerer, returnType) } : {}), }); } @@ -1381,17 +1394,17 @@ export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnIn // and awaits park this instance's fibers. const isAsync = decl.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; const nameBlame: ts.Node = (ts.isArrowFunction(decl) ? undefined : decl.name) ?? decl; - if (isAsync && inst.returnType.kind !== "promise") { + const isGenerator = decl.asteriskToken !== undefined; + if (isAsync && !isGenerator && inst.returnType.kind !== "promise") { lowerer.badType(nameBlame, lowerer.checker.getTypeAtLocation(nameBlame)); } // A generic GENERATOR instance mirrors async: the body returns the // resolved TReturn channel, calls enter through the instance's own // gen-spawn wrapper (the emitter routes by fn.generator). - const isGenerator = decl.asteriskToken !== undefined; - if (isGenerator && isAsync) { - lowerer.unsupported("SC1071", decl, "async generators (async function*)"); - } - if (isGenerator && inst.returnType.kind !== "generator") { + if ( + isGenerator && + (inst.returnType.kind !== "generator" || (inst.returnType.async === true) !== isAsync) + ) { lowerer.badType(nameBlame, lowerer.checker.getTypeAtLocation(nameBlame)); } let bodyReturn = isGenerator @@ -1405,7 +1418,7 @@ export function genericFnOf(lowerer: Lowerer, ident: ts.Identifier): GenericFnIn if (inst.implicitInferReturn) fnCtx.inferReturn = { entries: [] }; if (cls && info.member!.kind === "method") lowerer.currentClass = cls; if (isGenerator && inst.returnType.kind === "generator") { - fnCtx.generator = { yieldT: inst.returnType.yieldT, nextT: inst.returnType.nextT }; + fnCtx.generator = generatorMeta(lowerer, inst.returnType); } lowerer.fnStack.push(fnCtx); try { @@ -3656,6 +3669,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { // bake at the call site — no runtime entry exists to table. const cryptoServed = lowerer.lowerCryptoModuleCall(expr, bi, loc); if (cryptoServed) return cryptoServed; + const timersInterval = lowerer.lowerTimersPromisesSetInterval(expr, bi, loc); + if (timersInterval) return timersInterval; const builtinFn = builtinModuleFnOf(lowerer, bi.module, bi.member); if (!builtinFn) { // Typed by @types/node (the fallback declarations only declare @@ -5777,12 +5792,6 @@ function loweredTemplateStrings( * value rule (requireExactArityValue decides who may become a value). */ export function lambdaSignature(lowerer: Lowerer, node: ts.ArrowFunction | ts.FunctionExpression | ts.FunctionDeclaration | ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration,): { shapes: ParamShape[]; funcType: IrType & { kind: "func" } } { if (!node.body) lowerer.unsupported("SC1090", node, "function overload signatures"); - if ( - node.asteriskToken && - node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) - ) { - lowerer.unsupported("SC1071", node, "async generators (async function*)"); - } if (node.typeParameters) { // Generic function-like forms monomorphize only where a static home // exists: top-level generic function declarations, generic methods @@ -5984,12 +5993,15 @@ function loweredTemplateStrings( const isAsync = !ts.isAccessor(node) && node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; - if (isAsync && funcType.ret.kind !== "promise") lowerer.badType(node, lowerer.typeOf(node)); + const isGenerator = node.asteriskToken !== undefined; + if (isAsync && !isGenerator && funcType.ret.kind !== "promise") lowerer.badType(node, lowerer.typeOf(node)); // Generator lambdas (function* expressions and object-literal // *methods): the VALUE's type returns the generator; the lifted body // returns the TReturn channel (a `return v` is the done-value). - const isGenerator = node.asteriskToken !== undefined; - if (isGenerator && funcType.ret.kind !== "generator") lowerer.badType(node, lowerer.typeOf(node)); + if ( + isGenerator && + (funcType.ret.kind !== "generator" || (funcType.ret.async === true) !== isAsync) + ) lowerer.badType(node, lowerer.typeOf(node)); const bodyReturn = isGenerator ? lowerer.genBodyReturnType(funcType.ret) : lowerer.bodyReturnType(isAsync, funcType.ret); @@ -5997,7 +6009,7 @@ function loweredTemplateStrings( const fnCtx = newFnCtx(true, selfSymbol, funcType, bodyReturn); fnCtx.isAsync = isAsync; if (isGenerator && funcType.ret.kind === "generator") { - fnCtx.generator = { yieldT: funcType.ret.yieldT, nextT: funcType.ret.nextT }; + fnCtx.generator = generatorMeta(lowerer, funcType.ret); } const diagsBefore = lowerer.diags.length; lowerer.fnStack.push(fnCtx); diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index fb50bdd29..42991fc24 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -7,7 +7,7 @@ import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { BOOL, DATE_T, DYN, F64, bytesOf, IrClassDef, IrExpr, IrFunction, IrLocal, IrParam, IrStmt, IrType, JSVAL, RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, STRING, SrcLoc, UNDEFINED_T, URL_T, VOID, arrayOf, isSupportedMapKey, isUnitType, typeEquals } from "../../ir/ir.js"; -import { MAX_GENERIC_INSTANCES, genericCallInstance, implicitAnyParamSymbolsOf, implicitCallInstance, implicitMonoFile, omittedArgFor, type GenericFnInfo, type ParamShape } from "./lower-calls.js"; +import { MAX_GENERIC_INSTANCES, generatorMeta, genericCallInstance, implicitAnyParamSymbolsOf, implicitCallInstance, implicitMonoFile, omittedArgFor, type GenericFnInfo, type ParamShape } from "./lower-calls.js"; import { isGenericCallableMemberType, typeKey } from "../type-mapper.js"; import { cjsClassExprWholeExportOf, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeTypesPath, locOf } from "../program.js"; import { PoisonError, dynFallbackType, dynUndefinedExpr, newFnCtx, own } from "./lowerer.js"; @@ -45,7 +45,7 @@ export interface ClassInfo { * no-dynamic-dispatch semantics by construction). A `gen` entry is a * #private GENERATOR method: the body is a generator IrFunction and * calls enter through its gen-spawn wrapper. */ - methods: Map; + methods: Map }>; /** OWN GENERIC instance methods (own type parameters — `m(x: T)`), * monomorphized per call site like top-level generic functions: instance * `n` is the module function `%C.m%n` taking `this` as param 0. They @@ -1099,7 +1099,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla const fields = new Map(base ? base.fields : []); const symbolFields = new Map(base?.symbolFields ?? []); const fieldOrder: ClassInfo["fieldOrder"] = []; - const methods = new Map(); + const methods = new Map }>(); // Own accessor declarations ("get:x"/"set:x" → node), for the // partial-override analysis below (diagnostics need the node). const accessorNodes = new Map(); @@ -1601,29 +1601,20 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla } else if (ts.isMethodDeclaration(member)) { const mName = classMemberNameOf(lowerer, member.name); if (mName === null) lowerer.unsupported("SC1090", member, "computed method names"); - // PUBLIC generator METHODS stay fenced (virtualCall dispatch - // over gen-spawn wrappers has no story yet); module-level - // function* and object-literal *methods compile — and #PRIVATE - // generator methods (`*#walk()`) compile below: privates never - // enter vtables (a subclass redeclaration is fenced, so - // overrideBelow can never flip), every call is a direct call the - // emitter routes through the gen-spawn wrapper with `this` as - // param 0 — the async-method precedent, generator form. - if (member.asteriskToken !== undefined && !ts.isPrivateIdentifier(member.name)) { + // Sync PUBLIC generator methods stay fenced because virtualCall + // dispatch cannot hold spawn wrappers. Async generator methods + // use the existing static async-method dispatch discipline; + // overrides fence below. #private generators are always direct. + const asyncGenerator = + member.asteriskToken !== undefined && + member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true; + if (member.asteriskToken !== undefined && !asyncGenerator && !ts.isPrivateIdentifier(member.name)) { lowerer.unsupported( "SC1071", member, "generator methods (a #private generator method compiles — privates never dispatch dynamically; or declare a module-level function* and call it from the method)", ); } - // An async #private generator (`async *#m()`) is still an async - // generator — the blanket SC1071 fence. - if ( - member.asteriskToken !== undefined && - member.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) - ) { - lowerer.unsupported("SC1071", member, "async generators (async function*)"); - } // An ABSTRACT method is a signature with no body — type-world, // except that it declares the vtable slot: calls through // base-typed receivers are ordinary virtual dispatch, and tsc @@ -1811,7 +1802,12 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla // emitted gen-spawn wrapper, answering the suspended generator. if (member.asteriskToken !== undefined) { if (ft.ret.kind !== "generator") lowerer.badType(member.name, lowerer.typeOf(member.name)); - methods.set(mName, { params: shapes, ret: ft.ret, gen: { yieldT: ft.ret.yieldT, nextT: ft.ret.nextT } }); + methods.set(mName, { + params: shapes, + ret: ft.ret, + gen: generatorMeta(lowerer, ft.ret), + ...(asyncMember ? { async: true as const } : {}), + }); } else { methods.set(mName, asyncMember ? { params: shapes, ret: ft.ret, async: true as const } : { params: shapes, ret: ft.ret }); } @@ -3472,7 +3468,7 @@ export function collectClassShapeInner(lowerer: Lowerer, decl: ts.ClassLikeDecla /** The nearest declaration of `name` at or above `info` — the method a * receiver of that static class runs when nothing below overrides it. */ export function findMethodOn(lowerer: Lowerer, info: ClassInfo | null, - name: string,): { declarer: ClassInfo; sig: { params: ParamShape[]; ret: IrType; abstract?: true; async?: true; gen?: { yieldT: IrType; nextT: IrType } } } | null { + name: string,): { declarer: ClassInfo; sig: { params: ParamShape[]; ret: IrType; abstract?: true; async?: true; gen?: NonNullable } } | null { for (let c = info; c; c = c.base) { const sig = c.methods.get(name); if (sig) return { declarer: c, sig }; @@ -4086,17 +4082,17 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction // (callTargetC routes by fn.async; `this` rides as param 0 in the // spawn's argument pack). Dispatch is static by construction — the // override fence at collection keeps async methods out of vtables. - const isAsync = sig.async === true && sig.ret.kind === "promise"; + const isAsync = sig.async === true; // #PRIVATE GENERATOR methods: the module function is a generator // IrFunction — the body returns the TReturn channel, yields ride // ctx.generator, and every call (direct by construction — privates // never virtualize) enters through the emitted gen-spawn wrapper with // `this` in the argument pack, answering the suspended generator. const genCh = sig.gen !== undefined && sig.ret.kind === "generator" ? sig.gen : null; - const bodyReturn = isAsync && sig.ret.kind === "promise" - ? sig.ret.inner - : genCh !== null - ? lowerer.genBodyReturnType(sig.ret) + const bodyReturn = genCh !== null + ? lowerer.genBodyReturnType(sig.ret) + : isAsync && sig.ret.kind === "promise" + ? sig.ret.inner : sig.ret; const fnCtx = newFnCtx(false, null, null, bodyReturn); fnCtx.isAsync = isAsync; diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts index 105154d55..0ba2a1fe2 100644 --- a/packages/compiler/src/frontend/lowering/lower-generators.ts +++ b/packages/compiler/src/frontend/lowering/lower-generators.ts @@ -14,7 +14,7 @@ import { locOf } from "../program.js"; import { genResultRecord } from "../type-mapper.js"; import { forOfVarTarget } from "./lower-stmts.js"; -type GenType = IrType & { kind: "generator" }; +export type GenType = IrType & { kind: "generator" }; /** The interned IteratorResult record of a generator type (never null for * a MAPPED generator — mapType required it to intern). */ @@ -49,8 +49,8 @@ export function lowerYield(lowerer: Lowerer, expr: ts.YieldExpression): IrExpr { const loc = locOf(expr); const gen = lowerer.ctx.generator; if (!gen) { - // A yield in a body this compiler did not lower as a generator (an - // async generator's, a comptime callback's) — the blanket fence. + // A yield in a body this compiler did not lower as a generator (for + // example a comptime callback) — the blanket fence. lowerer.unsupported("SC1071", expr); } if (expr.asteriskToken) { @@ -61,8 +61,19 @@ export function lowerYield(lowerer: Lowerer, expr: ts.YieldExpression): IrExpr { ); } let value: IrExpr; + let awaited = false; if (expr.expression) { - value = lowerer.lowerExprExpecting(expr.expression, gen.yieldT); + const raw = lowerer.lowerExpr(expr.expression); + if (lowerer.ctx.isAsync && raw.type.kind === "promise") { + const awaitedValue: IrExpr = { kind: "awaitExpr", value: raw, type: raw.type.inner, loc: raw.loc }; + value = lowerer.coerceInto(expr.expression, awaitedValue, gen.yieldT); + // AsyncGeneratorYield itself awaits its operand. The explicit IR + // await above performs that one required hop; the runtime must settle + // in the same continuation instead of inserting another. + awaited = true; + } else { + value = lowerer.coerceInto(expr.expression, raw, gen.yieldT); + } } else { const u = channelUndefined(lowerer, gen.yieldT, loc); if (!u) { @@ -78,7 +89,7 @@ export function lowerYield(lowerer: Lowerer, expr: ts.YieldExpression): IrExpr { // the expression is void (statement position; the checker types reads of // it undefined, whose uses fence downstream). const type = gen.nextT.kind === "undefinedT" ? VOID : gen.nextT; - return { kind: "yieldExpr", value, type, loc }; + return { kind: "yieldExpr", value, ...(awaited ? { awaited: true as const } : {}), type, loc }; } /** `g.next(v)` / `g.return(v)` / `g.throw(e)` on a generator-typed @@ -101,6 +112,7 @@ export function lowerGenMethodCall( if (gen.type.kind !== "generator") return null; const genT = gen.type; const recT = resultRecordOf(lowerer, genT); + const resultT: IrType = genT.async ? { kind: "promise", inner: recT } : recT; const argNode = call.arguments[0]; let arg: IrExpr | null = null; if (name === "next") { @@ -169,7 +181,7 @@ export function lowerGenMethodCall( ); } } - return { kind: "genResume", mode: name, gen, arg, type: recT, loc }; + return { kind: "genResume", mode: name, gen, arg, type: resultT, loc }; } /** Extraction of a suspended resume's yield value out of the result @@ -231,6 +243,9 @@ export function lowerForOfGenerator( iterable: IrExpr & { type: GenType }, labels?: string[], ): IrStmt { + if (iterable.type.async) { + lowerer.unsupported("SC1070", stmt.expression, "synchronous for-of over an async generator (use 'for await')"); + } if (!ts.isVariableDeclarationList(stmt.initializer)) { lowerer.unsupported( "SC1090", @@ -359,6 +374,138 @@ export function lowerForOfGenerator( } } +/** `for await (const x of asyncGen)` — the async sibling of + * lowerForOfGenerator. Each resume produces Promise, so + * the loop awaits it before testing done and extracting the typed value. + * Early break awaits `.return()` to run the generator's finally blocks. */ +export function lowerForAwaitGenerator( + lowerer: Lowerer, + stmt: ts.ForOfStatement, + iterable: IrExpr & { type: GenType }, + labels?: string[], +): IrStmt { + if (!iterable.type.async) { + lowerer.unsupported("SC1070", stmt.expression, "for-await over this synchronous generator"); + } + if (!lowerer.ctx.isAsync) { + lowerer.unsupported("SC1090", stmt, "top-level 'for await' (await outside async functions)"); + } + if (!ts.isVariableDeclarationList(stmt.initializer)) { + lowerer.unsupported( + "SC1090", + stmt.initializer, + "for-await over a pre-declared variable (declare the loop variable in the loop: for await (const value of ...))", + ); + } + const list = stmt.initializer; + if ((list.flags & ts.NodeFlags.Using) !== 0) { + lowerer.unsupported("SC1090", list, "'await using' loop bindings over async generators"); + } + const isConst = (list.flags & ts.NodeFlags.Const) !== 0; + const isLet = (list.flags & ts.NodeFlags.Let) !== 0; + if (!isConst && !isLet) lowerer.unsupported("SC1030", list, "'var' loop bindings in 'for await' (use const)"); + const decl = list.declarations[0]!; + if (!ts.isIdentifier(decl.name)) lowerer.unsupported("SC1031", decl.name); + const genT = iterable.type; + if (genT.yieldT.kind === "void") { + lowerer.unsupported("SC1090", stmt.expression, "for-await over an async generator that never yields"); + } + if (genT.nextT.kind !== "undefinedT" && genT.nextT.kind !== "dyn") { + lowerer.unsupported( + "SC1090", + stmt.expression, + `for-await over an async generator whose yields expect .next(value) ('${lowerer.fmt(genT.nextT)}' — drive it with .next() calls instead)`, + ); + } + const loc = locOf(stmt); + const recT = resultRecordOf(lowerer, genT); + const promiseT: IrType = { kind: "promise", inner: recT }; + const shape = lowerer.shapes.get(recT.shapeId)!; + const valueT = shape.fields.find((f) => f.name === "value")!.type; + lowerer.scopes.push(new Map()); + try { + const g = lowerer.declareHiddenLocal("%fag", genT); + const done = lowerer.declareHiddenLocal("%fagdone", BOOL); + done.mutable = true; + const p = lowerer.declareHiddenLocal("%fagpromise", promiseT); + const r = lowerer.declareHiddenLocal("%fagresult", recT); + const gRef = (): IrExpr => ({ kind: "varRef", localId: g.id, type: genT, loc }); + const rRef = (): IrExpr => ({ kind: "varRef", localId: r.id, type: recT, loc }); + const valueRead: IrExpr = { kind: "recordGet", obj: rRef(), shapeId: recT.shapeId, field: "value", type: valueT, loc }; + const extracted = extractYieldValue(lowerer, genT, valueT, valueRead, loc); + if (!extracted) { + lowerer.unsupported( + "SC1090", + stmt.expression, + `for-await over an async generator yielding '${lowerer.fmt(genT.yieldT)}' (no per-element extraction exists)`, + ); + } + const x = lowerer.declareLocal(decl.name, decl.name.text, genT.yieldT, isLet); + const head: IrStmt[] = [ + { + kind: "varDecl", + localId: p.id, + init: { kind: "genResume", mode: "next", gen: gRef(), arg: null, type: promiseT, loc }, + loc, + }, + { + kind: "varDecl", + localId: r.id, + init: { + kind: "awaitExpr", + value: { kind: "varRef", localId: p.id, type: promiseT, loc }, + type: recT, + loc, + }, + loc, + }, + { + kind: "if", + cond: { kind: "recordGet", obj: rRef(), shapeId: recT.shapeId, field: "done", type: BOOL, loc }, + then: [ + { kind: "assign", localId: done.id, value: { kind: "boolLit", value: true, type: BOOL, loc }, loc }, + { kind: "break", loc }, + ], + else_: null, + loc, + }, + { kind: "varDecl", localId: x.id, init: extracted, loc }, + ]; + const body = lowerer.inCtl("loop", () => lowerer.lowerScopedBlock(stmt.statement), labels); + const closePromise: IrExpr = { kind: "genResume", mode: "return", gen: gRef(), arg: null, type: promiseT, loc }; + return { + kind: "block", + body: [ + { kind: "varDecl", localId: g.id, init: iterable, loc }, + { kind: "varDecl", localId: done.id, init: { kind: "boolLit", value: false, type: BOOL, loc }, loc }, + { + kind: "while", + cond: { kind: "boolLit", value: true, type: BOOL, loc }, + body: [...head, ...body], + ...(labels && { labels }), + loc, + }, + { + kind: "if", + cond: { kind: "unary", op: "!", operand: { kind: "varRef", localId: done.id, type: BOOL, loc }, type: BOOL, loc }, + then: [ + { + kind: "exprStmt", + expr: { kind: "awaitExpr", value: closePromise, type: recT, loc }, + loc, + }, + ], + else_: null, + loc, + }, + ], + loc, + }; + } finally { + lowerer.scopes.pop(); + } +} + /** Statement-position `yield* e;` — the forwarding loop: * * { const %dele = ; let %dr = %dele.next(); @@ -374,6 +521,13 @@ export function lowerYieldStarStatement(lowerer: Lowerer, expr: ts.Expression): if (!ts.isYieldExpression(expr) || expr.asteriskToken === undefined) return null; const gen = lowerer.ctx.generator; if (!gen) lowerer.unsupported("SC1071", expr); + if (lowerer.ctx.isAsync) { + lowerer.unsupported( + "SC1071", + expr, + "'yield*' in async generators (use 'for await' and yield each value explicitly)", + ); + } if (!expr.expression) lowerer.unsupported("SC1071", expr, "'yield*' with no operand"); const loc = locOf(expr); const delegate = lowerer.lowerExpr(expr.expression); diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index e0f5401b7..b00b363e9 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -6,7 +6,7 @@ import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { arrayValueRead, arrayValueStore, arrayValueType } from "./array-values.js"; -import { lowerForOfGenerator, lowerYieldStarStatement } from "./lower-generators.js"; +import { lowerForAwaitGenerator, lowerForOfGenerator, lowerYieldStarStatement, type GenType } from "./lower-generators.js"; import { BOOL, BYTES_U8, CAUGHT, DYN, F64, IrExpr, IrGlobal, IrJsOp, IrLocal, IrStmt, IrType, JSVAL, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/ir.js"; import { PoisonError, boundIdentifiersOf, dynFallbackType, dynUndefinedExpr, importCallHandleType, neverTaintedJsType, stmtUsesIsland, uncheckedOverloadHandleCall } from "./lowerer.js"; import { enforceLibBoundary } from "./lib-boundary.js"; @@ -6235,12 +6235,19 @@ function isEsModuleStamp(expr: ts.Expression): boolean { // desugars drop them — a labeled jump naming those loops fences at the // jump site (no label point exists in their desugared shape). const labels = lowerer.takeLabels(); - // `for await` is ASYNC ITERATION, not the shipped async/await. ONE - // async iterable has a lowering: process.stdin (the piped-input - // pattern real CLIs use) — see lowerForAwaitStdin. Everything else is - // named specifically (the bare SC1070 text would claim async/await - // itself is missing). + // `for await` is ASYNC ITERATION, not the shipped async/await. Typed + // async generators use the generic request protocol; process.stdin + // and readable streams retain their dedicated next-chunk paths. if (stmt.awaitModifier) { + { + const genT = lowerer.mapTypeOf(lowerer.typeOf(stmt.expression)); + if (genT?.kind === "generator" && genT.async) { + const iterable = lowerer.lowerExpr(stmt.expression); + if (iterable.type.kind === "generator" && iterable.type.async) { + return lowerForAwaitGenerator(lowerer, stmt, iterable as IrExpr & { type: GenType }, labels); + } + } + } if ( ts.isPropertyAccessExpression(stmt.expression) && lowerer.stdlibGlobalMember(stmt.expression, "process") === "stdin" @@ -6259,7 +6266,7 @@ function isEsModuleStamp(expr: ts.Expression): boolean { } } } - lowerer.unsupported("SC1070", stmt, "'for await' (async iteration over anything but process.stdin and readable streams)"); + lowerer.unsupported("SC1070", stmt, "'for await' (async iteration over anything but typed async generators, process.stdin, and readable streams)"); } lowerer.fenceStaticHeadersIteration(stmt.expression); // A stored numeric value iterator declared in this function keeps its diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 66eda5646..e7bf7786b 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -98,7 +98,7 @@ import { ParamShape, FnSig, GenericFnInfo, GenericInstance, bindingNeverReassign import { lowerArrayMethodCall, lowerBufferStaticCall, lowerBytesMethodCall, lowerBytesNew, lowerMapMethodCall, lowerMapForEachCall, buildMapForEachFn, lowerRecordOvfCaptureHelper, lowerEnvToPairsHelper, lowerSetMethodCall, lowerSetForEachCall, buildSetForEachFn, lowerRegexMethodCall, lowerStringMethodCall } from "./lower-containers.js"; import { lowerStreamModuleCall } from "./lower-stream.js"; import { lowerEmitOverrideSpec, type EmitSpecCtx, type EmitSpecRequest } from "./lower-event-emitter.js"; -import { builtinImportOf, createRequireBindingDecl, createRequireNamespaceDecl, createRequireSpecOf, stripTypeCasts, lowerBuiltinModuleCall, lowerFsToUnixTimestampCall, lowerFsLadderCall, lowerChildArgsArg, lowerSpawnSyncCall, lowerSpawnCall, lowerExecSyncCall, recordToEnvPairs, lowerJsonMethodCall, fencedBuiltinImportOf, lowerCryptoComposedCall, lowerUrlMethodCall, lowerSearchParamsMethodCall, lowerStatsMethodCall, lowerChildMethodCall, lowerAtomicsCall, lowerBuiltinExtraProperty, promisifiedExecFileDecl, lowerExecFileAsyncCall, execFileAsyncHelper, lowerStringDecoderMethodCall, strdecHelper, lowerReadlineMethodCall, lowerDcChannelMethodCall, lowerDcChannelProperty, lowerAlsMethodCall, lowerDcTracingChannelMethodCall, lowerDcTracingChannelProperty, lowerJsonProperty, lowerErrorCodeProperty, lowerProcessProperty, isProcessEnv, envValueType, lowerProcessEnvGet, lowerProcessMethodCall, lowerProcessOptionalMethodCall, lowerTimeoutMethodCall, envSnapshotHelper, isConsoleLog, consoleCallMember, lowerNumberStaticCall, lowerNumberStaticProperty, lowerDateCall, lowerTextCodecCall, lowerCryptoModuleCall, lowerFsConstantsProperty, lowerBuiltinConstantsProperty, builtinConstantBindingOf, builtinConstantsDestructureDecl, lowerProcessStreamProperty, lowerStringStaticCall, lowerStringLastIndexOfCall, lowerPromiseStaticCall, textCodecBindingClassOf } from "./lower-builtins.js"; +import { builtinImportOf, createRequireBindingDecl, createRequireNamespaceDecl, createRequireSpecOf, stripTypeCasts, lowerBuiltinModuleCall, lowerTimersPromisesSetInterval, lowerFsToUnixTimestampCall, lowerFsLadderCall, lowerChildArgsArg, lowerSpawnSyncCall, lowerSpawnCall, lowerExecSyncCall, recordToEnvPairs, lowerJsonMethodCall, fencedBuiltinImportOf, lowerCryptoComposedCall, lowerUrlMethodCall, lowerSearchParamsMethodCall, lowerStatsMethodCall, lowerChildMethodCall, lowerAtomicsCall, lowerBuiltinExtraProperty, promisifiedExecFileDecl, lowerExecFileAsyncCall, execFileAsyncHelper, lowerStringDecoderMethodCall, strdecHelper, lowerReadlineMethodCall, lowerDcChannelMethodCall, lowerDcChannelProperty, lowerAlsMethodCall, lowerDcTracingChannelMethodCall, lowerDcTracingChannelProperty, lowerJsonProperty, lowerErrorCodeProperty, lowerProcessProperty, isProcessEnv, envValueType, lowerProcessEnvGet, lowerProcessMethodCall, lowerProcessOptionalMethodCall, lowerTimeoutMethodCall, envSnapshotHelper, isConsoleLog, consoleCallMember, lowerNumberStaticCall, lowerNumberStaticProperty, lowerDateCall, lowerTextCodecCall, lowerCryptoModuleCall, lowerFsConstantsProperty, lowerBuiltinConstantsProperty, builtinConstantBindingOf, builtinConstantsDestructureDecl, lowerProcessStreamProperty, lowerStringStaticCall, lowerStringLastIndexOfCall, lowerPromiseStaticCall, textCodecBindingClassOf } from "./lower-builtins.js"; import { fenceFetchObjectAssignment, fenceFetchObjectBinding, fenceStaticAbortControllerMemberRead, fenceStaticHeadersIteration, fenceStaticHeadersMember, fenceStaticReadableStreamMember, fenceStaticResponseMember, fenceUnsupportedFetchConstructorMember, isIslandExpr, islandFuncValueFence, islandRegexpOf, jsvalIn, requireDynamicApi, islandGlobalFnOf, lowerAbortControllerNew, lowerDynamicHeadersIteratorCall, lowerDynamicHeadersSpread, lowerDynamicImportCall, lowerFetchCall, lowerFetchElementMethodCall, lowerResponseNew, lowerStaticFetchCompanionCall, lowerStaticAbortControllerCall, lowerStaticAbortSignalListenerCall, lowerStaticReadableStreamCancelCall, lowerStaticReadableStreamControllerCall, lowerStaticReadableStreamNew, lowerStaticReadableStreamReaderCall, lowerStaticResponseCall, lowerIslandMethodCall, lowerMathProperty, npmPackageOf, npmMemberFence, npmPackageOfSymbol } from "./lower-island.js"; import { lowerHttpHeadersElement, lowerNetModuleCall, lowerServerMethodCall, lowerServerProperty, lowerTlsRootCertificates } from "./lower-server.js"; import { lowerDgramDnsModuleCall, lowerDgramMethodCall } from "./lower-dgram.js"; @@ -188,7 +188,7 @@ export interface FnCtx { isAsync?: boolean; /** Yield is legal here (generator function body): the yield/next value * channels the yield lowering types itself against. */ - generator?: { yieldT: IrType; nextT: IrType } | null; + generator?: { yieldT: IrType; nextT: IrType; resultType: IrType & { kind: "record" } } | null; /** VARIADIC `arguments` form (rest-marked func type with no declared * rest param): the synthetic trailing dyn-array param `arguments` * reads resolve to. */ @@ -398,7 +398,7 @@ interface RuntimeFenceFunctionTarget extends RuntimeFenceBase { returnType: IrType; paramsMutable?: boolean; async?: true; - generator?: { yieldT: IrType; nextT: IrType }; + generator?: NonNullable; } interface RuntimeFenceClosureTarget extends Omit { @@ -9850,6 +9850,14 @@ export class Lowerer { return lowerBuiltinModuleCall(this, expr, bi, fn, loc); } + lowerTimersPromisesSetInterval( + expr: ts.CallExpression, + bi: { module: string; member: string }, + loc: SrcLoc, + ): IrExpr | null { + return lowerTimersPromisesSetInterval(this, expr, bi, loc); + } + lowerFsToUnixTimestampCall(expr: ts.CallExpression, bi: { module: string; member: string }, loc: SrcLoc,): IrExpr | null { diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 0ade93ae0..0765c936a 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -534,7 +534,7 @@ export function formatIrType(t: IrType, shapes: ShapeRegistry, unions: UnionRegi case "promise": return `Promise<${formatIrType(t.inner, shapes, unions, seen)}>`; case "generator": - return `Generator<${formatIrType(t.yieldT, shapes, unions, seen)}, ${formatIrType(t.retT, shapes, unions, seen)}, ${formatIrType(t.nextT, shapes, unions, seen)}>`; + return `${t.async ? "AsyncGenerator" : "Generator"}<${formatIrType(t.yieldT, shapes, unions, seen)}, ${formatIrType(t.retT, shapes, unions, seen)}, ${formatIrType(t.nextT, shapes, unions, seen)}>`; default: { const _exhaustive: never = t; void _exhaustive; @@ -2090,9 +2090,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (!inner) return null; return { kind: "promise", inner }; } - // Generator (and the lib's IterableIterator, - // the older annotation spelling — Generator extends it): the sync - // generator kind. Channel normalization keeps the runtime honest: + // Generator, AsyncGenerator (and + // the lib's IterableIterator, the older sync annotation spelling): + // the generator kind. Channel normalization keeps the runtime honest: // yield channel — T must be a real value type (a generator that could // only yield undefined has no C value form); `never` // (a generator that never yields) rides the VOID @@ -2108,14 +2108,18 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { // argument — fenced at the call). // Mixed dyn/concrete channels stay unmapped: the shared result record's // value slot would need a dyn union arm, which does not exist. - if (isStdlibInterface("Generator") || isStdlibInterface("IterableIterator")) { + if (isStdlibInterface("Generator") || isStdlibInterface("AsyncGenerator") || isStdlibInterface("IterableIterator")) { const args = checker.getTypeArguments(widened as ts.TypeReference); const channels = genChannels(args[0], args[1], args[2], ctx); if (!channels) return null; // The result record must exist too (its union must be legal), or // `.next()` could never answer — mapped generators always resume. if (!genResultRecord(channels.yieldT, channels.retT, ctx.shapes, unions)) return null; - return { kind: "generator", ...channels }; + return { + kind: "generator", + ...(isStdlibInterface("AsyncGenerator") ? { async: true as const } : {}), + ...channels, + }; } // IteratorResult — the checker's type of `g.next()` (an // alias for IteratorYieldResult | IteratorReturnResult): @@ -3047,7 +3051,7 @@ export function genResultRecord( shapeId: shapes.intern([ { name: "done", type: BOOL }, { name: "value", type: valueT }, - ]), + ], false, undefined, ["value", "done"]), }; } diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index e20d4450f..ceef7e85e 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -296,7 +296,7 @@ export type IrType = * generators still suspended at exit). Fenced out of union arms (no * narrowing test — the map/set rule), map keys/values, set elements, * array elements, and JSON. */ - | { kind: "generator"; yieldT: IrType; retT: IrType; nextT: IrType } + | { kind: "generator"; async?: true; yieldT: IrType; retT: IrType; nextT: IrType } /** The `undefined` unit type — a payload-less arm kind. Representable * ONLY as a union arm (`string | undefined`) or as the type of a * `unitLit` on its way into a `unionWrap`; it can never stand alone in @@ -706,7 +706,7 @@ export function typeKey(t: IrType): string { case "promise": return `promise<${typeKey(t.inner)}>`; case "generator": - return `generator<${typeKey(t.yieldT)},${typeKey(t.retT)},${typeKey(t.nextT)}>`; + return `${t.async ? "async-generator" : "generator"}<${typeKey(t.yieldT)},${typeKey(t.retT)},${typeKey(t.nextT)}>`; default: { const _exhaustive: never = t as Exclude; void _exhaustive; @@ -742,6 +742,7 @@ export function typeEquals(a: IrType, b: IrType): boolean { if (a.kind === "generator") { return ( b.kind === "generator" && + (a.async === true) === (b.async === true) && typeEquals(a.yieldT, b.yieldT) && typeEquals(a.retT, b.retT) && typeEquals(a.nextT, b.nextT) @@ -1272,7 +1273,9 @@ export interface IrFunction { * Each is also listed in `locals` (with boxed: true); it is NOT a param. */ captures?: IrParam[]; /** Async: the body runs on a fiber; `returnType` is the INNER type T (a - * `return v` fulfills with v) while call sites receive Promise. */ + * `return v` fulfills with v) while ordinary call sites receive + * Promise. With `generator` present, call sites receive the lazy + * async-generator object instead. */ async?: true; /** Async module initializers only: a module-global Promise slot where * the spawn wrapper caches its first evaluation promise. Every later @@ -1285,15 +1288,16 @@ export interface IrFunction { * becomes the runtime cycle root. Dynamic imports wait on this shared * completion verdict instead of a build-time-selected member. */ asyncCycleCacheGlobal?: string; - /** Generator (`function*`): the body runs on a fiber created SUSPENDED + /** Generator (`function*` / `async function*`): the body runs on a fiber created SUSPENDED * (nothing runs until the first `.next()`); `returnType` is the * generator's TReturn (VOID when it carries no value — `return;` * completes with the undefined arm) while call sites receive the - * generator type `{ yieldT, retT: returnType, nextT }` from an emitted + * generator type `{ async?, yieldT, retT: returnType, nextT }` from an emitted * spawn wrapper that only allocates. `yieldT` is what `yield e` sends * out, `nextT` what `.next(v)` sends in (the yield expression's result - * type). Mutually exclusive with `async` (async generators are fenced). */ - generator?: { yieldT: IrType; nextT: IrType }; + * type). `async` alongside this field marks an async generator: its + * resume methods queue requests and return promises. */ + generator?: { yieldT: IrType; nextT: IrType; resultType: IrType & { kind: "record" } }; body: IrStmt[]; loc: SrcLoc; } @@ -4645,7 +4649,7 @@ export type IrExpr = * through finally blocks but must NOT be taken by catch handlers * (backends emit a sentinel re-unwind prologue at catch entry inside * generator bodies; scr_exc_genret_pending answers it). */ - | { kind: "yieldExpr"; value: IrExpr | null; type: IrType; loc: SrcLoc } + | { kind: "yieldExpr"; value: IrExpr | null; awaited?: true; type: IrType; loc: SrcLoc } /** One consumer resume of a generator: `g.next(arg)`, `g.return(arg)`, * `g.throw(arg)`, and the for-of/yield* desugars. `gen` is a borrowed * generator-typed temp. `arg` is the sent value (moves in): next's @@ -4661,8 +4665,9 @@ export type IrExpr = * unless suspended (then the GENRET unwind runs finallys; a finally * yield answers done:false and parks the return value) / throw on a * non-suspended generator marks it done and re-throws at the call site. - * MAY-THROW SEED: a body exception (or the injected throw) propagates - * into the caller synchronously. */ + * Sync generators return that record directly and propagate body errors + * synchronously. Async generators return Promise; requests queue, + * and body errors reject the corresponding promise. */ | { kind: "genResume"; mode: "next" | "return" | "throw"; gen: IrExpr; arg: IrExpr | null; type: IrType; loc: SrcLoc } /** Await a promise: parks the current fiber until it settles; a rejected * promise re-throws into the awaiter (may-throw seed). Result is the diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index dd229d60f..48f6a5da1 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -1154,6 +1154,9 @@ export interface IrValidationError { * TReturn channel, so call sites see the generator type. The one place * the body/call-site split is spelled out in the validator. */ function callSiteReturnType(fn: IrFunction): IrType { + if (fn.async && fn.generator !== undefined) { + return { kind: "generator", async: true, yieldT: fn.generator.yieldT, retT: fn.returnType, nextT: fn.generator.nextT }; + } if (fn.async) return { kind: "promise", inner: fn.returnType }; if (fn.generator !== undefined) { return { kind: "generator", yieldT: fn.generator.yieldT, retT: fn.returnType, nextT: fn.generator.nextT }; @@ -1168,9 +1171,6 @@ export function validateModule(mod: IrModule): IrValidationError[] { if (functionsByName.has(fn.name)) { errors.push({ message: `duplicate function "${fn.name}"`, loc: fn.loc }); } - if (fn.async && fn.generator !== undefined) { - errors.push({ message: `function "${fn.name}" is both async and a generator (async generators are fenced)`, loc: fn.loc }); - } functionsByName.set(fn.name, fn); } const ffiByName = new Map[number]>(); @@ -4905,17 +4905,19 @@ function validateFunction( err(`genResume throw of a ${e.arg.type.kind} value`, e.loc); } } - // The result is the IteratorResult record { done: bool, value: V } + // Sync resumes return IteratorResult directly; async resumes return + // Promise. The record is { done: bool, value: V } // with V dyn (the any/unknown channel) or an undefined-armed union. - if (e.type.kind !== "record") { - err(`genResume result is ${e.type.kind}, not a record`, e.loc); + const resultT = genT.async ? (e.type.kind === "promise" ? e.type.inner : null) : e.type; + if (resultT?.kind !== "record") { + err(`genResume result is ${typeKey(e.type)}, not ${genT.async ? "a promise of " : ""}a record`, e.loc); break; } - const rec = records.get(e.type.shapeId); + const rec = records.get(resultT.shapeId); const doneF = rec?.fields.find((f) => f.name === "done"); const valueF = rec?.fields.find((f) => f.name === "value"); if (!rec || rec.fields.length !== 2 || doneF?.type.kind !== "bool" || valueF === undefined) { - err(`genResume result record ${e.type.shapeId} is not { done: bool, value: V }`, e.loc); + err(`genResume result record ${resultT.shapeId} is not { done: bool, value: V }`, e.loc); break; } if (valueF.type.kind === "dyn") break; diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 1c595a9df..0b19eab03 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -3538,6 +3538,13 @@ "status": "static", "note": "the lowered call form takes no arguments" }, + { + "id": "syntax.async-generators", + "kind": "syntax", + "name": "typed async generator functions and methods", + "status": "static", + "note": "lazy bodies with queued next/return/throw requests, await, direct yield, and Node-compatible completion promises" + }, { "id": "syntax.await-using-declarations", "kind": "syntax", @@ -3664,6 +3671,13 @@ "status": "static", "note": "return, throw, break, continue, and labeled jumps run crossed finally blocks; a finally completion replaces the pending one" }, + { + "id": "syntax.for-await-async-generators", + "kind": "syntax", + "name": "for await over typed async generators", + "status": "static", + "note": "awaits each IteratorResult and closes the generator on early break" + }, { "id": "syntax.for-using-of", "kind": "syntax", diff --git a/packages/compiler/test/ts7/baselines/order-parity.json b/packages/compiler/test/ts7/baselines/order-parity.json index bc4403686..bdb57a1f3 100644 --- a/packages/compiler/test/ts7/baselines/order-parity.json +++ b/packages/compiler/test/ts7/baselines/order-parity.json @@ -5930,6 +5930,66 @@ ], "diags": [] }, + "/tests/corpus/2840-finally-completions.ts": { + "order": [ + "/tests/corpus/2840-finally-completions.ts" + ], + "diags": [] + }, + "/tests/corpus/2841-using-sync.ts": { + "order": [ + "/tests/corpus/2841-using-sync.ts" + ], + "diags": [] + }, + "/tests/corpus/2842-await-using.ts": { + "order": [ + "/tests/corpus/2842-await-using.ts" + ], + "diags": [] + }, + "/tests/corpus/2843-using-suppressed-error.ts": { + "order": [ + "/tests/corpus/2843-using-suppressed-error.ts" + ], + "diags": [] + }, + "/tests/corpus/2844-node-disposable-resources.ts": { + "order": [ + "/tests/corpus/2844-node-disposable-resources.ts" + ], + "diags": [] + }, + "/tests/corpus/2845-for-using-of.ts": { + "order": [ + "/tests/corpus/2845-for-using-of.ts" + ], + "diags": [] + }, + "/tests/corpus/2846-generator-using.ts": { + "order": [ + "/tests/corpus/2846-generator-using.ts" + ], + "diags": [] + }, + "/tests/corpus/2850-async-generators-protocol.ts": { + "order": [ + "/tests/corpus/2850-async-generators-protocol.ts" + ], + "diags": [] + }, + "/tests/corpus/2851-async-generators-values.ts": { + "order": [ + "/tests/corpus/2851-async-generators-values.ts" + ], + "diags": [] + }, + "/tests/corpus/2852-timers-promises-interval.ts": { + "order": [ + "/tests/corpus/2852-timers-promises-interval.ts" + ], + "diags": [] + }, "/tests/corpus/300-if-else.ts": { "order": [ "/tests/corpus/300-if-else.ts" diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index 730dcd3cc..0043b2708 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -327,6 +327,9 @@ struct ScrFiber { * an UNCAUGHT exception, like Node's queueMicrotask, never a * rejection). Owned; NULL on real fibers. */ ScrClosure *micro_cb; + /* Async-generator settlement jobs: stackless READY-queue envelopes. + * The retained generator is pumped on the main stack. */ + ScrGen *gen_job; #ifdef SCR_ASAN_FIBERS void *fake_stack; #endif @@ -405,6 +408,9 @@ static void scr_ready_push(ScrFiber *f) { scr_ready[scr_ready_head + scr_ready_len++] = f; } +static void scr_async_gen_job(ScrGen *g); +static void scr_async_gen_resume_ready(ScrGen *g); + /* Timer min-heap, FIFO tiebreak via sequence numbers. `id` is nonzero only * for setInterval entries (the clearInterval handle; setTimeout has no * clear surface, so plain timeouts never need one) and `repeat_ms` is the @@ -2073,6 +2079,13 @@ void scr_loop_set_events(bool (*pending)(void), bool (*watching)(void), /* ── the event loop ───────────────────────────────────────────────────── */ static void scr_resume_fiber(ScrFiber *f) { + if (f->gen_job != NULL) { + ScrGen *g = f->gen_job; + free(f); + scr_async_gen_job(g); + scr_gen_release(g); + return; + } /* A queueMicrotask envelope: run the closure on the main stack, no * context switch. A throw leaves the exception cell pending — every * drain site returns to main's uncaught report, Node's queueMicrotask @@ -2084,6 +2097,10 @@ static void scr_resume_fiber(ScrFiber *f) { scr_closure_release(cb); return; } + if (f->gen != NULL) { + scr_async_gen_resume_ready(f->gen); + return; + } #ifdef _WIN32 /* The loop runs on the main stack; make sure scr_loop_ctx names its * fiber handle (a no-op after the first conversion). */ @@ -2939,7 +2956,13 @@ void scr_promise_run_executor2(ScrPromise *p, ScrClosure *exec, ScrClosure *reso * loop — .next() from the main stack (or any fiber) blocks until the * yield. */ -enum { SCR_GEN_UNSTARTED = 0, SCR_GEN_SUSPENDED, SCR_GEN_RUNNING, SCR_GEN_DONE }; +enum { + SCR_GEN_UNSTARTED = 0, + SCR_GEN_SUSPENDED, + SCR_GEN_AWAITING, + SCR_GEN_RUNNING, + SCR_GEN_DONE +}; /* One type-erased value slot: the exception cell's payload technique, * with only the release entry point (takes MOVE — nothing here retains). */ @@ -2951,6 +2974,16 @@ typedef struct { void (*release_fn)(void *); } ScrGenSlot; +enum { SCR_AG_NEXT = 0, SCR_AG_RETURN, SCR_AG_THROW }; + +typedef struct ScrAsyncGenRequest { + int mode; + ScrGenSlot arg; + ScrExcCell thrown; + ScrPromise *promise; /* owned until this request settles */ + struct ScrAsyncGenRequest *next; +} ScrAsyncGenRequest; + static void scr_gen_slot_reset(ScrGenSlot *s) { if (s->kind == SCR_EXC_REF && s->payload != NULL) s->release_fn(s->payload); s->kind = SCR_EXC_NONE; @@ -2958,6 +2991,21 @@ static void scr_gen_slot_reset(ScrGenSlot *s) { s->release_fn = NULL; } +static void scr_gen_exc_reset(ScrExcCell *cell) { + if (cell->kind == SCR_EXC_STR) { + scr_str_release((ScrStr *)cell->payload); + } else if (cell->kind == SCR_EXC_REF || cell->kind == SCR_EXC_OBJ) { + cell->release_fn(cell->payload); + } + memset(cell, 0, sizeof *cell); +} + +static void scr_gen_exc_move(ScrExcCell *dst, ScrExcCell *src) { + scr_gen_exc_reset(dst); + *dst = *src; + memset(src, 0, sizeof *src); +} + struct ScrGen { size_t rc; ScrFiber *fiber; /* NULL once torn down (done, or unstarted release) */ @@ -2968,15 +3016,26 @@ struct ScrGen { /* The never-started teardown: drops the packed (already-retained) * arguments the spawn wrapper built. Emitted per generator function. */ void (*drop_args)(void *); + bool is_async; + void (*settle_async)(ScrGen *, ScrPromise *); + ScrAsyncGenRequest *requests_head; + ScrAsyncGenRequest *requests_tail; + ScrPromise *active_request; + ScrExcCell async_error; + bool settle_queued; + bool success_hop_done; }; -ScrGen *scr_gen_new(void (*entry)(ScrFiber *, void *), void *argpack, - void (*drop_args)(void *)) { +static ScrGen *scr_gen_new_common(void (*entry)(ScrFiber *, void *), void *argpack, + void (*drop_args)(void *), + void (*settle_async)(ScrGen *, ScrPromise *)) { ScrGen *g = calloc(1, sizeof *g); if (!g) scr_oom(); g->rc = 1; g->state = SCR_GEN_UNSTARTED; g->drop_args = drop_args; + g->is_async = settle_async != NULL; + g->settle_async = settle_async; scr_obj_alloc_note(); ScrFiber *f = calloc(1, sizeof *f); @@ -3007,6 +3066,17 @@ ScrGen *scr_gen_new(void (*entry)(ScrFiber *, void *), void *argpack, return g; } +ScrGen *scr_gen_new(void (*entry)(ScrFiber *, void *), void *argpack, + void (*drop_args)(void *)) { + return scr_gen_new_common(entry, argpack, drop_args, NULL); +} + +ScrGen *scr_async_gen_new(void (*entry)(ScrFiber *, void *), void *argpack, + void (*drop_args)(void *), + void (*settle)(ScrGen *, ScrPromise *)) { + return scr_gen_new_common(entry, argpack, drop_args, settle); +} + ScrGen *scr_gen_retain(ScrGen *g) { if (g) g->rc++; return g; @@ -3017,6 +3087,7 @@ void scr_gen_release(ScrGen *g) { scr_gen_slot_reset(&g->out); scr_gen_slot_reset(&g->in); scr_gen_slot_reset(&g->ret); + scr_gen_exc_reset(&g->async_error); if (g->fiber != NULL) { if (g->state == SCR_GEN_UNSTARTED) { /* Never ran: nothing on the stack owns anything — clean teardown. @@ -3142,6 +3213,15 @@ void scr_gen_yield_ref(void *v, void (*release)(void *)) { scr_gen_yield_switch(); } +void scr_async_gen_hop_done(void) { + ScrGen *g = scr_gen_self(); + if (!g->is_async) { + fputs("scriptc: internal error: async-generator hop on sync generator\n", stderr); + abort(); + } + g->success_hop_done = true; +} + bool scr_exc_genret_pending(void) { return scr_exc_current_cell()->kind == SCR_EXC_GENRET; } @@ -3191,20 +3271,26 @@ static void scr_gen_switch_in(ScrGen *g) { if (f->done) { g->state = SCR_GEN_DONE; if (f->exc.kind != SCR_EXC_NONE) { - /* The body's exception escaped: move it into the resumer's cell - * (replace semantics match a throw at the resume site; the resumer's - * cell is clean here — a pending exception cannot reach a resume). */ - ScrExcCell *mine = scr_exc_current_cell(); - *mine = f->exc; - f->exc.kind = SCR_EXC_NONE; - f->exc.payload = NULL; - f->exc.trace_fn = NULL; + if (g->is_async) { + /* Async-generator failures reject the active request; they never + * throw synchronously from next/return/throw. */ + scr_gen_exc_move(&g->async_error, &f->exc); + } else { + /* The sync body's exception escapes at the resume site. */ + ScrExcCell *mine = scr_exc_current_cell(); + *mine = f->exc; + memset(&f->exc, 0, sizeof f->exc); + } } scr_fiber_destroy(f); g->fiber = NULL; scr_fibers_live--; } else { - g->state = SCR_GEN_SUSPENDED; + /* An async generator can return to its resumer because it yielded OR + * because await parked it. OUT distinguishes the two. */ + g->state = g->is_async && !scr_gen_out_has(g) + ? SCR_GEN_AWAITING + : SCR_GEN_SUSPENDED; } scr_gen_release(g); } @@ -3296,3 +3382,221 @@ void scr_gen_resume_throw(ScrGen *g) { } } } + +/* ── async generators ──────────────────────────────────────────────── + * The body is the same suspended generator fiber, now allowed to await. + * Each resume request owns a promise and a retained generator. The first + * idle request starts synchronously; successful yield/return settlement + * takes AsyncGeneratorYield's Await hop (the emitter marks a yielded + * promise whose await already supplied it), while thrown completions reject + * immediately. Requests queued behind an active one begin in that + * settlement continuation, preserving Node's ordering. */ + +static void scr_async_gen_start_queued(ScrGen *g); + +static void scr_async_gen_schedule_settle(ScrGen *g) { + if (g->settle_queued) return; + ScrFiber *job = calloc(1, sizeof *job); + if (!job) scr_oom(); + job->gen_job = scr_gen_retain(g); + g->settle_queued = true; + scr_ready_push(job); +} + +static void scr_async_gen_finish_active(ScrGen *g, bool rejected) { + ScrPromise *p = g->active_request; + if (p == NULL) { + fputs("scriptc: internal error: async generator settled without a request\n", stderr); + abort(); + } + g->active_request = NULL; + if (rejected) { + scr_promise_reject_from_cell(p, &g->async_error); + scr_promise_settle_wake(p); + } else { + g->settle_async(g, p); + } + scr_promise_release(p); + /* Every queued request retains its generator until settlement. */ + scr_gen_release(g); +} + +static void scr_async_gen_capture_caller_error(ScrGen *g) { + ScrExcCell *cell = scr_exc_current_cell(); + if (cell->kind != SCR_EXC_NONE) scr_gen_exc_move(&g->async_error, cell); +} + +static void scr_async_gen_run_active(ScrGen *g, ScrAsyncGenRequest *req) { + const bool was_done = g->state == SCR_GEN_DONE; + if (req->mode == SCR_AG_NEXT) { + scr_gen_slot_reset(&g->in); + g->in = req->arg; + memset(&req->arg, 0, sizeof req->arg); + scr_gen_resume(g); + } else if (req->mode == SCR_AG_RETURN) { + scr_gen_slot_reset(&g->ret); + g->ret = req->arg; + memset(&req->arg, 0, sizeof req->arg); + scr_gen_resume_return(g); + } else { + scr_gen_exc_move(scr_exc_current_cell(), &req->thrown); + scr_gen_resume_throw(g); + } + scr_async_gen_capture_caller_error(g); + + if (g->async_error.kind != SCR_EXC_NONE) { + scr_async_gen_finish_active(g, true); + return; + } + if (g->state == SCR_GEN_AWAITING) return; + + /* next() on an already-completed generator produces its undefined done + * result immediately. Yield, body return, and return(value) all pass + * through AsyncGeneratorYield's successful Await step. */ + if (was_done && req->mode == SCR_AG_NEXT) { + scr_async_gen_finish_active(g, false); + } else if (g->success_hop_done) { + g->success_hop_done = false; + scr_async_gen_finish_active(g, false); + } else { + scr_async_gen_schedule_settle(g); + } +} + +static void scr_async_gen_start_queued(ScrGen *g) { + /* Keep the object alive while an immediately-settled request releases + * its own retain and the loop inspects the next queue node. */ + scr_gen_retain(g); + while (g->active_request == NULL && g->requests_head != NULL) { + ScrAsyncGenRequest *req = g->requests_head; + g->requests_head = req->next; + if (g->requests_head == NULL) g->requests_tail = NULL; + g->active_request = req->promise; /* request ownership transfers */ + scr_async_gen_run_active(g, req); + scr_gen_slot_reset(&req->arg); + scr_gen_exc_reset(&req->thrown); + free(req); + /* Awaiting or deferred successful settlement keeps this request + * active. Immediate done/error settlement cleared it and can consume + * the next queued request in this same turn. */ + if (g->active_request != NULL) break; + } + scr_gen_release(g); +} + +static ScrPromise *scr_async_gen_enqueue(ScrGen *g, int mode, + ScrGenSlot *arg, + ScrExcCell *thrown) { + if (!g->is_async || g->settle_async == NULL) { + fputs("scriptc: internal error: async resume of a sync generator\n", stderr); + abort(); + } + ScrAsyncGenRequest *req = calloc(1, sizeof *req); + if (!req) scr_oom(); + req->mode = mode; + if (arg != NULL) { + req->arg = *arg; + memset(arg, 0, sizeof *arg); + } + if (thrown != NULL) scr_gen_exc_move(&req->thrown, thrown); + req->promise = scr_promise_new(); + /* The request owns the original promise reference and one generator + * reference; the caller receives a second promise reference. */ + ScrPromise *result = scr_promise_retain(req->promise); + scr_gen_retain(g); + if (g->requests_tail != NULL) g->requests_tail->next = req; + else g->requests_head = req; + g->requests_tail = req; + if (g->active_request == NULL && !g->settle_queued) { + scr_async_gen_start_queued(g); + } + return result; +} + +ScrPromise *scr_async_gen_next_none(ScrGen *g) { + ScrGenSlot arg = {0}; + return scr_async_gen_enqueue(g, SCR_AG_NEXT, &arg, NULL); +} + +ScrPromise *scr_async_gen_next_f64(ScrGen *g, double value) { + ScrGenSlot arg = {0}; + scr_gen_slot_f64(&arg, value); + return scr_async_gen_enqueue(g, SCR_AG_NEXT, &arg, NULL); +} + +ScrPromise *scr_async_gen_next_bool(ScrGen *g, bool value) { + ScrGenSlot arg = {0}; + scr_gen_slot_bool(&arg, value); + return scr_async_gen_enqueue(g, SCR_AG_NEXT, &arg, NULL); +} + +ScrPromise *scr_async_gen_next_ref(ScrGen *g, void *value, + void (*release)(void *)) { + ScrGenSlot arg = {0}; + scr_gen_slot_ref(&arg, value, release); + return scr_async_gen_enqueue(g, SCR_AG_NEXT, &arg, NULL); +} + +ScrPromise *scr_async_gen_return_none(ScrGen *g) { + ScrGenSlot arg = {0}; + return scr_async_gen_enqueue(g, SCR_AG_RETURN, &arg, NULL); +} + +ScrPromise *scr_async_gen_return_f64(ScrGen *g, double value) { + ScrGenSlot arg = {0}; + scr_gen_slot_f64(&arg, value); + return scr_async_gen_enqueue(g, SCR_AG_RETURN, &arg, NULL); +} + +ScrPromise *scr_async_gen_return_bool(ScrGen *g, bool value) { + ScrGenSlot arg = {0}; + scr_gen_slot_bool(&arg, value); + return scr_async_gen_enqueue(g, SCR_AG_RETURN, &arg, NULL); +} + +ScrPromise *scr_async_gen_return_ref(ScrGen *g, void *value, + void (*release)(void *)) { + ScrGenSlot arg = {0}; + scr_gen_slot_ref(&arg, value, release); + return scr_async_gen_enqueue(g, SCR_AG_RETURN, &arg, NULL); +} + +ScrPromise *scr_async_gen_throw(ScrGen *g) { + ScrExcCell thrown = {0}; + scr_gen_exc_move(&thrown, scr_exc_current_cell()); + return scr_async_gen_enqueue(g, SCR_AG_THROW, NULL, &thrown); +} + +static void scr_async_gen_job(ScrGen *g) { + g->settle_queued = false; + if (g->active_request == NULL || g->async_error.kind != SCR_EXC_NONE) { + fputs("scriptc: internal error: invalid async generator settlement job\n", stderr); + abort(); + } + scr_async_gen_finish_active(g, false); + scr_async_gen_start_queued(g); +} + +static void scr_async_gen_resume_ready(ScrGen *g) { + if (!g->is_async || g->active_request == NULL) { + fputs("scriptc: internal error: async generator resumed without a request\n", stderr); + abort(); + } + /* The active request normally owns the generator. Keep an independent + * reference across settlement, which releases that request ownership. */ + scr_gen_retain(g); + scr_gen_switch_in(g); + if (g->async_error.kind != SCR_EXC_NONE) { + scr_async_gen_finish_active(g, true); + scr_async_gen_start_queued(g); + } else if (g->state != SCR_GEN_AWAITING) { + if (g->success_hop_done) { + g->success_hop_done = false; + scr_async_gen_finish_active(g, false); + scr_async_gen_start_queued(g); + } else { + scr_async_gen_schedule_settle(g); + } + } + scr_gen_release(g); +} diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index 736eea1a9..f7d0ade48 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -4250,7 +4250,10 @@ ScrPromise *scr_fiber_promise(ScrFiber *f); typedef struct ScrGen ScrGen; ScrGen *scr_gen_new(void (*entry)(ScrFiber *, void *), void *argpack, - void (*drop_args)(void *)); + void (*drop_args)(void *)); +ScrGen *scr_async_gen_new(void (*entry)(ScrFiber *, void *), void *argpack, + void (*drop_args)(void *), + void (*settle)(ScrGen *, ScrPromise *)); ScrGen *scr_gen_retain(ScrGen *g); void scr_gen_release(ScrGen *g); /* NULL-tolerant */ void *scr_gen_retain_v(void *g); @@ -4261,6 +4264,20 @@ void scr_gen_resume_return(ScrGen *g); void scr_gen_resume_throw(ScrGen *g); bool scr_gen_done(ScrGen *g); +/* Async-generator resume methods. Reference arguments move into the + * queued request; every function returns a +1 Promise. */ +ScrPromise *scr_async_gen_next_none(ScrGen *g); +ScrPromise *scr_async_gen_next_f64(ScrGen *g, double value); +ScrPromise *scr_async_gen_next_bool(ScrGen *g, bool value); +ScrPromise *scr_async_gen_next_ref(ScrGen *g, void *value, + void (*release)(void *)); +ScrPromise *scr_async_gen_return_none(ScrGen *g); +ScrPromise *scr_async_gen_return_f64(ScrGen *g, double value); +ScrPromise *scr_async_gen_return_bool(ScrGen *g, bool value); +ScrPromise *scr_async_gen_return_ref(ScrGen *g, void *value, + void (*release)(void *)); +ScrPromise *scr_async_gen_throw(ScrGen *g); + /* IN slot (consumer stores before resume; body takes after the yield). */ void scr_gen_in_f64(ScrGen *g, double v); void scr_gen_in_bool(ScrGen *g, bool v); @@ -4284,6 +4301,7 @@ void scr_gen_ret_none(ScrGen *g); void scr_gen_yield_f64(double v); void scr_gen_yield_bool(bool v); void scr_gen_yield_ref(void *v, void (*release)(void *)); /* moves */ +void scr_async_gen_hop_done(void); void scr_gen_out_f64(ScrGen *g, double v); /* trampoline completion stores */ void scr_gen_out_bool(ScrGen *g, bool v); void scr_gen_out_ref(ScrGen *g, void *v, void (*release)(void *)); /* moves */ diff --git a/tests/corpus/2850-async-generators-protocol.ts b/tests/corpus/2850-async-generators-protocol.ts new file mode 100644 index 000000000..0bf468187 --- /dev/null +++ b/tests/corpus/2850-async-generators-protocol.ts @@ -0,0 +1,146 @@ +// Typed async generators: lazy creation, synchronous request start, +// asynchronous successful settlement, queued next/return requests, await +// and promised yields, throw injection, and finally-yield return behavior. +const ordering: string[] = []; + +async function* ordered(): AsyncGenerator { + ordering.push("body"); + yield 1; +} + +async function showOrdering(): Promise { + ordering.push("before"); + const first = ordered().next(); + ordering.push("after"); + void first.then(() => ordering.push("settled")); + queueMicrotask(() => ordering.push("micro")); + await first; + await Promise.resolve(); + console.log(ordering.join(",")); +} + +async function* promisedYield(events: string[]): AsyncGenerator { + events.push("body1"); + yield Promise.resolve(1); + events.push("body2"); + yield 2; +} + +async function showPromisedYieldOrdering(): Promise { + const events: string[] = []; + const iterator = promisedYield(events); + const first = iterator.next(); + const second = iterator.next(); + void first.then(() => events.push("first")); + void second.then(() => events.push("second")); + queueMicrotask(() => events.push("micro")); + events.push("end"); + await first; + await second; + await Promise.resolve(); + console.log(events.join(",")); +} + +async function* values(): AsyncGenerator { + console.log("values body"); + yield 1; + await Promise.resolve(); + yield Promise.resolve(2); + return 3; +} + +async function showQueue(): Promise { + const iterator = values(); + console.log("made"); + const first = iterator.next(); + const second = iterator.next(); + const returned = iterator.return(9); + console.log("queued"); + console.log(JSON.stringify(await first)); + console.log(JSON.stringify(await second)); + console.log(JSON.stringify(await returned)); +} + +async function* sentValues(): AsyncGenerator { + const message = yield 5; + return message; +} + +async function showSentValues(): Promise { + const iterator = sentValues(); + const first = iterator.next("discarded"); + const second = iterator.next("accepted"); + console.log(JSON.stringify(await first)); + console.log(JSON.stringify(await second)); + const forced = sentValues(); + console.log(JSON.stringify(await forced.return("forced"))); +} + +async function* catches(): AsyncGenerator { + try { + yield "ready"; + yield "miss"; + } catch (error) { + if (error instanceof Error) console.log("caught", error.message); + yield "after"; + } finally { + console.log("caught finally"); + } +} + +async function showThrow(): Promise { + const iterator = catches(); + console.log(JSON.stringify(await iterator.next())); + console.log(JSON.stringify(await iterator.throw(new Error("boom")))); + console.log(JSON.stringify(await iterator.next())); +} + +async function* closing(): AsyncGenerator { + try { + yield 1; + } finally { + console.log("closing finally"); + yield 4; + console.log("closing finally 2"); + } + return 0; +} + +async function showReturn(): Promise { + const iterator = closing(); + console.log(JSON.stringify(await iterator.next())); + console.log(JSON.stringify(await iterator.return(8))); + console.log(JSON.stringify(await iterator.next())); + console.log(JSON.stringify(await iterator.next())); +} + +async function* fails(): AsyncGenerator { + throw new Error("body failure"); +} + +async function showFailure(): Promise { + try { + await fails().next(); + } catch (error) { + if (error instanceof Error) console.log(error.message); + } + const iterator = values(); + try { + await iterator.throw(new Error("early")); + } catch (error) { + if (error instanceof Error) console.log(error.message); + } + console.log(JSON.stringify(await iterator.next())); +} + +async function main(): Promise { + await showOrdering(); + await showPromisedYieldOrdering(); + await showQueue(); + await showSentValues(); + await showThrow(); + await showReturn(); + await showFailure(); +} + +void main(); diff --git a/tests/corpus/2851-async-generators-values.ts b/tests/corpus/2851-async-generators-values.ts new file mode 100644 index 000000000..c98ca27ae --- /dev/null +++ b/tests/corpus/2851-async-generators-values.ts @@ -0,0 +1,53 @@ +// Async generators as declarations, generic instantiations, function +// expressions, object methods, public class methods, and private methods. +class Source { + private start: number; + + constructor(start: number) { + this.start = start; + } + + async *values(): AsyncGenerator { + yield this.start; + await Promise.resolve(); + yield this.start + 1; + } + + async *#privateValues(): AsyncGenerator { + yield "private"; + } + + async showPrivate(): Promise { + for await (const value of this.#privateValues()) console.log(value); + } +} + +async function* generic(value: T): AsyncGenerator { + yield value; +} + +async function* inferred() { + yield 11; +} + +const object = { + async *values(): AsyncGenerator { + yield 9; + }, +}; + +const expression = async function* (): AsyncGenerator { + yield "expression"; +}; + +async function main(): Promise { + for await (const value of new Source(4).values()) console.log(value); + await new Source(0).showPrivate(); + console.log(JSON.stringify(await generic("generic").next())); + console.log(JSON.stringify(await generic(7).next())); + console.log(JSON.stringify(await inferred().next())); + console.log(JSON.stringify(await object.values().next())); + console.log(JSON.stringify(await expression().next())); +} + +void main(); diff --git a/tests/corpus/2852-timers-promises-interval.ts b/tests/corpus/2852-timers-promises-interval.ts new file mode 100644 index 000000000..83bf075de --- /dev/null +++ b/tests/corpus/2852-timers-promises-interval.ts @@ -0,0 +1,18 @@ +// node:timers/promises setInterval(delay, value): a lazy typed async +// iterator built on the generic async-generator request protocol. Breaking +// the for-await loop closes it and cancels the generator before another +// timer is armed. +import { setInterval as every } from "node:timers/promises"; + +async function main(): Promise { + const iterator = every(1, "tick"); + let count = 0; + for await (const value of iterator) { + console.log(value, count); + count += 1; + if (count === 3) break; + } + console.log("done"); +} + +void main(); diff --git a/tests/coverage-fixtures/unreached.ts b/tests/coverage-fixtures/unreached.ts index 772896ac7..5bcb57a6f 100644 --- a/tests/coverage-fixtures/unreached.ts +++ b/tests/coverage-fixtures/unreached.ts @@ -21,11 +21,10 @@ function unusedCorner(bag: Bag, pairs: Map): void { } } -// Never referenced and its declaration doesn't collect (an ASYNC -// generator signature — sync generators compile now): deferred — only a -// reference would make it fail a build. +// Never referenced and its body uses the remaining async-generator +// delegation fence: deferred — only a reference would fail a build. async function* counter(limit: number): AsyncGenerator { - for (let i = 0; i < limit; i++) yield i; + yield* [limit]; } console.log(used(41)); diff --git a/tests/diagnostics/node-fallback-fence.ts b/tests/diagnostics/node-fallback-fence.ts index 86810eb4c..9d2ff271d 100644 --- a/tests/diagnostics/node-fallback-fence.ts +++ b/tests/diagnostics/node-fallback-fence.ts @@ -23,9 +23,11 @@ const tty = process.stdout.isTTY; const errTty = process.stderr.isTTY; import { accessSync, constants, mkdtempSync, readFileSync } from "node:fs"; +import { setInterval as promiseInterval } from "node:timers/promises"; accessSync("/bin/sh", constants.X_OK); const tmp = mkdtempSync("/tmp/scr-"); const raw = readFileSync("/etc/hosts").toString(tty ? "hex" : "latin1"); // runtime BufferEncoding selection lowers too +const intervalWithSignal = promiseInterval(1, "tick", {}); // AbortSignal options stay fenced // The computed encoding is now part of the static Buffer surface. import { deflateSync, gzipSync } from "node:zlib"; const packed = deflateSync("data"); // string data: the wrap-it-first hint diff --git a/tests/diagnostics/private-members.ts b/tests/diagnostics/private-members.ts index fbb78abb3..003765971 100644 --- a/tests/diagnostics/private-members.ts +++ b/tests/diagnostics/private-members.ts @@ -81,7 +81,8 @@ class GenBrand { } console.log(new GenBrand().same(new GenBrand())); -// Async private generators are still async generators — the blanket fence. +// Positive control: async private generators use direct dispatch and the +// same request queue as module-level async generators. class AsyncGen { async *#pump(): AsyncGenerator { yield 1; diff --git a/tests/harness/__snapshots__/coverage-unreached.txt b/tests/harness/__snapshots__/coverage-unreached.txt index 326af5bfb..fe4a703bb 100644 --- a/tests/harness/__snapshots__/coverage-unreached.txt +++ b/tests/harness/__snapshots__/coverage-unreached.txt @@ -1,10 +1,9 @@ scriptc coverage tests/coverage-fixtures/unreached.ts - statements analyzed 8 - compile statically 6 (75%) - (+1 function not analyzed — signature blocked) + statements analyzed 9 + compile statically 6 (66%) in unreached code (never lowered — cannot fail a build) ×1 for-in over class instances (which keys exist on an instance depends on runtime property creation the class model does not track — for-in over records and arrays compiles) SC1052 ×1 'break outer' targeting this statement form (the labeled statement lowers through a desugar that has no label point) SC1050 - ×1 async generators (async function*) SC1071 \ No newline at end of file + ×1 'yield*' in async generators (use 'for await' and yield each value explicitly) SC1071 \ No newline at end of file diff --git a/tests/harness/__snapshots__/node-fallback-fence.ts.txt b/tests/harness/__snapshots__/node-fallback-fence.ts.txt index 4088e4560..d60e3c620 100644 --- a/tests/harness/__snapshots__/node-fallback-fence.ts.txt +++ b/tests/harness/__snapshots__/node-fallback-fence.ts.txt @@ -16,20 +16,29 @@ node-fallback-fence.ts:17:13 - error SC2020: 'Buffer.reverse' i hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations) -node-fallback-fence.ts:31:28 - error SC2020: 'deflateSync of 'string' data' is part of the standard library types but has no scriptc lowering yet +node-fallback-fence.ts:30:28 - error SC2020: 'timers/promises.setInterval with 3 arguments' is part of the standard library types but has no scriptc lowering yet - 30 | import { deflateSync, gzipSync } from "node:zlib"; - 31 | const packed = deflateSync("data"); // string data: the wrap-it-first hint + 29 | const raw = readFileSync("/etc/hosts").toString(tty ? "hex" : "latin1"); // runtime BufferEncoding selection lowers too + 30 | const intervalWithSignal = promiseInterval(1, "tick", {}); // AbortSignal options stay fenced + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 31 | // The computed encoding is now part of the static Buffer surface. + + hint: the lowered form is setInterval(delay, value) with an explicit yielded value; AbortSignal options are not supported yet + +node-fallback-fence.ts:33:28 - error SC2020: 'deflateSync of 'string' data' is part of the standard library types but has no scriptc lowering yet + + 32 | import { deflateSync, gzipSync } from "node:zlib"; + 33 | const packed = deflateSync("data"); // string data: the wrap-it-first hint | ^~~~~~ - 32 | const zipped = gzipSync("data"); // beyond the lowered pair: fenced + 34 | const zipped = gzipSync("data"); // beyond the lowered pair: fenced hint: zlib works on Buffers: deflateSync(Buffer.from(s, "utf8")) -node-fallback-fence.ts:32:16 - error SC2020: 'zlib.gzipSync' is part of the standard library types but has no scriptc lowering yet +node-fallback-fence.ts:34:16 - error SC2020: 'zlib.gzipSync' is part of the standard library types but has no scriptc lowering yet - 31 | const packed = deflateSync("data"); // string data: the wrap-it-first hint - 32 | const zipped = gzipSync("data"); // beyond the lowered pair: fenced + 33 | const packed = deflateSync("data"); // string data: the wrap-it-first hint + 34 | const zipped = gzipSync("data"); // beyond the lowered pair: fenced | ^~~~~~~~~~~~~~~~ - 33 | // The remaining imports continue the declared-but-not-lowered surface. + 35 | // The remaining imports continue the declared-but-not-lowered surface. hint: deflateSync and inflateSync are the lowered zlib surface \ No newline at end of file diff --git a/tests/harness/__snapshots__/private-members.ts.txt b/tests/harness/__snapshots__/private-members.ts.txt index edbf593ff..01d05f80c 100644 --- a/tests/harness/__snapshots__/private-members.ts.txt +++ b/tests/harness/__snapshots__/private-members.ts.txt @@ -52,25 +52,4 @@ private-members.ts:79:12 - error SC1090: '#tag in …' inside a generic class (J 78 | same(o: GenBrand): boolean { 79 | return #tag in o; | ^~~~~~~~~ - 80 | } - -private-members.ts:86:3 - error SC1071: async generators (async function*) are not supported yet - - 85 | class AsyncGen { - 86 | async *#pump(): AsyncGenerator { - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 87 | yield 1; - -private-members.ts:93:12 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet - - 92 | } - 93 | const ag = new AsyncGen(); - | ^~~~~~~~~~~~~~ - 94 | void ag.drain(); - -private-members.ts:94:6 - error SC1090: method calls like 'ag.drain' is not supported yet - - 93 | const ag = new AsyncGen(); - 94 | void ag.drain(); - | ^~~~~~~~~~ - 95 | \ No newline at end of file + 80 | } \ No newline at end of file