diff --git a/packages/compiler/shims/undici/core/client.ts b/packages/compiler/shims/undici/core/client.ts index f391e6d27..ddc1ae22e 100644 --- a/packages/compiler/shims/undici/core/client.ts +++ b/packages/compiler/shims/undici/core/client.ts @@ -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`); diff --git a/packages/compiler/src/backend/llvm/alloc.ts b/packages/compiler/src/backend/llvm/alloc.ts index c28a63e62..aa6345dab 100644 --- a/packages/compiler/src/backend/llvm/alloc.ts +++ b/packages/compiler/src/backend/llvm/alloc.ts @@ -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`, diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index 5daaf4300..94da472d4 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -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" }; diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index 03ee8caa1..7ca2216d2 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -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, ); diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 0d5533f85..7bbd3d453 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -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()`] : []), diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 4504ae58e..0bdbe23b2 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -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`, `}`, ``, diff --git a/packages/compiler/src/backend/llvm/publish.test.ts b/packages/compiler/src/backend/llvm/publish.test.ts index 5a84caa96..c8008fdff 100644 --- a/packages/compiler/src/backend/llvm/publish.test.ts +++ b/packages/compiler/src/backend/llvm/publish.test.ts @@ -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, diff --git a/packages/compiler/src/backend/llvm/publish.ts b/packages/compiler/src/backend/llvm/publish.ts index ebe9b1a64..bb7e0c8a8 100644 --- a/packages/compiler/src/backend/llvm/publish.ts +++ b/packages/compiler/src/backend/llvm/publish.ts @@ -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)`, diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 58295e42b..6de92f667 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -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)`, diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index a41751f01..c90e1135b 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -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, 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, 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); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 99dea40a6..02d26e652 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -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([...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(); - 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(); + const usedBy = new Map>(); + 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; - 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(); + 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; + 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 = [ diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 731a5cabd..a7362c821 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -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); } } diff --git a/packages/compiler/test/await-call-emission.test.ts b/packages/compiler/test/await-call-emission.test.ts index b3867c928..b206b829b 100644 --- a/packages/compiler/test/await-call-emission.test.ts +++ b/packages/compiler/test/await-call-emission.test.ts @@ -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(); +async function leaf(x: number): Promise { + 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 { + 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 { + await null; + return x; +} +async function coroutine(x: number): Promise { + return (await slow(x)) + (await leaf(x)); +} +async function* tick(): AsyncGenerator { + yield 0; +} +async function fibered(x: number): Promise { + for await (const t of tick()) x += t; + return await leaf(x); +} +async function main(): Promise { + 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)", + ); + } + } }); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6620-weak-keys-net-sockets.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6620-weak-keys-net-sockets.ts.json new file mode 100644 index 000000000..f5ef58606 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6620-weak-keys-net-sockets.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6620-weak-keys-net-sockets.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7103-fiberless-plain-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7103-fiberless-plain-calls.ts.json new file mode 100644 index 000000000..80fe9b85f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7103-fiberless-plain-calls.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7103-fiberless-plain-calls.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/acyclic-classes-in-traced-hierarchies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/acyclic-classes-in-traced-hierarchies.ts.json new file mode 100644 index 000000000..8daa576cb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/acyclic-classes-in-traced-hierarchies.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/acyclic-classes-in-traced-hierarchies.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index c64b85947..c00d1b4f6 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -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 diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index 591f544b8..5e5029e14 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -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)) diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 54411bd54..b8f16630b 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -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; diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index ab7da1ebd..f091daede 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -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); } } diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 9cb88e6e8..892531965 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -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 diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c index 02da2fac2..d943acfb2 100644 --- a/packages/runtime/test/test_array.c +++ b/packages/runtime/test/test_array.c @@ -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(); diff --git a/packages/runtime/test/test_cycle.c b/packages/runtime/test/test_cycle.c index 723e9519f..4e00b852e 100644 --- a/packages/runtime/test/test_cycle.c +++ b/packages/runtime/test/test_cycle.c @@ -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; diff --git a/tests/corpus/6620-weak-keys-net-sockets.ts b/tests/corpus/6620-weak-keys-net-sockets.ts new file mode 100644 index 000000000..65878887e --- /dev/null +++ b/tests/corpus/6620-weak-keys-net-sockets.ts @@ -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(); +const seen = new WeakSet(); +const servers = new WeakMap(); +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 { + const bothClosed = new Promise((resolve) => { + serverClosed = resolve; + }); + const clientClosed = new Promise((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))); +}); diff --git a/tests/corpus/7103-fiberless-plain-calls.ts b/tests/corpus/7103-fiberless-plain-calls.ts new file mode 100644 index 000000000..68b96e984 --- /dev/null +++ b/tests/corpus/7103-fiberless-plain-calls.ts @@ -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 { + log.push(`leaf ${n}`); + return n + 1; +} +async function nestedLeaf(n: number): Promise { + 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 { + log.push(`thrower ${n}`); + if (n > 0) throw new TypeError(`bad ${n}`); + return n; +} +function ticker(name: string, turns: number): Promise { + 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 { + 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 { + yield 1; + yield 2; +} +async function fibered(): Promise { + 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 { + 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 thrower(9); + await Promise.all([ticker("t", 6), stackless(), fibered(), spawned(), nestedLeaf(3)]); + for (const line of log) console.log(line); +} +void main(); diff --git a/tests/corpus/acyclic-classes-in-traced-hierarchies.ts b/tests/corpus/acyclic-classes-in-traced-hierarchies.ts new file mode 100644 index 000000000..d25def1da --- /dev/null +++ b/tests/corpus/acyclic-classes-in-traced-hierarchies.ts @@ -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)); diff --git a/tests/fixtures/npm/cases/undici5/main.ts b/tests/fixtures/npm/cases/undici5/main.ts index b51b16c65..ae39bacf9 100644 --- a/tests/fixtures/npm/cases/undici5/main.ts +++ b/tests/fixtures/npm/cases/undici5/main.ts @@ -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(); +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"); diff --git a/tests/fixtures/npm/cases/undici7/main.ts b/tests/fixtures/npm/cases/undici7/main.ts index f39e3031d..28cfd8353 100644 --- a/tests/fixtures/npm/cases/undici7/main.ts +++ b/tests/fixtures/npm/cases/undici7/main.ts @@ -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(); +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"); diff --git a/tests/harness/__snapshots__/coverage-explain-dynamic.txt b/tests/harness/__snapshots__/coverage-explain-dynamic.txt index f168988e6..6e561634d 100644 --- a/tests/harness/__snapshots__/coverage-explain-dynamic.txt +++ b/tests/harness/__snapshots__/coverage-explain-dynamic.txt @@ -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) diff --git a/tests/harness/corpus-costs/7103-fiberless-plain-calls.ts.json b/tests/harness/corpus-costs/7103-fiberless-plain-calls.ts.json new file mode 100644 index 000000000..93470e6d4 --- /dev/null +++ b/tests/harness/corpus-costs/7103-fiberless-plain-calls.ts.json @@ -0,0 +1 @@ +{ "plain": 3400, "san": 4500 } diff --git a/tests/harness/corpus-costs/acyclic-classes-in-traced-hierarchies.ts.json b/tests/harness/corpus-costs/acyclic-classes-in-traced-hierarchies.ts.json new file mode 100644 index 000000000..bf904aec4 --- /dev/null +++ b/tests/harness/corpus-costs/acyclic-classes-in-traced-hierarchies.ts.json @@ -0,0 +1 @@ +{ "plain": 3350, "san": 3910 }