Skip to content

Commit 0bc94ed

Browse files
Reuben Brooksclaude
andcommitted
Make funAsync wrappers plain functions: drop the double promise layer
The wrapped f is itself async, so an async wrapper allocated a second promise per call that merely resolved to the callee's promise. The wrappers (arity-specialized and the generic fallback) are now plain functions returning the callee's promise directly. Wrappers carry an own-property async:true marker; fun's dispatch and js.async? test it alongside instanceof AsyncFunction. The compiler's sync-call detection is purely syntactic and never inspects wrapper asyncness (audited all AsyncFunction uses across lib/ and bin/). Wrapper-level awaited recursion: deno 1.57x, bun 1.40x, node ~1.0x (V8 already fast-paths the chained layer). Kernel suite improved in every paired round on node and deno, directionally on bun. All suites green on node, bun 1.3.14, and deno 2.8.3: backend 369, frontend 10, kernel 134/134 on all three engines, extensions, ratatoskr e2e, smoke. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 12a888a commit 0bc94ed

2 files changed

Lines changed: 23 additions & 9 deletions

File tree

lib/frontend.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,9 @@ export default async $ => {
247247
************************/
248248

249249
await defineTyped('js.array?', A_boolean, x => asShenBool(Array.isArray(x)));
250-
await defineTyped('js.async?', A_boolean, x => asShenBool(x instanceof AsyncFunction));
250+
// funAsync wrappers (lib/runtime.js) are plain functions marked with an
251+
// own-property `async: true`; raw async closures are AsyncFunction instances
252+
await defineTyped('js.async?', A_boolean, x => asShenBool(x instanceof AsyncFunction || typeof x === 'function' && x.async === true));
251253
await defineTyped('js.boolean?', A_boolean, x => asShenBool(typeof x === 'boolean'));
252254
await defineTyped('js.defined?', A_boolean, x => asShenBool(typeof x !== 'undefined'));
253255
await defineTyped('js.false?', A_boolean, x => asShenBool(x === false));

lib/runtime.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,31 +136,43 @@ export default (options = {}) => {
136136
return arguments.length === 4 ? f(a, b, c, d) : funSyncGeneric(f, 4, slice(arguments));
137137
}
138138
];
139+
// The async wrappers are deliberately PLAIN functions: the wrapped f is
140+
// itself async, so an async wrapper would allocate a second promise per
141+
// call that merely resolves to the callee's promise. Returning the
142+
// callee's promise directly avoids that double layer; callers always go
143+
// through settle/future/await, which handle promise and non-promise
144+
// results alike. Since the wrappers are no longer AsyncFunction
145+
// instances, they carry an own-property `async: true` marker (set in
146+
// funAsync below) that asyncness checks (fun's dispatch here,
147+
// js.async? in lib/frontend.js) test alongside instanceof AsyncFunction.
139148
const funAsyncs = [
140-
f => async function () {
149+
f => function () {
141150
return arguments.length === 0 ? f() : funAsyncGeneric(f, 0, slice(arguments));
142151
},
143-
f => async function (a) {
152+
f => function (a) {
144153
return arguments.length === 1 ? f(a) : funAsyncGeneric(f, 1, slice(arguments));
145154
},
146-
f => async function (a, b) {
155+
f => function (a, b) {
147156
return arguments.length === 2 ? f(a, b) : funAsyncGeneric(f, 2, slice(arguments));
148157
},
149-
f => async function (a, b, c) {
158+
f => function (a, b, c) {
150159
return arguments.length === 3 ? f(a, b, c) : funAsyncGeneric(f, 3, slice(arguments));
151160
},
152-
f => async function (a, b, c, d) {
161+
f => function (a, b, c, d) {
153162
return arguments.length === 4 ? f(a, b, c, d) : funAsyncGeneric(f, 4, slice(arguments));
154163
}
155164
];
156165
const funSync = (f, arity) =>
157166
arity < funSyncs.length ? funSyncs[arity](f) :
158167
(...args) => funSyncGeneric(f, arity, args);
159168
const funAsync = (f, arity) =>
160-
arity < funAsyncs.length ? funAsyncs[arity](f) :
161-
async (...args) => funAsyncGeneric(f, arity, args);
169+
Object.assign(
170+
arity < funAsyncs.length ? funAsyncs[arity](f) :
171+
(...args) => funAsyncGeneric(f, arity, args),
172+
{ async: true });
173+
const isAsync = f => f instanceof AsyncFunction || f.async === true;
162174
const fun = (f, arity = f.arity || f.length) =>
163-
Object.assign((f instanceof AsyncFunction ? funAsync : funSync)(f, arity), { arity });
175+
Object.assign((isAsync(f) ? funAsync : funSync)(f, arity), { arity });
164176

165177
const bounce = (f, ...args) => new Trampoline(f, args);
166178
const future = async x => {

0 commit comments

Comments
 (0)