refactor: share async generator scaffolding (#227)

- Extract per-backend argument-pack and trampoline prologue helpers.

- Preserve async promise and generator completion, drop, cache, and spawn epilogues.
This commit is contained in:
Chris Tate
2026-08-23 16:14:15 -05:00
committed by GitHub
parent c9f532f532
commit 2412b05289
2 changed files with 163 additions and 171 deletions
@@ -5,7 +5,65 @@ import { InternalCompilerError } from "../../errors.js";
import type { CEmitter } from "./emitter.js";
import { mangleArgPack, mangleAsyncSpawn, mangleChildDataThunk, mangleChildExitThunk, mangleCloseBindThunk, mangleCloseOverrideWrap, mangleConnectResThunk, mangleConnectSockThunk, mangleDgramMsgThunk, mangleDnsLookupThunk, mangleField, mangleFsRenameThunk, mangleFunction, mangleGenDrop, mangleGenResThunk, mangleGenSpawn, mangleGlobal, mangleLocal, mangleRaceThunk, mangleRawParam, mangleNetLookupAnswerThunk, mangleEmitterInvokeThunk, mangleStreamCbThunk, mangleStreamDoneFn, mangleRecordNew, mangleRecordRelease, mangleRecordStruct, mangleResolveThunk, mangleSniAnswerThunk, mangleTrampoline } from "../mangle.js";
import { cDecl, cType, releaseCallC, retainCallC, vAdapters } from "./emit-types.js";
import { IrType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/nodes.js";
import { IrFunction, IrType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/nodes.js";
interface ArgPackAndTrampolinePrologue {
definitions: string[];
pack: string;
lifted: boolean;
pname: (p: IrFunction["params"][number]) => string;
ret: IrType;
lines: string[];
spawnParams: string[];
argPackLines: string[];
}
/** The argument-pack ABI and trampoline prefix shared by async functions
* and generators. Their promise/generator completion and spawn tails stay
* with the callers below. */
function emitArgPackAndTrampolinePrologue(
fn: IrFunction,
): ArgPackAndTrampolinePrologue {
const pack = mangleArgPack(fn.name);
const lifted = fn.captures !== undefined;
const fields: string[] = [];
if (lifted) fields.push("ScrClosure *sc_env;");
const boxedIds = new Set(fn.locals.filter((l) => l.boxed).map((l) => l.id));
const pname = (p: IrFunction["params"][number]) =>
boxedIds.has(p.localId) ? mangleRawParam(p.localId) : mangleLocal(p.localId);
for (const p of fn.params) fields.push(`${cDecl(p.type, pname(p))};`);
const definitions = [``, `typedef struct { ${fields.join(" ") || "char sc_unused;"} } ${pack};`];
const callArgs = [
...(lifted ? ["sc_a.sc_env"] : []),
...fn.params.map((p) => `sc_a.${pname(p)}`),
].join(", ");
const ret = fn.returnType;
const bodyCall = `${mangleFunction(fn.name)}(${callArgs})`;
const lines: string[] = [
`static void ${mangleTrampoline(fn.name)}(ScrFiber *sc_self, void *sc_ap0) {`,
` ${pack} sc_a = *(${pack} *)sc_ap0;`,
` free(sc_ap0);`,
];
if (ret.kind === "void") {
lines.push(` ${bodyCall};`);
} else {
lines.push(` ${cDecl(ret, "sc_r")} = ${bodyCall};`);
}
if (lifted) lines.push(` scr_closure_release(sc_a.sc_env);`);
const spawnParams = [
...(lifted ? ["ScrClosure *sc_env"] : []),
...fn.params.map((p) => cDecl(p.type, pname(p))),
];
const argPackLines = [
` ${pack} *sc_ap = malloc(sizeof *sc_ap);`,
` if (!sc_ap) { scr_trap("scriptc: out of memory\\n"); }`,
...(lifted ? [` sc_ap->sc_env = scr_closure_retain(sc_env);`] : []),
...fn.params.map((p) => ` sc_ap->${pname(p)} = ${pname(p)};`),
];
return { definitions, pack, lifted, pname, ret, lines, spawnParams, argPackLines };
}
/** Per-async-function machinery: an argument pack, a fiber trampoline
* (unpacks, runs the ordinary compiled body, settles the promise), and a
@@ -14,33 +72,9 @@ import { IrType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/
export function emitAsyncScaffolding(E: CEmitter, out: string[]): void {
for (const fn of E.mod.functions) {
if (!fn.async) continue;
const pack = mangleArgPack(fn.name);
const lifted = fn.captures !== undefined;
const fields: string[] = [];
if (lifted) fields.push("ScrClosure *sc_env;");
const boxedIds = new Set(fn.locals.filter((l) => l.boxed).map((l) => l.id));
const pname = (p: { localId: string }) =>
boxedIds.has(p.localId) ? mangleRawParam(p.localId) : mangleLocal(p.localId);
for (const p of fn.params) fields.push(`${cDecl(p.type, pname(p))};`);
out.push(``, `typedef struct { ${fields.join(" ") || "char sc_unused;"} } ${pack};`);
const callArgs = [
...(lifted ? ["sc_a.sc_env"] : []),
...fn.params.map((p) => `sc_a.${pname(p)}`),
].join(", ");
const ret = fn.returnType;
const bodyCall = `${mangleFunction(fn.name)}(${callArgs})`;
const lines: string[] = [
`static void ${mangleTrampoline(fn.name)}(ScrFiber *sc_self, void *sc_ap0) {`,
` ${pack} sc_a = *(${pack} *)sc_ap0;`,
` free(sc_ap0);`,
];
if (ret.kind === "void") {
lines.push(` ${bodyCall};`);
} else {
lines.push(` ${cDecl(ret, "sc_r")} = ${bodyCall};`);
}
if (lifted) lines.push(` scr_closure_release(sc_a.sc_env);`);
const { definitions, ret, lines, spawnParams, argPackLines } =
emitArgPackAndTrampolinePrologue(fn);
out.push(...definitions);
lines.push(` if (!scr_exc_pending()) {`);
switch (ret.kind) {
case "void":
@@ -71,10 +105,6 @@ import { IrType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/
lines.push(`}`);
out.push(...lines);
const spawnParams = [
...(lifted ? ["ScrClosure *sc_env"] : []),
...fn.params.map((p) => cDecl(p.type, pname(p))),
];
const cache = fn.asyncCacheGlobal !== undefined ? mangleGlobal(fn.asyncCacheGlobal) : null;
const cycleCache =
fn.asyncCycleCacheGlobal !== undefined ? mangleGlobal(fn.asyncCycleCacheGlobal) : null;
@@ -83,10 +113,7 @@ import { IrType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/
...(cache !== null
? [` if (${cache}) return scr_promise_retain(${cache});`]
: []),
` ${pack} *sc_ap = malloc(sizeof *sc_ap);`,
` if (!sc_ap) { scr_trap("scriptc: out of memory\\n"); }`,
...(lifted ? [` sc_ap->sc_env = scr_closure_retain(sc_env);`] : []),
...fn.params.map((p) => ` sc_ap->${pname(p)} = ${pname(p)};`),
...argPackLines,
` ScrPromise *sc_p = scr_async_spawn(&${mangleTrampoline(fn.name)}, sc_ap);`,
...(cache !== null
? [
@@ -136,33 +163,9 @@ import { IrType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/
function emitGenScaffolding(E: CEmitter, out: string[]): void {
for (const fn of E.mod.functions) {
if (!fn.generator) continue;
const pack = mangleArgPack(fn.name);
const lifted = fn.captures !== undefined;
const fields: string[] = [];
if (lifted) fields.push("ScrClosure *sc_env;");
const boxedIds = new Set(fn.locals.filter((l) => l.boxed).map((l) => l.id));
const pname = (p: { localId: string }) =>
boxedIds.has(p.localId) ? mangleRawParam(p.localId) : mangleLocal(p.localId);
for (const p of fn.params) fields.push(`${cDecl(p.type, pname(p))};`);
out.push(``, `typedef struct { ${fields.join(" ") || "char sc_unused;"} } ${pack};`);
const callArgs = [
...(lifted ? ["sc_a.sc_env"] : []),
...fn.params.map((p) => `sc_a.${pname(p)}`),
].join(", ");
const ret = fn.returnType;
const bodyCall = `${mangleFunction(fn.name)}(${callArgs})`;
const lines: string[] = [
`static void ${mangleTrampoline(fn.name)}(ScrFiber *sc_self, void *sc_ap0) {`,
` ${pack} sc_a = *(${pack} *)sc_ap0;`,
` free(sc_ap0);`,
];
if (ret.kind === "void") {
lines.push(` ${bodyCall};`);
} else {
lines.push(` ${cDecl(ret, "sc_r")} = ${bodyCall};`);
}
if (lifted) lines.push(` scr_closure_release(sc_a.sc_env);`);
const { definitions, pack, lifted, pname, ret, lines, spawnParams, argPackLines } =
emitArgPackAndTrampolinePrologue(fn);
out.push(...definitions);
lines.push(` ScrGen *sc_g = scr_gen_of_fiber(sc_self);`);
// Normal completion stores the (typed) return value; void completes
// with the NONE slot — JS's undefined done-value. A GENRET unwind
@@ -196,10 +199,6 @@ import { IrType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/
);
out.push(...lines);
const spawnParams = [
...(lifted ? ["ScrClosure *sc_env"] : []),
...fn.params.map((p) => cDecl(p.type, pname(p))),
];
out.push(
`static void ${mangleGenDrop(fn.name)}(void *sc_ap0) {`,
` ${pack} sc_a = *(${pack} *)sc_ap0;`,
@@ -211,10 +210,7 @@ import { IrType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/
...(!lifted && !fn.params.some((p) => isRefCounted(p.type)) ? [` (void)sc_a;`] : []),
`}`,
`static ScrGen *${mangleGenSpawn(fn.name)}(${spawnParams.join(", ") || "void"}) {`,
` ${pack} *sc_ap = malloc(sizeof *sc_ap);`,
` if (!sc_ap) { scr_trap("scriptc: out of memory\\n"); }`,
...(lifted ? [` sc_ap->sc_env = scr_closure_retain(sc_env);`] : []),
...fn.params.map((p) => ` sc_ap->${pname(p)} = ${pname(p)};`),
...argPackLines,
` return scr_gen_new(&${mangleTrampoline(fn.name)}, sc_ap, &${mangleGenDrop(fn.name)});`,
`}`,
);
+96 -100
View File
@@ -109,6 +109,17 @@ interface LlStreamTypedRefContext {
adapters: Map<string, LlStreamTypedRefAdapter>;
}
interface LlArgPackAndTrampolinePrologue {
definitions: string[];
pack: string;
lifted: boolean;
fieldTys: string[];
ret: IrType;
tr: string[];
spawnParams: string[];
argPackLines: string[];
}
function ffiNativeTypeLl(
cls: IrFfiCallbackParamClass | IrFfiValueParamClass | IrFfiReturnClass,
): string {
@@ -2556,6 +2567,66 @@ class LlEmitter {
return out;
}
/** The argument-pack ABI and trampoline prefix shared by async functions
* and generators. Their promise/generator completion and spawn tails stay
* with the callers below. */
private emitArgPackAndTrampolinePrologue(fn: IrFunction): LlArgPackAndTrampolinePrologue {
const pack = mangleArgPack(fn.name);
const lifted = fn.captures !== undefined;
const fieldTys = [...(lifted ? ["ptr"] : []), ...fn.params.map((p) => this.llType(p.type))];
const definitions = [`%${pack} = type { ${fieldTys.join(", ") || "i8"} } ; ${fn.name} args`];
const sizeOf = `ptrtoint (ptr getelementptr (%${pack}, ptr null, i32 1) to ${this.sizeType})`;
this.declare(`declare void @free(ptr)`);
this.declare(`declare ptr @malloc(${this.sizeType})`);
this.declare(`declare zeroext i1 @scr_exc_pending()`);
const tr: string[] = [
`define internal void @${mangleTrampoline(fn.name)}(ptr %self, ptr %ap) ${FN_ATTRS} {`,
`entry:`,
];
const loads: string[] = [];
fieldTys.forEach((ty, i) => {
tr.push(
` %fp${i} = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${i}`,
` %a${i} = load ${ty}, ptr %fp${i}`,
);
loads.push(`${ty} %a${i}`);
});
tr.push(` call void @free(ptr %ap)`);
const ret = fn.returnType;
const retTy = this.llType(ret);
const bodyCall = `call ${retTy} @${mangleFunction(fn.name)}(${loads.join(", ")})`;
tr.push(retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`);
if (lifted && !this.wasi) {
tr.push(` call void @scr_closure_release(ptr %a0)`);
}
const spawnParams = fieldTys.map((ty, i) => `${ty} %a${i}`);
const argPackLines = [
` %ap = call ptr @malloc(${this.sizeType} ${sizeOf})`,
` %isnull = icmp eq ptr %ap, null`,
` br i1 %isnull, label %oom, label %ok`,
`oom:`,
` call void @sc_oom()`,
` unreachable`,
`ok:`,
];
if (lifted) {
// scr_closure_retain is a header static inline — the `_v` twin is
// the exported symbol.
argPackLines.push(` %env = call ptr @scr_closure_retain_v(ptr %a0)`);
}
fieldTys.forEach((ty, i) => {
const src = lifted && i === 0 ? "%env" : `%a${i}`;
argPackLines.push(
` %sp${i} = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${i}`,
` store ${ty} ${src}, ptr %sp${i}`,
);
});
return { definitions, pack, lifted, fieldTys, ret, tr, spawnParams, argPackLines };
}
/** Per-async-function machinery — emit-async.ts's scaffolding, .ll
* flavored: an argument-pack struct type, a fiber trampoline (unpacks,
* frees the pack, runs the ordinary compiled body, settles the
@@ -2584,46 +2655,20 @@ class LlEmitter {
}
for (const fn of this.mod.functions) {
if (fn.async !== true) continue;
const pack = mangleArgPack(fn.name);
const lifted = fn.captures !== undefined;
const fieldTys = [...(lifted ? ["ptr"] : []), ...fn.params.map((p) => this.llType(p.type))];
out.push(`%${pack} = type { ${fieldTys.join(", ") || "i8"} } ; ${fn.name} args`);
const sizeOf = `ptrtoint (ptr getelementptr (%${pack}, ptr null, i32 1) to ${this.sizeType})`;
this.declare(`declare void @free(ptr)`);
this.declare(`declare ptr @malloc(${this.sizeType})`);
this.declare(`declare zeroext i1 @scr_exc_pending()`);
const { definitions, ret, tr, spawnParams, argPackLines } =
this.emitArgPackAndTrampolinePrologue(fn);
out.push(...definitions);
this.declare(`declare ptr @scr_fiber_promise(ptr)`);
this.declare(`declare ptr @scr_async_spawn(ptr, ptr)`);
this.needOom();
// Trampoline: unpack, free, run the body, settle.
const tr: string[] = [
`define internal void @${mangleTrampoline(fn.name)}(ptr %self, ptr %ap) ${FN_ATTRS} {`,
`entry:`,
];
const loads: string[] = [];
fieldTys.forEach((ty, i) => {
tr.push(
` %fp${i} = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${i}`,
` %a${i} = load ${ty}, ptr %fp${i}`,
);
loads.push(`${ty} %a${i}`);
});
tr.push(` call void @free(ptr %ap)`);
const ret = fn.returnType;
const retTy = this.llType(ret);
const bodyCall = `call ${retTy} @${mangleFunction(fn.name)}(${loads.join(", ")})`;
tr.push(retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`);
if (this.wasi) {
// The coroutine body settles its own promise and owns the retained
// lifted environment until final suspension. Its initial call
// returns here at the first suspend (or final suspend).
tr.push(` ret void`, `}`, ``);
} else {
if (lifted) {
if (fn.captures !== undefined) {
this.declare(`declare void @scr_closure_release(ptr)`);
tr.push(` call void @scr_closure_release(ptr %a0)`);
}
tr.push(
` %pend = call zeroext i1 @scr_exc_pending()`,
@@ -2667,7 +2712,6 @@ class LlEmitter {
out.push(...tr);
// Spawn wrapper: pack the args (+1 moves in), spawn the fiber.
const params = fieldTys.map((ty, i) => `${ty} %a${i}`);
const cache = fn.asyncCacheGlobal !== undefined ? mangleGlobal(fn.asyncCacheGlobal) : null;
const cycleCache =
fn.asyncCycleCacheGlobal !== undefined ? mangleGlobal(fn.asyncCycleCacheGlobal) : null;
@@ -2678,8 +2722,11 @@ class LlEmitter {
if (cache !== null) {
this.declare(`declare void @scr_promise_mark_handled(ptr)`);
}
if (fn.captures !== undefined) {
this.declare(`declare ptr @scr_closure_retain_v(ptr)`);
}
const sp: string[] = [
`define internal ptr @${mangleAsyncSpawn(fn.name)}(${params.join(", ")}) ${FN_ATTRS} { ; spawn ${fn.name}`,
`define internal ptr @${mangleAsyncSpawn(fn.name)}(${spawnParams.join(", ")}) ${FN_ATTRS} { ; spawn ${fn.name}`,
`entry:`,
...(cache !== null
? [
@@ -2692,27 +2739,8 @@ class LlEmitter {
`cache_miss:`,
]
: []),
` %ap = call ptr @malloc(${this.sizeType} ${sizeOf})`,
` %isnull = icmp eq ptr %ap, null`,
` br i1 %isnull, label %oom, label %ok`,
`oom:`,
` call void @sc_oom()`,
` unreachable`,
`ok:`,
...argPackLines,
];
if (lifted) {
// scr_closure_retain is a header static inline — the `_v` twin is
// the exported symbol.
this.declare(`declare ptr @scr_closure_retain_v(ptr)`);
sp.push(` %env = call ptr @scr_closure_retain_v(ptr %a0)`);
}
fieldTys.forEach((ty, i) => {
const src = lifted && i === 0 ? "%env" : `%a${i}`;
sp.push(
` %sp${i} = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${i}`,
` store ${ty} ${src}, ptr %sp${i}`,
);
});
sp.push(
` %p = call ptr @scr_async_spawn(ptr @${mangleTrampoline(fn.name)}, ptr %ap)`,
...(cache !== null
@@ -2767,42 +2795,25 @@ class LlEmitter {
const out: string[] = [];
for (const fn of this.mod.functions) {
if (fn.generator === undefined) continue;
const pack = mangleArgPack(fn.name);
const lifted = fn.captures !== undefined;
const fieldTys = [...(lifted ? ["ptr"] : []), ...fn.params.map((p) => this.llType(p.type))];
out.push(`%${pack} = type { ${fieldTys.join(", ") || "i8"} } ; ${fn.name} args`);
const sizeOf = `ptrtoint (ptr getelementptr (%${pack}, ptr null, i32 1) to ${this.sizeType})`;
this.declare(`declare void @free(ptr)`);
this.declare(`declare ptr @malloc(${this.sizeType})`);
this.declare(`declare zeroext i1 @scr_exc_pending()`);
const {
definitions,
pack,
lifted,
fieldTys,
ret,
tr,
spawnParams,
argPackLines,
} = this.emitArgPackAndTrampolinePrologue(fn);
out.push(...definitions);
this.declare(`declare zeroext i1 @scr_exc_genret_pending()`);
this.declare(`declare void @scr_exc_clear()`);
this.declare(`declare ptr @scr_gen_of_fiber(ptr)`);
this.declare(`declare void @scr_gen_ret_to_out(ptr)`);
this.declare(`declare ptr @scr_gen_new(ptr, ptr, ptr)`);
this.needOom();
const tr: string[] = [
`define internal void @${mangleTrampoline(fn.name)}(ptr %self, ptr %ap) ${FN_ATTRS} {`,
`entry:`,
];
const loads: string[] = [];
fieldTys.forEach((ty, i) => {
tr.push(
` %fp${i} = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${i}`,
` %a${i} = load ${ty}, ptr %fp${i}`,
);
loads.push(`${ty} %a${i}`);
});
tr.push(` call void @free(ptr %ap)`);
const ret = fn.returnType;
const retTy = this.llType(ret);
const bodyCall = `call ${retTy} @${mangleFunction(fn.name)}(${loads.join(", ")})`;
tr.push(retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`);
if (lifted && !this.wasi) {
this.declare(`declare void @scr_closure_release(ptr)`);
tr.push(` call void @scr_closure_release(ptr %a0)`);
}
// Normal completion stores the (typed) return value; void completes
// with the NONE slot — JS's undefined done-value. A GENRET unwind
@@ -2878,29 +2889,14 @@ class LlEmitter {
// Spawn wrapper: pack the args (+1 moves in), allocate the
// SUSPENDED fiber — nothing runs until the first .next().
const params = fieldTys.map((ty, i) => `${ty} %a${i}`);
const sp: string[] = [
`define internal ptr @${mangleGenSpawn(fn.name)}(${params.join(", ")}) ${FN_ATTRS} { ; gen spawn ${fn.name}`,
`entry:`,
` %ap = call ptr @malloc(${this.sizeType} ${sizeOf})`,
` %isnull = icmp eq ptr %ap, null`,
` br i1 %isnull, label %oom, label %ok`,
`oom:`,
` call void @sc_oom()`,
` unreachable`,
`ok:`,
];
if (lifted) {
this.declare(`declare ptr @scr_closure_retain_v(ptr)`);
sp.push(` %env = call ptr @scr_closure_retain_v(ptr %a0)`);
}
fieldTys.forEach((ty, i) => {
const src = lifted && i === 0 ? "%env" : `%a${i}`;
sp.push(
` %sp${i} = getelementptr inbounds %${pack}, ptr %ap, i64 0, i32 ${i}`,
` store ${ty} ${src}, ptr %sp${i}`,
);
});
const sp: string[] = [
`define internal ptr @${mangleGenSpawn(fn.name)}(${spawnParams.join(", ")}) ${FN_ATTRS} { ; gen spawn ${fn.name}`,
`entry:`,
...argPackLines,
];
sp.push(
` %gg = call ptr @scr_gen_new(ptr @${mangleTrampoline(fn.name)}, ptr %ap, ptr @${mangleGenDrop(fn.name)})`,
` ret ptr %gg`,