Merge remote-tracking branch 'origin/main' into ni/1-libnode-loader

This commit is contained in:
Malte Ubl
2026-10-22 05:30:31 -07:00
31 changed files with 839 additions and 108 deletions
@@ -370,7 +370,7 @@ export class Client extends DispatcherBase {
localAddress: this.__localAddress,
};
if (channels.beforeConnect.hasSubscribers) {
channels.beforeConnect.publish({ connectParams });
channels.beforeConnect.publish({ connectParams, connector: this.__connector });
}
let settled = false;
const onConnected = (err: Error | null, socket: Socket | null): void => {
@@ -428,7 +428,7 @@ export class Client extends DispatcherBase {
if (this.__socket === socket) this.__onSocketClose(socket);
});
if (channels.connected.hasSubscribers) {
channels.connected.publish({ connectParams });
channels.connected.publish({ connectParams, connector: this.__connector, socket });
}
this.emit("connect", this.__url, [this as Dispatcher]);
this.__resume(false);
@@ -447,7 +447,7 @@ export class Client extends DispatcherBase {
): void {
this.__connecting = false;
if (channels.connectError.hasSubscribers) {
channels.connectError.publish({ connectParams, error: err });
channels.connectError.publish({ connectParams, connector: this.__connector, error: err });
}
this.__onError(err);
this.emit("connectionError", this.__url, [this as Dispatcher], err);
@@ -658,7 +658,7 @@ export class Client extends DispatcherBase {
}
header += request.headers;
if (channels.sendHeaders.hasSubscribers) {
channels.sendHeaders.publish({ request: request.diagnosticView(), headers: header });
channels.sendHeaders.publish({ request: request.diagnosticView(), headers: header, socket });
}
if (body === null) {
if (contentLength === 0) socket.write(`${header}content-length: 0\r\n\r\n`);
+7 -2
View File
@@ -244,9 +244,12 @@ export function cycleAppendHelper(): string[] {
}
/** A collector header to install: the object type's constant ScrCycType
* descriptor (its trace and teardown; see cycTypeDescriptor). */
* descriptor (its trace and teardown; see cycTypeDescriptor). `acyclic`:
* the descriptor's trace is scr_cyc_no_children, and `buf` starts out
* SCR_CYC_ACYCLIC (scr_cyc_alloc does the same on its path). */
export interface CycHeader {
type: string;
acyclic?: boolean;
}
/** The first collector statistics index of an emitted ScrCycType
@@ -391,7 +394,9 @@ function allocLines(
` %sa.cgp = getelementptr inbounds i8, ptr %sa.blk, i64 2`,
` store i16 0, ptr %sa.cgp ; ScrCycHdr.color, gen`,
` %sa.bufp = getelementptr inbounds i8, ptr %sa.blk, i64 ${CYC_BUF}`,
` store i32 0, ptr %sa.bufp ; ScrCycHdr.buf`,
cyc.acyclic === true
? ` store i32 -1, ptr %sa.bufp ; ScrCycHdr.buf = SCR_CYC_ACYCLIC`
: ` store i32 0, ptr %sa.bufp ; ScrCycHdr.buf`,
` %sa.live = load i64, ptr @scr_cyc_live`,
` %sa.live1 = add i64 %sa.live, 1`,
` store i64 %sa.live1, ptr @scr_cyc_live`,
@@ -157,6 +157,44 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts",
expect(body(mangleClassRelease("Owner"))).toContain("load i32, ptr %bufp");
});
test("classes whose trace visits nothing start acyclic in a traced hierarchy", () => {
const mapper = { kind: "object" as const, className: "Mapper" };
const mod: IrModule = {
irVersion: 17,
sourceFile: loc.file,
entry: "main",
functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }],
classes: [
{ name: "Mapper", fields: [], methods: [], loc },
// A dyn field can reach anything: the hierarchy carries collector headers.
{
name: "Linked",
base: "Mapper",
fields: [
{ name: "next", type: mapper },
{ name: "hook", type: DYN },
],
methods: [],
loc,
},
{ name: "Simple", base: "Mapper", fields: [{ name: "value", type: F64 }], methods: [], loc },
],
};
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
const body = (name: string): string =>
new RegExp(`^define internal [a-z]+ @${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(llvm)?.[0] ?? "";
const descriptor = (cls: string): string =>
llvm.split("\n").find((line) => line.startsWith(`@${mangleClassTrace(cls)}.cyctype = `)) ?? "";
expect(descriptor("Simple")).toContain("{ ptr @scr_cyc_no_children, ");
expect(body(mangleClassNew("Simple"))).toContain("store i32 -1, ptr %sa.bufp");
expect(descriptor("Linked")).toContain(`{ ptr @${mangleClassTrace("Linked")}, `);
expect(body(mangleClassNew("Linked"))).toContain("store i32 0, ptr %sa.bufp");
// Releases share the hierarchy's possible-root step, which skips the
// call for SCR_CYC_ACYCLIC, and a death unbuffers only below -1.
expect(body(mangleClassRelease("Simple"))).toContain("icmp slt i32 %buf, 0");
expect(body(mangleClassReleaseDirect("Simple"))).toContain("icmp slt i32 %dbuf, -1");
});
test("ProgramLifetime classes pack their fields after a 32-bit header", () => {
const node = { kind: "object" as const, className: "Node" };
const leaf = { kind: "object" as const, className: "Leaf" };
+12 -3
View File
@@ -906,6 +906,15 @@ export function emitClassShapes(
})),
];
const refFields = indexedFields.filter((f) => isRefCounted(f.type));
const tracedFields = traced
? indexedFields.filter((f) => traceAdapter(host, f.type) !== null)
: [];
// A traced class whose trace visits nothing carries the collector header
// only because its hierarchy is traced: it is never on a cycle. Its
// descriptor names scr_cyc_no_children and its objects start out
// SCR_CYC_ACYCLIC, so releases never buffer them.
const acyclic =
traced && runtimePrefix === null && !isStreamRooted && tracedFields.length === 0;
const bounded =
runtimePrefix !== null ||
isStreamRooted ||
@@ -1089,11 +1098,12 @@ export function emitClassShapes(
`entry:`,
];
const cycType = `@${mangleClassTrace(cls.name)}.cyctype`;
if (acyclic) host.declare(`declare void @scr_cyc_no_children(ptr, ptr, ptr)`);
if (traced)
defs.push(
cycTypeDescriptor(
cycType,
`@${mangleClassTrace(cls.name)}`,
acyclic ? "@scr_cyc_no_children" : `@${mangleClassTrace(cls.name)}`,
`@${mangleClassGcFree(cls.name)}`,
host.nextCycKind?.() ?? 0,
),
@@ -1113,7 +1123,7 @@ export function emitClassShapes(
...emitObjectAlloc(
host,
sizeOf,
traced && !lifetime ? { type: cycType } : null,
traced && !lifetime ? { type: cycType, acyclic } : null,
storedHeaderBytes(meta),
),
);
@@ -1188,7 +1198,6 @@ export function emitClassShapes(
// exactly the complement, then free (the trace/teardown complement
// contract in scr_runtime.h).
const indexed = indexedFields;
const tracedFields = indexed.filter((f) => traceAdapter(host, f.type) !== null);
const untracedRefFields = indexed.filter(
(f) => isRefCounted(f.type) && traceAdapter(host, f.type) === null,
);
@@ -1117,8 +1117,11 @@ export function emitAsyncAwaitCall(
``,
];
}
emitter.declare(`declare void @scr_async_inline_enter(ptr)`);
emitter.declare(`declare void @scr_async_inline_exit(ptr)`);
const framed = emitter.fiberlessFrames;
if (framed) {
emitter.declare(`declare void @scr_async_inline_enter(ptr)`);
emitter.declare(`declare void @scr_async_inline_exit(ptr)`);
}
emitter.declare(`declare void @scr_await_hop()`);
emitter.declare(`declare ptr @scr_promise_new()`);
emitter.declare(`declare void @scr_promise_reject_pending(ptr)`);
@@ -1127,11 +1130,12 @@ export function emitAsyncAwaitCall(
const out = [
head,
`entry:`,
` %frame = alloca { ptr, ptr }`,
` call void @scr_async_inline_enter(ptr %frame)`,
...(framed
? [` %frame = alloca { ptr, ptr }`, ` call void @scr_async_inline_enter(ptr %frame)`]
: []),
retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`,
...emitter.pendingTestLines("%pend"),
` call void @scr_async_inline_exit(ptr %frame)`,
...(framed ? [` call void @scr_async_inline_exit(ptr %frame)`] : []),
// Adopting: the then job's turn comes before the settled promise's
// own reaction, whichever way the body completed.
...(adopt ? [` call void @scr_await_hop()`] : []),
+39 -16
View File
@@ -1798,16 +1798,33 @@ export class LlEmitter {
});
}
/** Whether a fiberless body running on its caller's stack needs the
* frame scr_async_inline_enter/exit keep: it isolates the Error.stack
* frame chain, which only stack-observing programs push, and the
* AsyncLocalStorage context, which only programs that enter stores
* change (RuntimeFeatures asyncContext), as a fresh fiber would. Other
* programs run the body with a plain call. The frame also counts the
* bodies running on a fiber's stack for the runtime's trap on a fiberless
* body that suspends. */
get fiberlessFrames(): boolean {
return this.stackTraces || this.schedulesInContext;
}
/** The spawn wrapper of a fiberless async function: runs the body on the
* caller's stack between scr_async_inline_enter/leave and settles the
* fresh promise from its outcome — the fiber trampoline's epilogue,
* without the fiber. */
* caller's stack (between scr_async_inline_enter/leave when
* fiberlessFrames) and settles a fresh promise from its outcome — the
* fiber trampoline's epilogue, without the fiber. */
emitFiberlessAsyncSpawn(fn: IrFunction, spawnParams: string[]): string[] {
const ret = fn.returnType;
const retTy = this.llType(ret);
const lifted = fn.captures !== undefined;
this.declare(`declare void @scr_async_inline_enter(ptr)`);
this.declare(`declare ptr @scr_async_inline_leave(ptr)`);
const framed = this.fiberlessFrames;
if (framed) {
this.declare(`declare void @scr_async_inline_enter(ptr)`);
this.declare(`declare ptr @scr_async_inline_leave(ptr)`);
} else {
this.declare(`declare ptr @scr_promise_new()`);
}
this.declare(`declare void @scr_promise_reject_pending(ptr)`);
if (lifted) {
this.declare(`declare ptr @scr_closure_retain_v(ptr)`);
@@ -1817,15 +1834,17 @@ export class LlEmitter {
const out = [
`define internal ptr @${mangleAsyncSpawn(fn.name)}(${spawnParams.join(", ")}) ${FN_ATTRS} { ; fiberless ${fn.name}`,
`entry:`,
` %frame = alloca { ptr, ptr }`,
...(framed ? [` %frame = alloca { ptr, ptr }`] : []),
// The fiber path holds +1 on a lifted environment for the body's
// duration (packed, released by the trampoline); keep that lifetime.
...(lifted ? [` %env = call ptr @scr_closure_retain_v(ptr %a0)`] : []),
` call void @scr_async_inline_enter(ptr %frame)`,
...(framed ? [` call void @scr_async_inline_enter(ptr %frame)`] : []),
retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`,
...(lifted ? [` call void @scr_closure_release(ptr %a0)`] : []),
...this.pendingTestLines("%pend"),
` %p = call ptr @scr_async_inline_leave(ptr %frame)`,
framed
? ` %p = call ptr @scr_async_inline_leave(ptr %frame)`
: ` %p = call ptr @scr_promise_new()`,
` br i1 %pend, label %thrown, label %clean`,
`clean:`,
...this.asyncFulfillLines(ret, "%p"),
@@ -1864,23 +1883,27 @@ export class LlEmitter {
}
/** The inline await entry of a fiberless async function (`sc_ai_*`): its
* body on the caller's stack between scr_async_inline_enter/exit, as in
* its spawn wrapper, returning the result and leaving a throw pending for
* the awaiting coroutine (emitCoroInlineAwait). */
* body on the caller's stack (between scr_async_inline_enter/exit when
* fiberlessFrames), as in its spawn wrapper, returning the result and
* leaving a throw pending for the awaiting coroutine (emitCoroInlineAwait). */
emitAsyncInlineAwaitCall(fn: IrFunction): string[] {
const retTy = this.llType(fn.returnType);
const params = fn.params.map((p, i) => `${this.llType(p.type)} %a${i}`);
const args = params.join(", ");
this.declare(`declare void @scr_async_inline_enter(ptr)`);
this.declare(`declare void @scr_async_inline_exit(ptr)`);
const framed = this.fiberlessFrames;
if (framed) {
this.declare(`declare void @scr_async_inline_enter(ptr)`);
this.declare(`declare void @scr_async_inline_exit(ptr)`);
}
const bodyCall = `call ${retTy} @${mangleFunction(fn.name)}(${args})`;
return [
`define internal ${retTy} @${mangleAsyncInlineAwaitCall(fn.name)}(${args}) ${FN_ATTRS} { ; inline await ${fn.name}`,
`entry:`,
` %frame = alloca { ptr, ptr }`,
` call void @scr_async_inline_enter(ptr %frame)`,
...(framed
? [` %frame = alloca { ptr, ptr }`, ` call void @scr_async_inline_enter(ptr %frame)`]
: []),
retTy === "void" ? ` ${bodyCall}` : ` %r = ${bodyCall}`,
` call void @scr_async_inline_exit(ptr %frame)`,
...(framed ? [` call void @scr_async_inline_exit(ptr %frame)`] : []),
retTy === "void" ? ` ret void` : ` ret ${retTy} %r`,
`}`,
``,
@@ -60,6 +60,9 @@ test("publication walkers mark and push inline, with the runtime for the rest",
expect(fn).toContain("icmp ult i64 %mrc, 4294967294");
expect(fn).toContain("getelementptr inbounds { ptr, i32, i32 }, ptr %mlog, i64 %mlogged");
expect(fn).toContain("store i64 -1, ptr %o");
// A headered object leaves its candidate buffer only when `buf` is below
// -1: SCR_CYC_ACYCLIC (-1) marks an object that is never in one.
expect(fn).toContain("icmp sge i32 %mbufw, -1");
// Anything else goes through scr_pub_mark.
expect(fn).toMatch(/call zeroext i1 @scr_pub_mark\(ptr %ctx, ptr %o, i1 (true|false), i32 4\)/);
// A string is a leaf, marked at once; other references go on the stack,
@@ -434,12 +434,13 @@ export class PublishWalkers {
let buf = classId || compact ? String(PUB_LOG_COUNT) : "0";
if (cyc) {
// ScrCycHdr.buf, the word before the object: its top bit while the
// object is a buffered candidate.
// object is a buffered candidate, and -1 (SCR_CYC_ACYCLIC) for an
// object that never is one.
this.host.declare(`declare i32 @scr_cyc_pub_take(ptr)`);
lines.push(
` %mbufp = getelementptr inbounds i8, ptr %o, i64 -4`,
` %mbufw = load i32, ptr %mbufp`,
` %mfree = icmp sge i32 %mbufw, 0`,
` %mfree = icmp sge i32 %mbufw, -1`,
` br i1 %mfree, label %mput, label %mtake`,
`mtake:`,
` %mix = call i32 @scr_cyc_pub_take(ptr %o)`,
+8 -7
View File
@@ -1369,7 +1369,7 @@ export function directChildReleaseLines(
lines.push(
` %${tag}.bufp = getelementptr i8, ptr ${value}, ${S} -${host.cycleBufOffset}`,
` %${tag}.buf = load i32, ptr %${tag}.bufp`,
` %${tag}.queued = icmp slt i32 %${tag}.buf, 0`,
` %${tag}.queued = icmp slt i32 %${tag}.buf, -1 ; buffered, not SCR_CYC_ACYCLIC`,
` br i1 %${tag}.queued, label %${tag}.unbuffer, label %${tag}.teardown`,
`${tag}.unbuffer:`,
` call void @scr_cyc_on_dead(ptr ${value})`,
@@ -1595,8 +1595,9 @@ function requestCycleAppend(host: ShapeHost): boolean {
/** The possible-cycle-root step of a release that left a headered object
* alive: scr_cyc_on_release with its early return inlined. Test the sign
* of `buf` (the i32 at obj-4; negative for a queued candidate or a tenured
* object), and buffer a new candidate: through the module's
* of `buf` (the i32 at obj-4; negative for a queued candidate, a young
* object, or an object that is never a candidate, SCR_CYC_ACYCLIC), and
* buffer a new candidate: through the module's
* cycleAppendHelper in optimized builds that support it, through
* scr_cyc_on_release otherwise. Every emitted release of a headered object
* shares this sequence (scr_runtime.h asserts the offsets); it ends by
@@ -1619,16 +1620,16 @@ export function cycleRootLines(host: ShapeHost, done: string): string[] {
}
/** The death of a headered object: scr_cyc_on_dead with its early return
* inlined. Only an object still in a candidate buffer
* (`buffered` nonzero) has an entry to remove; the sequence continues at
* the new block `next`. */
* inlined. Only an object still in a candidate buffer (`buf` negative and
* not SCR_CYC_ACYCLIC, which is -1) has an entry to remove; the sequence
* continues at the new block `next`. */
export function cycleDeadLines(host: ShapeHost, next: string): string[] {
const S = host.sizeType;
host.declare(`declare void @scr_cyc_on_dead(ptr)`);
return [
` %dbufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufOffset}`,
` %dbuf = load i32, ptr %dbufp`,
` %dqueued = icmp slt i32 %dbuf, 0`,
` %dqueued = icmp slt i32 %dbuf, -1 ; buffered, not SCR_CYC_ACYCLIC`,
` br i1 %dqueued, label %unbuffer, label %${next}`,
`unbuffer:`,
` call void @scr_cyc_on_dead(ptr %o)`,
@@ -407,9 +407,27 @@ function withTypedReceiver(value: IrExpr, receiver: IrExpr): IrExpr {
});
}
/** Re-adds the entries of `set` after the first `from` sorted. */
function sortAdded(set: Set<string>, from: number): void {
const added = [...set].slice(from).sort();
/** Re-adds the classes of `set` after the first `from` in a fixed order:
* the program's classes by name, then the classes the runtime implements by
* name. The dispatch helpers add each class's branch in front of the
* earlier ones, so a runtime class's branch, which answers its methods for
* instances of the program's subclasses too, is tested before the
* subclasses' own branches. */
function sortAdded(lowerer: Lowerer, set: Set<string>, from: number): void {
const runtime = (name: string): boolean => {
const info = lowerer.classes.get(name);
return !!(
info &&
(info.def.runtime || info.builtinEmitter || info.builtinStream || info.builtinError)
);
};
const added = [...set]
.slice(from)
.sort(
(left, right) =>
Number(runtime(left)) - Number(runtime(right)) ||
(left < right ? -1 : left > right ? 1 : 0),
);
for (const name of added) set.delete(name);
for (const name of added) set.add(name);
}
@@ -932,10 +950,10 @@ export class ClassDynamicDispatch {
return true;
},
});
// The classes this pass found join the tables by name, not in the order
// the scan met them.
sortAdded(this.boxed, boxedBefore);
sortAdded(this.boxedConstructors, boxedConstructorsBefore);
// The classes this pass found join the tables in a fixed order, not in
// the order the scan met them.
sortAdded(lowerer, this.boxed, boxedBefore);
sortAdded(lowerer, this.boxedConstructors, boxedConstructorsBefore);
changed = this.caughtDispatch.dynamicPayloads(lowerer, caughtSites, this.boxed) || changed;
if (this.boxed.size === 0 && rewrite.size === 0) return changed;
const converting = this.convertingHints(lowerer);
@@ -36,7 +36,8 @@ import {
import { identityPreservingWidening } from "./coercions/identity.js";
import { scanDivergences } from "./divergences.js";
import { applyKeyOrderWitnesses, KeyOrderNotes } from "./key-order-witness.js";
import { observeRecordLiteralRebuilds } from "../../ir/traverse.js";
import { exprFunctionReference } from "../../ir/function-references.js";
import { everyStmtList, observeRecordLiteralRebuilds } from "../../ir/traverse.js";
import { checkedClassAssertion } from "./class-assertions.js";
import { narrowStoredClassValue, narrowGenericClassValue } from "./class-unions.js";
import {
@@ -3269,11 +3270,13 @@ export class Lowerer {
if (typeof helper === "string") this.sharedHelperNames.add(helper);
else this.sharedHelpers.add(helper);
}
/** Gives each shared helper the location of the entry file's start: the
* helpers marked by shareHelper, and those interned by key in the helper
* tables. */
private settleSharedHelperLocations(): void {
const loc: SrcLoc = { file: this.entry.fileName, start: 0, end: 0 };
/** Gives each shared helper (those marked by shareHelper, and those
* interned by key in the helper tables) one location for all its nodes:
* the earliest site in `functions` that uses it, by file and position, so
* reports such as --explain-dynamic point at a use whatever order bodies
* lowered in. A helper used only by other shared helpers takes their
* location; one with no use takes the entry file's start. */
private settleSharedHelperLocations(functions: readonly IrFunction[]): void {
const interned = new Set<string>([...this.sharedHelperNames, ...this.classMethodValueHelpers]);
for (const table of [
this.equalityHelpers,
@@ -3296,21 +3299,68 @@ export class Lowerer {
for (const name of table.values()) interned.add(name);
for (const fn of [...this.liftedFns, ...this.implicitFns])
if (interned.has(fn.name)) this.sharedHelpers.add(fn);
const seen = new Set<object>();
const visit = (node: unknown): void => {
if (typeof node !== "object" || node === null || seen.has(node)) return;
seen.add(node);
if (Array.isArray(node)) {
for (const item of node) visit(item);
return;
if (this.sharedHelpers.size === 0) return;
const shared = new Map([...this.sharedHelpers].map((fn) => [fn.name, fn]));
const earlier = (left: SrcLoc, right: SrcLoc): boolean =>
left.file < right.file || (left.file === right.file && left.start < right.start);
// The earliest use of each shared helper outside the shared helpers, and
// which shared helpers each shared helper uses.
const uses = new Map<string, SrcLoc>();
const usedBy = new Map<string, Set<string>>();
for (const fn of functions) {
const inShared = shared.has(fn.name) ? fn.name : undefined;
everyStmtList(fn.body, {
stmt: () => true,
expr: (expr) => {
const name = expr.kind === "call" ? expr.callee : exprFunctionReference(expr);
if (name === undefined || name === inShared || !shared.has(name)) return true;
if (inShared !== undefined) {
let users = usedBy.get(name);
if (users === undefined) usedBy.set(name, (users = new Set()));
users.add(inShared);
} else {
const current = uses.get(name);
if (current === undefined || earlier(expr.loc, current)) uses.set(name, expr.loc);
}
return true;
},
});
}
// Helpers reached only through other shared helpers take the earliest
// location among those, to a fixed point.
for (let changed = true; changed;) {
changed = false;
for (const [name, users] of usedBy) {
for (const user of users) {
const loc = uses.get(user);
const current = uses.get(name);
if (loc !== undefined && (current === undefined || earlier(loc, current))) {
uses.set(name, loc);
changed = true;
}
}
}
const record = node as Record<string, unknown>;
for (const key of Object.keys(record)) {
if (key === "loc") record[key] = loc;
else if (key !== "type") visit(record[key]);
}
};
for (const fn of this.sharedHelpers) visit(fn);
}
const fallback: SrcLoc = { file: this.entry.fileName, start: 0, end: 0 };
for (const fn of this.sharedHelpers) {
const use = uses.get(fn.name) ?? fallback;
const loc: SrcLoc = { file: use.file, start: use.start, end: use.end };
const seen = new Set<object>();
const visit = (node: unknown): void => {
if (typeof node !== "object" || node === null || seen.has(node)) return;
seen.add(node);
if (Array.isArray(node)) {
for (const item of node) visit(item);
return;
}
const record = node as Record<string, unknown>;
for (const key of Object.keys(record)) {
if (key === "loc") record[key] = loc;
else if (key !== "type") visit(record[key]);
}
};
visit(fn);
}
}
/** The first lifted function named `name`, the one
* `liftedFns.find((fn) => fn.name === name)` returns. */
@@ -7404,7 +7454,7 @@ export class Lowerer {
// Lambdas lifted while lowering any of the above (plus synthetic
// array-HOF loop functions, which ride the same list), and the
// implicit-any instances lowered eagerly at their first call sites.
this.settleSharedHelperLocations();
this.settleSharedHelperLocations([...functions, ...this.liftedFns, ...this.implicitFns]);
functions.push(...this.liftedFns);
functions.push(...this.implicitFns);
functions.push(...runtimeClassConstructs(this, functions));
@@ -8120,7 +8170,13 @@ export class Lowerer {
]);
observeRecordLiteralRebuilds(null);
for (const finalize of this.shapeOrderHelperFinalizers) finalize();
this.settleSharedHelperLocations();
this.settleSharedHelperLocations([
...orderedUnits,
...initFunctions,
...instanceFunctions,
...this.liftedFns,
...this.implicitFns,
]);
// Lifted and implicit functions are created in lowering order; they
// are assembled by name.
const functions = [
+3 -1
View File
@@ -4354,7 +4354,9 @@ function canBoxDynCompositeAt(
}
}
default:
return false;
// Runtime handles (a socket in a diagnostics message) box by
// reference wherever the walker meets them, as at the top level.
return DYN_HANDLE_KINDS.has(t.kind);
}
}
@@ -62,7 +62,9 @@ test("awaiting a direct call to a body that never suspends skips the promise", a
expect(definition(llvm, "sc_f_middle")).toContain("call double @sc_aw_leaf(double");
const entry = definition(llvm, "sc_aw_leaf");
expect(entry).toContain("call double @sc_f_leaf(double %a0)");
expect(entry).toContain("call void @scr_async_inline_exit(ptr %frame)");
// Nothing here enters an AsyncLocalStorage store or reads a stack, so the
// body runs with a plain call (fiberlessFrames).
expect(entry).not.toContain("@scr_async_inline_enter");
// The clean result takes the await's microtask turn and returns directly;
// only the throw path builds the rejected promise.
const clean = entry.slice(entry.indexOf("clean:"), entry.indexOf("thrown:"));
@@ -122,10 +124,10 @@ test("returning a direct call adopts its promise one microtask turn later", asyn
// A body that never suspends: the adoption's then-job turn, then the
// settled promise's own turn, still without materializing the promise.
const entry = definition(llvm, "sc_ad_leaf");
const exit = entry.indexOf("call void @scr_async_inline_exit(ptr %frame)");
const body = entry.indexOf("call double @sc_f_leaf(double %a0)");
const hop = entry.indexOf("call void @scr_await_hop()");
expect(exit).toBeGreaterThan(0);
expect(hop).toBeGreaterThan(exit);
expect(body).toBeGreaterThan(0);
expect(hop).toBeGreaterThan(body);
expect(hop).toBeLessThan(entry.indexOf("clean:"));
const clean = entry.slice(entry.indexOf("clean:"), entry.indexOf("thrown:"));
expect(clean).toContain("call void @scr_await_hop()");
@@ -190,7 +192,7 @@ test("bodies that suspend only at their own awaits compile to stackless coroutin
expect(middle).toContain("call double @sc_ai_leaf(double");
const inline = definition(llvm, "sc_ai_leaf");
expect(inline).toContain("call double @sc_f_leaf(double %a0)");
expect(inline).toContain("call void @scr_async_inline_exit(ptr %frame)");
expect(inline).not.toContain("@scr_async_inline_enter");
expect(middle).toContain("call zeroext i1 @scr_coro_hop_prepare(ptr");
expect(middle).toContain("call void @scr_promise_reject_pending(ptr");
expect(middle).toContain("call void @scr_coro_async_finish(ptr");
@@ -205,6 +207,58 @@ test("bodies that suspend only at their own awaits compile to stackless coroutin
const entry = definition(llvm, "sc_aw_middle");
expect(entry).toContain("call ptr @sc_as_middle(double %a0)");
expect(entry).toContain("call double @scr_await_f64(ptr %p)");
// A body that never suspends stays fiberless.
expect(definition(llvm, "sc_as_leaf")).toContain("@scr_async_inline_enter(");
// A body that never suspends stays fiberless: its spawn wrapper calls the
// body and settles a fresh promise.
const spawn = definition(llvm, "sc_as_leaf");
expect(spawn).toContain("call double @sc_f_leaf(double %a0)");
expect(spawn).toContain("call ptr @scr_promise_new()");
expect(spawn).not.toContain("@scr_async_spawn");
});
const framedSources = {
// Entering a store makes the frame isolate the AsyncLocalStorage context.
context: `
import { AsyncLocalStorage } from "node:async_hooks";
const store = new AsyncLocalStorage<string>();
async function leaf(x: number): Promise<number> {
store.enterWith("leaf");
return x + 1;
}`,
// Reading a stack makes the frame isolate the Error.stack frame chain.
stack: `
async function leaf(x: number): Promise<number> {
if (x < 0) console.log(new Error("negative").stack);
return x + 1;
}`,
};
test("fiberless bodies keep their frame where it isolates a context or a stack", async () => {
for (const [name, prefix] of Object.entries(framedSources)) {
const llvm = await emit(`${prefix}
async function slow(x: number): Promise<number> {
await null;
return x;
}
async function coroutine(x: number): Promise<number> {
return (await slow(x)) + (await leaf(x));
}
async function* tick(): AsyncGenerator<number, void, undefined> {
yield 0;
}
async function fibered(x: number): Promise<number> {
for await (const t of tick()) x += t;
return await leaf(x);
}
async function main(): Promise<void> {
const pending = leaf(1);
console.log((await coroutine(2)) + (await fibered(3)) + (await pending));
}
void main();
`);
for (const symbol of ["sc_ai_leaf", "sc_aw_leaf", "sc_as_leaf"]) {
expect(definition(llvm, symbol), `${name} ${symbol}`).toContain(
"call void @scr_async_inline_enter(ptr %frame)",
);
}
}
});
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6620-weak-keys-net-sockets.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7103-fiberless-plain-calls.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/acyclic-classes-in-traced-hierarchies.ts"
],
"diags": []
}
+47 -15
View File
@@ -131,13 +131,6 @@ uint32_t scr_arr_ops_register(ScrArrOps *ops) {
const ScrArrOps *scr_arr_ops_overflow(const ScrArr *a) { return scr_arr_ext_of(a)->ops; }
/* Record `ops` as the element ops of `a` (NULL for none). */
static void scr_arr_set_ops(ScrArr *a, const ScrArrOps *ops) {
uint32_t id = scr_arr_ops_id(ops);
a->spare = (uint16_t)((a->spare & ((1u << SCR_ARR_OPS_SHIFT) - 1)) | (id << SCR_ARR_OPS_SHIFT));
if (id == SCR_ARR_OPS_OVERFLOW) scr_arr_ext(a)->ops = ops;
}
/* Canonical array indices grow indexed storage and may leave holes. Negative,
* fractional, NaN, infinity, and 2^32-1 numeric keys use the ordinary
* property vector; only a missing property or a low-level read of a hole
@@ -402,9 +395,10 @@ static void scr_arr_sparse_drop_prefix(ScrArr *a, size_t count) {
#define SCR_ARR_MIN_GROWTH ((size_t)4)
/* Inline slots: an array built by scr_arr_new_small / scr_arr_new_ops_small
* (empty literals the compiler sees appended to) keeps its first dense
* slots and states in its own block, right after the ScrArr, so it costs
* one allocation and its first elements share the header's cache lines.
* (empty literals the compiler sees appended to), or constructed with a
* known element count of up to SCR_ARR_KNOWN_INLINE_MAX, keeps its first
* dense slots and states in its own block, right after the ScrArr, so it
* costs one allocation and its first elements share the header's cache lines.
* `spare` records it. Growth past the inline capacity moves the slots to an
* ordinary allocation; the inline room then stays unused until the array is
* freed. */
@@ -526,6 +520,30 @@ static size_t scr_arr_small_cap(size_t prefix, bool bools) {
#endif
}
/* The most slots a construction with a known element count (array
* literals, copies, slices and the arrays the runtime builds) keeps inline.
* Larger counts, and blocks past the small-object allocator, keep separate
* storage: an array that then grows leaves its inline room unused, which
* this bound keeps small. */
#define SCR_ARR_KNOWN_INLINE_MAX ((size_t)16)
/* Inline capacity of a construction of `need` elements with `prefix` bytes
* of block before the slots, rounded up to fill the block; 0 when its slots
* stay separate. */
static size_t scr_arr_known_cap(size_t prefix, size_t need, bool bools) {
if (need == 0 || need > SCR_ARR_KNOWN_INLINE_MAX) return 0;
#if SCR_SMALL_ALLOC
size_t unit = bools ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES;
size_t bytes = prefix + need * unit;
if (bytes > SCR_SA_MAX) return 0;
return (scr_sa_class_size(scr_sa_class(bytes)) - prefix) / unit;
#else
(void)prefix;
(void)bools;
return need;
#endif
}
static void scr_arr_use_inline(ScrArr *a, size_t cap) {
a->data = (uint64_t *)((char *)a + SCR_ARR_INLINE_OFFSET);
a->cap = (uint32_t)cap;
@@ -1180,7 +1198,8 @@ static inline __attribute__((always_inline)) ScrArr *scr_arr_make(ScrElemKind el
size_t initial_cap,
bool small) {
size_t unit = elem == SCR_ELEM_BOOL ? SCR_ARR_BOOL_SLOT_BYTES : SCR_ARR_SLOT_BYTES;
size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, elem == SCR_ELEM_BOOL) : 0;
size_t inline_cap = small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET, elem == SCR_ELEM_BOOL)
: scr_arr_known_cap(SCR_ARR_INLINE_OFFSET, initial_cap, elem == SCR_ELEM_BOOL);
ScrArr *a = scr_mem_alloc(inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * unit : sizeof(ScrArr));
if (!a) scr_arr_oom();
a->rc = 1;
@@ -1310,11 +1329,17 @@ static void scr_arr_gc_free(void *a0) {
scr_cyc_free_inline(a);
}
static ScrArr *scr_arr_make_ops(const ScrArrOps *ops, size_t initial_cap, bool small) {
/* Inlined into each constructor like scr_arr_make: with `small` and the
* count constant, scr_arr_new_ops_small's size class folds to a table
* index. */
static inline __attribute__((always_inline)) ScrArr *scr_arr_make_ops(const ScrArrOps *ops,
size_t initial_cap,
bool small) {
ScrArr *a;
bool cyc = ops->trace != NULL;
size_t prefix = SCR_ARR_INLINE_OFFSET + (cyc ? sizeof(ScrCycHdr) : 0);
size_t inline_cap =
small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET + (cyc ? sizeof(ScrCycHdr) : 0), false) : 0;
small ? scr_arr_small_cap(prefix, false) : scr_arr_known_cap(prefix, initial_cap, false);
size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * SCR_ARR_SLOT_BYTES : sizeof(ScrArr);
if (cyc) {
static ScrCycType cyc_type = { &scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY };
@@ -1328,12 +1353,19 @@ static ScrArr *scr_arr_make_ops(const ScrArrOps *ops, size_t initial_cap, bool s
a->cap = 0;
a->elem = SCR_ELEM_REF;
a->cyc = cyc;
a->spare = 0;
/* The ops id is written with the other header fields: reading `spare`
* back after scr_arr_use_inline sets its inline flag stalls the
* store-to-load forward. */
uint32_t id = scr_arr_ops_id(ops);
a->spare = (uint16_t)(id << SCR_ARR_OPS_SHIFT);
a->filled = 0;
a->data = NULL;
scr_arr_set_ops(a, ops);
if (inline_cap) scr_arr_use_inline(a, inline_cap);
else if (initial_cap > 0) scr_arr_grow_dense_to(a, initial_cap, SCR_ARR_GROW_EXACT);
/* After the storage: ops past the registry live in the ext, whose
* pointer precedes the dense allocation, so inline slots move out first
* (scr_arr_ext). */
if (id == SCR_ARR_OPS_OVERFLOW) scr_arr_ext(a)->ops = ops;
#ifdef SCR_RC_AUDIT
scr_live_arrays++;
#endif
+24 -6
View File
@@ -361,7 +361,15 @@ uint32_t scr_cyc_register(ScrCycType *type) {
}
void *scr_cyc_alloc(size_t size, ScrCycType *type) {
return scr_cyc_alloc_inline(size, type);
void *obj = scr_cyc_alloc_inline(size, type);
if (type->trace == scr_cyc_no_children) scr_cyc_hdr(obj)->buf = SCR_CYC_ACYCLIC;
return obj;
}
void scr_cyc_no_children(void *obj, ScrTraceVisit visit, void *ctx) {
(void)obj;
(void)visit;
(void)ctx;
}
void scr_cyc_free(void *obj) { scr_cyc_free_inline(obj); }
@@ -1175,7 +1183,13 @@ void scr_cyc_on_release(void *obj) {
ScrCycHdr *h = scr_cyc_hdr(obj);
/* Releasing an existing candidate cannot advance the trigger. A pass
* always re-arms after its teardowns, including any candidates they add. */
if (scr_cyc_is_buffered(h)) return;
if (scr_cyc_release_done(h)) return;
/* An object without cycle-capable children whose SCR_CYC_ACYCLIC a pass
* or the young-in nursery replaced gets it back. */
if (scr_cyc_type(h)->trace == scr_cyc_no_children) {
h->buf = SCR_CYC_ACYCLIC;
return;
}
#if SCR_CYC_INLINE_BUFFERING
if (scr_cyc_append(h, obj)) return;
#endif
@@ -1346,14 +1360,17 @@ static void scr_scan_black(void *obj, uintptr_t depth) {
ScrCycHdr *h = scr_cyc_hdr(obj);
scr_cyc_set_color(h, SCR_CYC_BLACK);
/* Drop markGray's component number: a survivor leaves the pass unbuffered
* unless it is a candidate that a limited pass did not take. */
if (!scr_cyc_is_buffered(h)) scr_cyc_clear_buffered(h);
* unless it is a candidate that a limited pass did not take, and one
* without cycle-capable children leaves it SCR_CYC_ACYCLIC again. */
const ScrCycType *type = scr_cyc_type(h);
bool acyclic = type->trace == scr_cyc_no_children;
if (!scr_cyc_is_buffered(h)) h->buf = acyclic ? SCR_CYC_ACYCLIC : 0;
if (scr_cyc_gen(h) < SCR_CYC_OLD) scr_cyc_push_work(&scr_promote, obj);
if (acyclic) return;
if (depth == SCR_CYC_WALK_DEPTH) {
scr_cyc_push_work(&scr_restore_pending, obj);
return;
}
const ScrCycType *type = scr_cyc_type(h);
SCR_CYC_CHILDREN(obj, type, scr_sb_array, scr_sb_visit, depth + 1);
}
static void scr_restore_from(void *root) {
@@ -1881,7 +1898,8 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit, bool doomed) {
void *obj = scr_cands.v[i];
ScrCycHdr *h = scr_cyc_hdr(obj);
if (scr_cyc_color(h) == SCR_CYC_DOOMED) continue; /* being freed below */
if (!scr_cyc_is_buffered(h)) {
/* A candidate without cycle-capable children was never on a cycle. */
if (!scr_cyc_is_buffered(h) && h->buf != SCR_CYC_ACYCLIC) {
/* A survivor promoted into a level its type bypasses moves past
* it, as a release would buffer it. */
if (scr_cyc_gen(h) == SCR_CYC_MATURE && scr_cyc_bypassed(scr_cyc_type(h), SCR_CYC_MATURE))
+2 -1
View File
@@ -14065,7 +14065,8 @@ static int scr_weak_key(const ScrDyn *key, void **ptr, unsigned *kind) {
if (key->v.handle.tag != SCR_DYNH_ARRAY_BUFFER && key->v.handle.tag != SCR_DYNH_SHARED_ARRAY_BUFFER && key->v.handle.tag != SCR_DYNH_WEAK_MAP &&
key->v.handle.tag != SCR_DYNH_WEAK_SET && key->v.handle.tag != SCR_DYNH_STDIO &&
key->v.handle.tag != SCR_DYNH_FETCH_REQUEST && key->v.handle.tag != SCR_DYNH_FETCH_RESPONSE &&
key->v.handle.tag != SCR_DYNH_MAP && key->v.handle.tag != SCR_DYNH_SET) return -1;
key->v.handle.tag != SCR_DYNH_MAP && key->v.handle.tag != SCR_DYNH_SET &&
key->v.handle.tag != SCR_DYNH_NET_SOCKET && key->v.handle.tag != SCR_DYNH_NET_SERVER) return -1;
*ptr = key->v.handle.ptr; *kind = 4 + key->v.handle.tag; return 1;
case SCR_DYN_TYPED_REF:
if (!key->v.typed_ref.observed) return -1;
+2
View File
@@ -678,6 +678,7 @@ void scr_net_server_release(ScrNetServer *s) {
#ifdef SCR_RC_AUDIT
scr_net_live--;
#endif
scr_weak_dispose_inline(s); /* weak collections keyed by this socket or server */
free(s);
}
}
@@ -724,6 +725,7 @@ void scr_net_sock_release(ScrNetSocket *s) {
#ifdef SCR_RC_AUDIT
scr_net_live--;
#endif
scr_weak_dispose_inline(s); /* weak collections keyed by this socket or server */
free(s);
}
}
+27 -5
View File
@@ -141,6 +141,11 @@ typedef struct ScrCycType {
* Emitted allocation paths read it and take the slow path while it is 0. */
uint32_t id;
} ScrCycType;
/* The trace of a type that holds nothing cycle-capable: it visits nothing.
* Emitted classes whose fields hold no cycle-capable value but whose
* hierarchy is traced name it (llvm/classes.ts); their objects are
* allocated with `buf` SCR_CYC_ACYCLIC. */
void scr_cyc_no_children(void *obj, ScrTraceVisit visit, void *ctx);
/* Runtime families' kinds. Emitted descriptors count from
* SCR_CYC_KIND_EMITTED (llvm/alloc.ts FIRST_EMITTED_CYC_KIND). */
enum {
@@ -169,7 +174,7 @@ enum {
* collector color (SCR_CYC_*) and generation (SCR_CYC_NURSERY..SCR_CYC_OLD,
* the walk filter), and `buf`: SCR_CYC_BUFFERED_BIT while the object sits in
* its generation's candidate buffer, with its position there below it (O(1)
* removal when rc hits 0). */
* removal when rc hits 0), or SCR_CYC_ACYCLIC. */
typedef struct ScrCycHdr {
uint16_t id;
uint8_t color;
@@ -177,13 +182,25 @@ typedef struct ScrCycHdr {
uint32_t buf;
} ScrCycHdr;
#define SCR_CYC_BUFFERED_BIT 0x80000000u
/* `buf` of an object whose type's trace is scr_cyc_no_children: it has no
* cycle-capable children, so it is never on a cycle and never a candidate.
* Such a type still carries the header when it shares a hierarchy with
* cycle-capable classes (headers are uniform per hierarchy). The value has
* the buffered bit set, so a release that leaves the object alive returns
* at the buffered test and makes no call; scr_cyc_is_buffered is false for
* it. No buffer position reaches it (scr_cyc_grow caps the buffers well
* below 2^31 entries). */
#define SCR_CYC_ACYCLIC 0xffffffffu
/* This layout is an ABI, not an implementation detail. The emitted
* allocation stores the id and zeroes the rest; every emitted release of a
* allocation stores the id and zeroes the rest (`buf` SCR_CYC_ACYCLIC for a
* class whose trace is scr_cyc_no_children); every emitted release of a
* headered object (llvm/shapes.ts cycleRootLines, used by the record, class
* and inline RC helpers) mirrors scr_cyc_on_release's early return by
* testing the sign of `buf` as an i32 at obj-4, and the emitted inline free
* reads the generation as an i8 at obj-5. Hence the assertions. */
* testing the sign of `buf` as an i32 at obj-4, the emitted death mirrors
* scr_cyc_on_dead's by testing `buf` below -1 (cycleDeadLines), and the
* emitted inline free reads the generation as an i8 at obj-5. Hence the
* assertions. */
_Static_assert(sizeof(ScrCycHdr) == 8, "LLVM backend expects an 8-byte cycle header");
_Static_assert(offsetof(ScrCycHdr, id) == 0 && sizeof(((ScrCycHdr *)0)->id) == 2,
"the emitted allocation stores the registry id as an i16 at the block start");
@@ -215,6 +232,11 @@ static inline void scr_cyc_set_color(ScrCycHdr *h, unsigned color) { h->color =
static inline unsigned scr_cyc_gen(const ScrCycHdr *h) { return h->gen; }
static inline void scr_cyc_set_gen(ScrCycHdr *h, unsigned gen) { h->gen = (uint8_t)gen; }
static inline bool scr_cyc_is_buffered(const ScrCycHdr *h) {
return (h->buf & SCR_CYC_BUFFERED_BIT) != 0 && h->buf != SCR_CYC_ACYCLIC;
}
/* A release that leaves the object alive has nothing to buffer: it is a
* candidate already, or it is never one (SCR_CYC_ACYCLIC). */
static inline bool scr_cyc_release_done(const ScrCycHdr *h) {
return (h->buf & SCR_CYC_BUFFERED_BIT) != 0;
}
static inline uint32_t scr_cyc_buf_index(const ScrCycHdr *h) {
@@ -608,7 +630,7 @@ static inline bool scr_cyc_append(ScrCycHdr *h, void *obj) {
* Most releases of long-lived containers take the first early return. */
static inline void scr_cyc_released(void *obj) {
ScrCycHdr *h = scr_cyc_hdr(obj);
if (scr_cyc_is_buffered(h)) return;
if (scr_cyc_release_done(h)) return;
#if SCR_CYC_INLINE_BUFFERING
if (scr_cyc_append(h, obj)) return;
#endif
+62 -2
View File
@@ -398,6 +398,8 @@ static MockRec *mock_cyc_new(int value) {
static ScrArrOps mock_ops = { &mock_retain, &mock_release, NULL };
static ScrArrOps mock_cyc_ops = { &mock_cyc_retain, &mock_cyc_release, &mock_trace };
/* First used after test_ops_overflow fills the registry: its arrays keep the ops in their ext. */
static ScrArrOps mock_cyc_ops_late = { &mock_cyc_retain, &mock_cyc_release, &mock_trace };
static void test_ref_elements(void) {
#ifdef SCR_RC_AUDIT
@@ -514,6 +516,38 @@ static void test_ref_cycle(void) {
#endif
}
static void test_ref_known_count_cycle(ScrArrOps *ops) {
#ifdef SCR_RC_AUDIT
long arrays0 = scr_arr_live_count();
#endif
/* A known-count construction of cycle-capable elements keeps its slots
* after the collector header: the collector traces them, and both the
* collected array and one released by its count free the one block. */
ScrArr *arr = scr_arr_new_ops(ops, 2);
bool in_block = arr->data == (uint64_t *)((char *)arr + ((sizeof(ScrArr) + 7) & ~(size_t)7));
/* Ops past the registry (once test_ops_overflow has filled it) live in
* the ext, which moves the slots to their own allocation. */
bool overflow = (arr->spare >> SCR_ARR_OPS_SHIFT) == SCR_ARR_OPS_OVERFLOW;
check(arr->cap >= 2 && in_block == !overflow && scr_arr_ops(arr) == ops,
"known-count cycle-capable construction is inline unless its ops need the ext");
MockRec *first = mock_cyc_new(1);
first->owner = (ScrArr *)scr_arr_retain(arr);
scr_arr_push_ref(arr, first);
scr_arr_push_ref(arr, mock_cyc_new(2));
scr_arr_release(arr);
scr_collect_cycles();
check(mock_live == 0, "cycle through an inline array collected");
ScrArr *plain = scr_arr_new_ops(ops, 3);
for (int i = 0; i < 4; i++) scr_arr_push_ref(plain, mock_cyc_new(i));
check(scr_arr_len(plain) == 4, "a cycle-capable inline array grows out of its block");
scr_arr_release(plain);
scr_collect_cycles();
check(mock_live == 0, "released inline cycle-capable array frees its elements");
#ifdef SCR_RC_AUDIT
check(scr_arr_live_count() == arrays0, "known-count cycle: no arrays leaked");
#endif
}
static void test_ref_truncate_cycle(void) {
#ifdef SCR_RC_AUDIT
long arrays0 = scr_arr_live_count();
@@ -1543,9 +1577,33 @@ static void test_inline_slots(void) {
long strings0 = scr_str_live_count();
long arrays0 = scr_arr_live_count();
#endif
ScrArr *plain = scr_arr_new(SCR_ELEM_F64, 4);
check(!inline_slots(plain), "plain constructions keep separate storage");
ScrArr *plain = scr_arr_new(SCR_ELEM_F64, 0);
check(!inline_slots(plain) && plain->data == NULL, "empty plain constructions allocate no slots");
scr_arr_release(plain);
/* A construction with a known count up to 16 slots keeps them in its own
* block; growth past them moves them out like any inline array. */
ScrArr *known = scr_arr_new(SCR_ELEM_F64, 4);
check(inline_slots(known) && known->cap >= 4 && known->len == 0 && !scr_arr_has(known, 0) &&
scr_arr_states_end(known) == (uint8_t *)(known->data + known->cap) + known->cap,
"a known-count construction places its slots in the array block");
size_t known_cap = known->cap;
for (size_t i = 0; i <= known_cap; i++) scr_arr_push_f64(known, (double)i);
check(!inline_slots(known) && scr_arr_len(known) == known_cap + 1 && scr_arr_get_f64(known, 0) == 0 &&
scr_arr_get_f64(known, (double)known_cap) == (double)known_cap,
"growth past a known-count construction moves the slots out");
scr_arr_release(known);
ScrArr *sixteen = scr_arr_new(SCR_ELEM_STR, 16);
for (int i = 0; i < 16; i++) scr_arr_push_ref(sixteen, scr_str_new("s", 1));
check(inline_slots(sixteen) && scr_arr_len(sixteen) == 16, "sixteen known slots stay inline");
scr_arr_release(sixteen);
ScrArr *large = scr_arr_new(SCR_ELEM_F64, 17);
check(!inline_slots(large) && large->cap >= 17, "larger known counts keep separate storage");
scr_arr_release(large);
ScrArr *known_flags = scr_arr_new(SCR_ELEM_BOOL, 5);
scr_arr_push_bool(known_flags, true);
check(inline_slots(known_flags) && known_flags->cap >= 5 && scr_arr_get_bool(known_flags, 0),
"known-count boolean constructions are inline too");
scr_arr_release(known_flags);
ScrArr *a = scr_arr_new_small(SCR_ELEM_F64);
check(inline_slots(a) && a->cap >= 4 && a->len == 0 &&
scr_arr_states_end(a) == (uint8_t *)(a->data + a->cap) + a->cap,
@@ -2205,7 +2263,9 @@ int main(int argc, char **argv) {
test_nested_rc();
test_index_of_includes();
test_ref_elements();
test_ref_known_count_cycle(&mock_cyc_ops);
test_ops_overflow();
test_ref_known_count_cycle(&mock_cyc_ops_late);
test_ext_through_storage_changes();
test_ref_cycle();
test_ref_truncate_cycle();
+33
View File
@@ -1434,6 +1434,38 @@ static void check_death_brings_full_pass(void) {
free(keep);
}
/* An object whose type's trace is scr_cyc_no_children starts out
* SCR_CYC_ACYCLIC and is never buffered. A pass that reaches it from a dead
* cycle frees it with the cycle when the cycle held its last reference, and
* otherwise leaves it SCR_CYC_ACYCLIC with its outside count. */
static void check_acyclic_objects(void) {
static ScrCycType leaf_type = { &scr_cyc_no_children, &node_free, 0 };
size_t before = freed;
Node *leaf = scr_cyc_alloc(sizeof(*leaf), &leaf_type);
ScrCycHdr *h = scr_cyc_hdr(leaf);
check(h->buf == SCR_CYC_ACYCLIC && !scr_cyc_is_buffered(h), "acyclic object did not start acyclic");
leaf->rc = 2; /* an outside owner and a temporary reference */
release_live(leaf);
check(h->buf == SCR_CYC_ACYCLIC, "release buffered an acyclic object");
Node *first = make_ring(2);
first->other = leaf;
leaf->rc++;
release_live(first);
scr_cyc_pass_at(SCR_CYC_NURSERY);
check(freed == before + 2, "nursery pass did not free the ring");
check(leaf->rc == 1 && h->buf == SCR_CYC_ACYCLIC, "acyclic survivor lost its count or its mark");
Node *second = make_ring(2);
second->next->other = leaf;
leaf->rc++;
release_live(leaf); /* the outside owner lets go: only the ring holds it */
check(h->buf == SCR_CYC_ACYCLIC, "release buffered an acyclic object");
release_live(second);
scr_collect_cycles();
check(freed == before + 5, "acyclic object held only by a dead ring was not freed with it");
}
int main(void) {
check_inline_buffering();
check_old_garbage_reclaimed_while_growing();
@@ -1467,6 +1499,7 @@ int main(void) {
check_limited_pass();
check_large_old_buffer_with_nursery_candidates();
check_unjudged_backlog_follows_growth();
check_acyclic_objects();
printf("cycle collection checks passed: threshold=%zu growth_cap=%zu\n",
configured_nursery_threshold(), configured_growth_cap());
return 0;
@@ -0,0 +1,81 @@
// Sockets and servers key WeakMaps and WeakSets by identity: the socket a
// connect callback sees, the one a 'connection' listener sees, and the
// same socket read back out of a diagnostics_channel message are one key.
// Entries of closed sockets stop answering once the socket is gone.
import * as net from "node:net";
import type { Server, Socket } from "node:net";
import { channel } from "node:diagnostics_channel";
interface Conn {
label: string;
bytes: number;
}
const connections = new WeakMap<Socket, Conn>();
const seen = new WeakSet<Socket>();
const servers = new WeakMap<Server, string>();
const opened = channel("example:socket:opened");
opened.subscribe((message) => {
const { socket, label } = message as { socket: Socket; label: string };
console.log(`[diag] ${label} known ${connections.has(socket)}, seen ${seen.has(socket)}`);
const conn = connections.get(socket);
if (conn !== undefined) conn.label = `${conn.label}+diag`;
});
// Each dial waits for both ends to close, so the two sides log in order.
let serverClosed: () => void = () => {};
const server = net.createServer((socket) => {
seen.add(socket);
connections.set(socket, { label: "server side", bytes: 0 });
socket.on("data", (chunk) => {
const conn = connections.get(socket);
if (conn !== undefined) conn.bytes += chunk.length;
socket.end(`echo ${chunk.toString()}`);
});
socket.on("close", () => {
const conn = connections.get(socket);
console.log("server socket closed", conn?.label, conn?.bytes);
console.log("delete", connections.delete(socket), connections.delete(socket));
serverClosed();
});
});
servers.set(server, "echo server");
function dial(port: number, label: string): Promise<void> {
const bothClosed = new Promise<void>((resolve) => {
serverClosed = resolve;
});
const clientClosed = new Promise<void>((resolve) => {
const client: Socket = net.connect(port, "127.0.0.1", () => {
connections.set(client, { label, bytes: 0 });
console.log(label, "connected; has", connections.has(client), "seen", seen.has(client));
opened.publish({ socket: client, label });
client.write(label);
});
let reply = "";
client.on("data", (chunk) => {
reply += chunk.toString();
});
client.on("close", () => {
console.log(label, "reply", JSON.stringify(reply), "entry", connections.get(client)?.label);
resolve();
});
});
return Promise.all([clientClosed, bothClosed]).then(() => undefined);
}
server.listen(0, "127.0.0.1", async () => {
console.log("server known", servers.has(server), servers.get(server));
const address = server.address();
const port = typeof address === "object" && address !== null ? address.port : 0;
await dial(port, "first");
await dial(port, "second");
try {
connections.set(1 as unknown as Socket, { label: "number", bytes: 0 });
} catch (err) {
console.log((err as Error).name, (err as Error).message);
}
server.close(() => console.log("closed; server still known", servers.has(server)));
});
@@ -0,0 +1,65 @@
// Never-suspending async functions in a program that enters no
// AsyncLocalStorage store and reads no stacks: awaited from stackless and
// fiber bodies, called without an await, nested, throwing, and rejecting
// without a handler, against concurrent microtasks.
const log: string[] = [];
async function leaf(n: number): Promise<number> {
log.push(`leaf ${n}`);
return n + 1;
}
async function nestedLeaf(n: number): Promise<number> {
const inner = leaf(n * 10);
void inner.then((v) => log.push(`nestedLeaf inner ${v}`));
log.push(`nestedLeaf ${n}`);
return n;
}
async function thrower(n: number): Promise<number> {
log.push(`thrower ${n}`);
if (n > 0) throw new TypeError(`bad ${n}`);
return n;
}
function ticker(name: string, turns: number): Promise<void> {
let p = Promise.resolve();
for (let i = 0; i < turns; i++) p = p.then(() => void log.push(`${name} tick ${i}`));
return p;
}
async function stackless(): Promise<void> {
const a = await leaf(1);
const b = await leaf(a);
try {
await thrower(b);
} catch (e) {
log.push(`stackless caught ${(e as Error).name} ${(e as Error).message}`);
}
log.push(`stackless ${a + b}`);
}
async function* source(): AsyncGenerator<number, void, undefined> {
yield 1;
yield 2;
}
async function fibered(): Promise<void> {
let s = 0;
for await (const v of source()) s += await leaf(v);
try {
s += await thrower(5);
} catch (e) {
log.push(`fibered caught ${(e as Error).message}`);
}
log.push(`fibered ${s}`);
}
async function spawned(): Promise<void> {
const p = leaf(7);
const q = thrower(0);
log.push("spawned before await");
log.push(`spawned ${(await p) + (await q)}`);
}
process.on("unhandledRejection", (reason) => console.log(`unhandled ${(reason as Error).message}`));
async function main(): Promise<void> {
void thrower(9);
await Promise.all([ticker("t", 6), stackless(), fibered(), spawned(), nestedLeaf(3)]);
for (const line of log) console.log(line);
}
void main();
@@ -0,0 +1,117 @@
import { publish } from "@scriptc/threads";
// Classes whose own fields hold nothing cycle-capable, in a hierarchy where
// other classes form cycles: their instances are held by dead cycles, by
// live cycles and by plain locals while collections run, and are published
// with the contexts that hold them.
abstract class Mapper {
abstract map(n: number): number;
}
class Offset extends Mapper {
by: number;
constructor(by: number) {
super();
this.by = by;
}
map(n: number): number {
return n + this.by;
}
}
class Scale extends Offset {
readonly factor: number;
constructor(by: number, factor: number) {
super(by);
this.factor = factor;
}
map(n: number): number {
return super.map(n) * this.factor;
}
}
class Context {
readonly steps: Mapper[] = [];
mapper: Mapper | undefined = undefined;
total = 0;
}
class ContextMapper extends Mapper {
readonly context: Context;
constructor(context: Context) {
super();
this.context = context;
}
map(n: number): number {
let value = n;
for (const step of this.context.steps) value = step.map(value);
return value;
}
}
class Chain extends Mapper {
readonly first: Mapper;
next: Mapper | undefined = undefined;
constructor(first: Mapper) {
super();
this.first = first;
}
map(n: number): number {
const value = this.first.map(n);
return this.next === undefined ? value : this.next.map(value);
}
}
function makeContext(i: number, shared: Mapper): Context {
const context = new Context();
context.steps.push(new Offset(i % 7), shared, new Scale(1, 2));
context.mapper = new ContextMapper(context);
context.total = context.mapper.map(i);
return context;
}
const shared = new Scale(3, 1);
const kept: Context[] = [];
let sum = 0;
for (let i = 0; i < 20000; i++) {
const context = makeContext(i, shared);
sum += context.total;
if (i % 1000 === 0) kept.push(context);
}
// A chain that loops back to itself, holding mappers that are also held
// outside it.
const outside: Mapper[] = [];
let loops = 0;
for (let i = 0; i < 5000; i++) {
const offset = new Offset(i);
const head = new Chain(offset);
const tail = new Chain(new Scale(i, 3));
head.next = tail;
tail.next = undefined;
loops += head.map(1);
tail.next = head;
if (i % 500 === 0) outside.push(offset);
}
let keptTotal = 0;
for (const context of kept) keptTotal += context.mapper!.map(1);
let outsideTotal = 0;
for (const mapper of outside) outsideTotal += mapper.map(2);
console.log(sum, loops, kept.length, keptTotal, outside.length, outsideTotal, shared.map(4));
publish(kept);
let publishedTotal = 0;
for (const context of kept) publishedTotal += context.mapper!.map(3);
console.log("published", publishedTotal);
try {
shared.by = 5;
console.log("write: ok");
} catch (e) {
console.log(`write: ${(e as Error).name}: ${(e as Error).message}`);
}
const fresh = makeContext(7, new Offset(1));
fresh.steps.push(shared);
console.log("after", fresh.mapper!.map(2), outside[3]!.map(1));
+34 -1
View File
@@ -81,6 +81,34 @@ channel("undici:request:trailers").subscribe((message) => {
if (t !== undefined) console.log(` [diag] ${t.label} done after ${t.dispatched} dispatch`);
});
// A transport's connection registry: sockets the wrapped connector dialed,
// keyed weakly, and the client diagnostics messages that carry them.
const dialed = new WeakMap<Socket, number>();
const clientDiag = { beforeConnect: 0, connector: 0, connected: 0, sendHeaders: 0, known: 0, unknown: 0 };
const noteSocket = (socket: Socket): void => {
const n = dialed.get(socket);
if (n === undefined) clientDiag.unknown++;
else clientDiag.known++;
};
channel("undici:client:beforeConnect").subscribe((message) => {
const { connector } = message as { connector: unknown };
clientDiag.beforeConnect++;
if (typeof connector === "function") clientDiag.connector++;
});
channel("undici:client:connected").subscribe((message) => {
const { socket, connectParams } = message as {
socket: Socket;
connectParams: { protocol: string; hostname: string };
};
clientDiag.connected++;
if (connectParams.protocol === "http:" && connectParams.hostname === "127.0.0.1") noteSocket(socket);
});
channel("undici:client:sendHeaders").subscribe((message) => {
const { socket, headers } = message as { socket: Socket; headers: string };
clientDiag.sendHeaders++;
if (headers.length > 0) noteSocket(socket);
});
/** A transport-style Agent: one Pool per origin built by the factory, a
* wrapped connector, and a Client interceptor that marks dispatches. */
class TracingAgent extends Agent {
@@ -102,7 +130,9 @@ class TracingAgent extends Agent {
pipelining: 1,
connect(options, callback) {
opened++;
return connect(options, callback);
const socket = connect(options, callback);
dialed.set(socket, opened);
return socket;
},
interceptors: {
Client: [
@@ -198,6 +228,9 @@ server.listen(0, "127.0.0.1", async () => {
await r.body.text();
}
console.log("connections opened:", agent.connections);
console.log(
`client diagnostics: ${clientDiag.beforeConnect} before connect (${clientDiag.connector} with a connector), ${clientDiag.connected} connected, ${clientDiag.sendHeaders} send headers; ${clientDiag.known} carry a dialed socket, ${clientDiag.unknown} another`,
);
console.log("stats:", agent.statsFor(origin));
console.log("== Pool with a connection limit");
+34 -1
View File
@@ -115,6 +115,34 @@ channel("undici:request:trailers").subscribe((message) => {
if (t !== undefined) console.log(` [diag] ${t.label} done after ${t.dispatched} dispatch`);
});
// A transport's connection registry: sockets the wrapped connector dialed,
// keyed weakly, and the client diagnostics messages that carry them.
const dialed = new WeakMap<Socket, number>();
const clientDiag = { beforeConnect: 0, connector: 0, connected: 0, sendHeaders: 0, known: 0, unknown: 0 };
const noteSocket = (socket: Socket): void => {
const n = dialed.get(socket);
if (n === undefined) clientDiag.unknown++;
else clientDiag.known++;
};
channel("undici:client:beforeConnect").subscribe((message) => {
const { connector } = message as { connector: unknown };
clientDiag.beforeConnect++;
if (typeof connector === "function") clientDiag.connector++;
});
channel("undici:client:connected").subscribe((message) => {
const { socket, connectParams } = message as {
socket: Socket;
connectParams: { protocol: string; hostname: string };
};
clientDiag.connected++;
if (connectParams.protocol === "http:" && connectParams.hostname === "127.0.0.1") noteSocket(socket);
});
channel("undici:client:sendHeaders").subscribe((message) => {
const { socket, headers } = message as { socket: Socket; headers: string };
clientDiag.sendHeaders++;
if (headers.length > 0) noteSocket(socket);
});
/** A transport-style Agent: one Pool per origin built by the factory, a
* wrapped connector, and a Client interceptor that marks dispatches. */
class TracingAgent extends Agent {
@@ -136,7 +164,9 @@ class TracingAgent extends Agent {
pipelining: 1,
connect(options, callback) {
opened++;
return connect(options, callback);
const socket = connect(options, callback);
dialed.set(socket, opened);
return socket;
},
interceptors: {
Client: [
@@ -232,6 +262,9 @@ server.listen(0, "127.0.0.1", async () => {
await r.body.text();
}
console.log("connections opened:", agent.connections);
console.log(
`client diagnostics: ${clientDiag.beforeConnect} before connect (${clientDiag.connector} with a connector), ${clientDiag.connected} connected, ${clientDiag.sendHeaders} send headers; ${clientDiag.known} carry a dialed socket, ${clientDiag.unknown} another`,
);
console.log("stats:", agent.statsFor(origin));
console.log("== Pool with a connection limit");
@@ -1,8 +1,8 @@
dynamic tier (tier assignment, report-only; --tier=auto)
4 functions already operate on dynamic values (1 statement)
readName tests/coverage-fixtures/tier-demotions.ts:15:1: %dyn.error.stringConstructor at tests/coverage-fixtures/tier-demotions.ts:17:10
%dyn.error.stringConstructor.0 tests/coverage-fixtures/tier-demotions.ts:17:10: dynCall at tests/coverage-fixtures/tier-demotions.ts:17:10
%dyn.class.property.0 tests/coverage-fixtures/tier-demotions.ts:17:17: %dyn.class.readPropertiesOrEmpty at tests/coverage-fixtures/tier-demotions.ts:17:17
%dyn.error.stringConstructor.0 tests/coverage-fixtures/tier-demotions.ts:17:10: dynCall at tests/coverage-fixtures/tier-demotions.ts:17:10
%method.value:%%Error.toString tests/coverage-fixtures/tier-demotions.ts:17:10: dyn.this at tests/coverage-fixtures/tier-demotions.ts:17:10
2 demotion requests (refusals and fences the dynamic tier would replace)
@@ -0,0 +1 @@
{ "plain": 3400, "san": 4500 }
@@ -0,0 +1 @@
{ "plain": 3350, "san": 3910 }