mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-04 08:04:37 +08:00
Shrink the cycle-collector object header to 8 bytes (#969)
This commit is contained in:
@@ -70,7 +70,7 @@ Without `-o`, artifacts are written to `.scriptc/` next to the input. Different
|
||||
|
||||
### External linking
|
||||
|
||||
Program objects define `main` and leave selected `scr_*` runtime functions undefined. A strong reference to `scr_runtime_abi_v19` checks runtime compatibility at link time.
|
||||
Program objects define `main` and leave selected `scr_*` runtime functions undefined. A strong reference to `scr_runtime_abi_v20` checks runtime compatibility at link time.
|
||||
|
||||
`--print=native-link-info` emits the program object and a JSON recipe containing the runtime pack and link order for external builds. This object ABI is experimental and requires an exact runtime version. See [Native Program Objects](/docs/native-objects) for examples.
|
||||
|
||||
|
||||
@@ -15,4 +15,4 @@ $ ./app-ld
|
||||
|
||||
`cc` uses the C compiler as a linker driver. `ld` links the same precompiled runtime objects directly with the selected Apple SDK. The object defines `main`; it is a complete program object, not a library to load into another process. Use `scriptc build --lib --profile ...` for a host-callable static library.
|
||||
|
||||
The external object ABI is experimental. Always consume the runtime pack at the exact `runtime_pack.version` reported by the same scriptc installation. The object requires `scr_runtime_abi_v19`, so a mismatched runtime fails during the link instead of starting with an incompatible ABI.
|
||||
The external object ABI is experimental. Always consume the runtime pack at the exact `runtime_pack.version` reported by the same scriptc installation. The object requires `scr_runtime_abi_v20`, so a mismatched runtime fails during the link instead of starting with an incompatible ABI.
|
||||
|
||||
@@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
minimum_os: "14.0",
|
||||
},
|
||||
program: { object, entry_symbol: "main" },
|
||||
runtime_abi: { version: 19, marker: "scr_runtime_abi_v19" },
|
||||
runtime_abi: { version: 20, marker: "scr_runtime_abi_v20" },
|
||||
runtime_pack: {
|
||||
kind: "precompiled",
|
||||
package: "@scriptc/runtime-darwin-arm64",
|
||||
@@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => {
|
||||
() => null,
|
||||
(failure: { stderr?: string }) => failure,
|
||||
);
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v19");
|
||||
expect(error?.stderr).toContain("scr_runtime_abi_v20");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,7 +85,17 @@ describe("emitted object allocation", () => {
|
||||
|
||||
const cyc = emitObjectAlloc(h, "40", { type: "@ty" });
|
||||
expectSingleDefinitions(cyc);
|
||||
expect(cyc).toContain(" %sa.n = add i64 40, 16");
|
||||
expect(cyc).toContain(" %sa.n = add i64 40, 8");
|
||||
// The 8-byte header: the descriptor's i16 registry id, then zero color,
|
||||
// generation and buffer state; an unregistered descriptor (id 0) takes
|
||||
// the slow path.
|
||||
expect(cyc).toContain(
|
||||
" %sa.tidp = getelementptr inbounds { ptr, ptr, i32, i32 }, ptr @ty, i32 0, i32 3",
|
||||
);
|
||||
expect(cyc).toContain(" %sa.tid = load atomic i32, ptr %sa.tidp acquire, align 4");
|
||||
expect(cyc).toContain(" %sa.small = and i1 %sa.reg, %sa.fits0");
|
||||
expect(cyc).toContain(" store i16 %sa.id, ptr %sa.blk ; ScrCycHdr.id");
|
||||
expect(cyc).toContain(" store i32 0, ptr %sa.bufp ; ScrCycHdr.buf");
|
||||
expect(cyc).toContain(" %sa.s = call ptr @scr_cyc_alloc(i64 40, ptr @ty)");
|
||||
expect(cyc).toContain(" store i64 %sa.live1, ptr @scr_cyc_live");
|
||||
expect(h.decls).toContain(
|
||||
@@ -104,6 +114,12 @@ describe("emitted object allocation", () => {
|
||||
expect(sys).toContain(` call void @scr_rt_free(ptr ${blk})`);
|
||||
expect(lines).toContain(" call void %sf.hook(ptr %o)");
|
||||
expect(lines.some((l) => l.includes("@scr_cyc_live"))).toBe(cyc);
|
||||
if (cyc) {
|
||||
// The generation is the i8 at obj-5.
|
||||
expect(lines).toContain(" %sf.blk = getelementptr inbounds i8, ptr %o, i64 -8");
|
||||
expect(lines).toContain(" %sf.genp = getelementptr inbounds i8, ptr %sf.blk, i64 3");
|
||||
expect(lines).toContain(" %sf.old = icmp eq i8 %sf.gen, 2");
|
||||
}
|
||||
expect(lines.at(-1)).toBe("sf.done:");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -117,11 +117,16 @@ const SA_BUMP = 280;
|
||||
const SA_LIM = 536;
|
||||
const SA_STEP_SHIFT = 4; // 16-byte classes
|
||||
const SA_MAX = 512;
|
||||
/** sizeof(ScrCycHdr); on 64-bit targets the type descriptor is its first
|
||||
* word, and every other header word starts zero. */
|
||||
const CYC_HDR = 16;
|
||||
/** ScrCycHdr.gen, an i8 at this header offset (scr_runtime.h asserts it). */
|
||||
const CYC_GEN = 9;
|
||||
/** sizeof(ScrCycHdr): the descriptor's i16 registry id, i8 color and
|
||||
* generation, and an i32 `buf` whose sign bit marks a buffered candidate
|
||||
* (scr_runtime.h asserts the offsets). */
|
||||
export const CYC_HDR = 8;
|
||||
/** ScrCycHdr.gen, an i8 at this header offset. */
|
||||
const CYC_GEN = 3;
|
||||
/** ScrCycHdr.buf, an i32 at this header offset. */
|
||||
export const CYC_BUF = 4;
|
||||
/** The emitted descriptor `{ trace, free_fn, kind, id }` (ScrCycType). */
|
||||
const CYC_TYPE = "{ ptr, ptr, i32, i32 }";
|
||||
|
||||
function declareState(host: AllocHost): void {
|
||||
if (host.threadLocalAlloc !== true) host.declare(`@scr_sa = external global ${SA_TYPE}`);
|
||||
@@ -144,12 +149,13 @@ export interface CycHeader {
|
||||
export const FIRST_EMITTED_CYC_KIND = 32;
|
||||
|
||||
/** The definition of a shape's ScrCycType descriptor `{ trace, free_fn,
|
||||
* kind }`, referenced by every header of that shape. `kind` indexes the
|
||||
* collector's per-type statistics (scr_runtime.h); emitted shapes number
|
||||
* themselves from FIRST_EMITTED_CYC_KIND (ShapeHost.nextCycKind) and 0
|
||||
* keeps none. */
|
||||
* kind, id }`, named by every header of that shape through its registry
|
||||
* id. `kind` indexes the collector's per-type statistics (scr_runtime.h);
|
||||
* emitted shapes number themselves from FIRST_EMITTED_CYC_KIND
|
||||
* (ShapeHost.nextCycKind) and 0 keeps none. The runtime assigns `id` on the
|
||||
* first allocation, so the descriptor is writable. */
|
||||
export function cycTypeDescriptor(name: string, trace: string, free: string, kind = 0): string {
|
||||
return `${name} = internal constant { ptr, ptr, i32 } { ptr ${trace}, ptr ${free}, i32 ${kind} }`;
|
||||
return `${name} = internal global ${CYC_TYPE} { ptr ${trace}, ptr ${free}, i32 ${kind}, i32 0 }`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,11 +209,21 @@ function allocLines(
|
||||
}
|
||||
declareState(host);
|
||||
// Bytes of the block that the fast path itself stores after the clear:
|
||||
// the header's descriptor word, or the object's leading words.
|
||||
const keep = cyc ? 8 : stored;
|
||||
// the header, or the object's leading words.
|
||||
const keep = cyc ? CYC_HDR : stored;
|
||||
// A type's first object registers its descriptor on the slow path.
|
||||
const registered = cyc
|
||||
? [
|
||||
` %sa.tidp = getelementptr inbounds ${CYC_TYPE}, ptr ${cyc.type}, i32 0, i32 3`,
|
||||
` %sa.tid = load atomic i32, ptr %sa.tidp acquire, align 4`,
|
||||
` %sa.reg = icmp ne i32 %sa.tid, 0`,
|
||||
` %sa.fits0 = icmp ule i64 %sa.n, ${SA_MAX}`,
|
||||
` %sa.small = and i1 %sa.reg, %sa.fits0`,
|
||||
]
|
||||
: [` %sa.small = icmp ule i64 %sa.n, ${SA_MAX}`];
|
||||
const lines = [
|
||||
` %sa.n = add i64 ${size}, ${cyc ? CYC_HDR : 0}`,
|
||||
` %sa.small = icmp ule i64 %sa.n, ${SA_MAX}`,
|
||||
...registered,
|
||||
` br i1 %sa.small, label %sa.class, label %sa.slow`,
|
||||
`sa.class:`,
|
||||
` %sa.nm1 = sub i64 %sa.n, 1`,
|
||||
@@ -253,7 +269,12 @@ function allocLines(
|
||||
lines.push(
|
||||
`sa.fast:`,
|
||||
` %sa.blk = phi ptr [ %sa.b, %sa.pop ], [ %sa.p, %sa.carve ]`,
|
||||
` store ptr ${cyc.type}, ptr %sa.blk ; ScrCycHdr.type`,
|
||||
` %sa.id = trunc i32 %sa.tid to i16`,
|
||||
` store i16 %sa.id, ptr %sa.blk ; ScrCycHdr.id`,
|
||||
` %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`,
|
||||
` %sa.live = load i64, ptr @scr_cyc_live`,
|
||||
` %sa.live1 = add i64 %sa.live, 1`,
|
||||
` store i64 %sa.live1, ptr @scr_cyc_live`,
|
||||
@@ -339,7 +360,7 @@ function freeLines(host: AllocHost, cyc: boolean): string[] {
|
||||
` store i64 %sf.live1, ptr @scr_cyc_live`,
|
||||
` %sf.blk = getelementptr inbounds i8, ptr %o, i64 -${CYC_HDR}`,
|
||||
` %sf.genp = getelementptr inbounds i8, ptr %sf.blk, i64 ${CYC_GEN}`,
|
||||
` %sf.gen = load i8, ptr %sf.genp`,
|
||||
` %sf.gen = load i8, ptr %sf.genp ; ScrCycHdr.gen`,
|
||||
` %sf.old = icmp eq i8 %sf.gen, 2`,
|
||||
` br i1 %sf.old, label %sf.oldcount, label %sf.accounted`,
|
||||
`sf.oldcount:`,
|
||||
|
||||
@@ -79,7 +79,7 @@ test("releases split into an inlinable fast path and an out-of-line remainder",
|
||||
// takes the possible-root step without loading the vtable.
|
||||
const fast = body(mangleClassRelease("Node"));
|
||||
expect(fast).toContain("%shared = icmp ult i64 %rcm2, -3");
|
||||
expect(fast).toContain("load i16, ptr %bufp");
|
||||
expect(fast).toContain("load i32, ptr %bufp");
|
||||
expect(fast).toContain(`call void @${mangleClassRelease("Node")}_slow(ptr %o)`);
|
||||
expect(fast).not.toContain("%ScrVt");
|
||||
// Only the last reference dispatches, from a noinline remainder.
|
||||
@@ -136,7 +136,7 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts",
|
||||
// The mortal Owner keeps its header, counts and possible-root step.
|
||||
expect(body(mangleClassNew("Owner"))).toContain("store i64 1, ptr %o");
|
||||
expect(body(mangleClassNew("Owner"))).toContain("scr_cyc_alloc");
|
||||
expect(body(mangleClassRelease("Owner"))).toContain("load i16, ptr %bufp");
|
||||
expect(body(mangleClassRelease("Owner"))).toContain("load i32, ptr %bufp");
|
||||
});
|
||||
|
||||
test("published ProgramLifetime instances use the published mark every other object uses", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { emitLlvmLayouts } from "./layouts.js";
|
||||
import { compileRegexMatcher, type RegexMatcherCode } from "./regex-matchers.js";
|
||||
import { FIRST_EMITTED_CYC_KIND, inlineAllocSupported } from "./alloc.js";
|
||||
import { CYC_BUF, CYC_HDR, FIRST_EMITTED_CYC_KIND, inlineAllocSupported } from "./alloc.js";
|
||||
import { hasLoneSurrogate, llvmBytes as llBytes, wtf8Bytes } from "../literals.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -550,7 +550,7 @@ export class LlEmitter {
|
||||
private debugScope: string | null = null;
|
||||
readonly sizeType: "i32" | "i64";
|
||||
readonly ffiExtendNarrowIntegers: boolean;
|
||||
readonly cycleBufferedOffset: number;
|
||||
readonly cycleBufOffset: number;
|
||||
/** Emitted ScrCycType descriptors so far (their collector statistics
|
||||
* indices; see ShapeHost.nextCycKind). */
|
||||
private cycKinds = 0;
|
||||
@@ -976,10 +976,8 @@ export class LlEmitter {
|
||||
this.runtimeAbiMarker = options.runtimeAbiMarker === true;
|
||||
this.detachedExit = options.detachedExit === true;
|
||||
this.rcHelpers = options.inlineRc === true ? new Set() : null;
|
||||
// ScrCycHdr.buffered sits 6 bytes behind every object. The wasm32
|
||||
// header pads before its descriptor so its payload remains
|
||||
// double-aligned without changing this ABI offset.
|
||||
this.cycleBufferedOffset = 6;
|
||||
// ScrCycHdr.buf sits 4 bytes behind every object on every target.
|
||||
this.cycleBufOffset = CYC_HDR - CYC_BUF;
|
||||
this.ffiCallbackAdapters = allocateFfiCallbackAdapters(mod.ffiImports ?? []);
|
||||
this.ffiHasRetainedCallback = hasRetainedFfiCallback(mod.ffiImports ?? []);
|
||||
this.ffiHasForeignCallback = hasForeignFfiCallback(mod.ffiImports ?? []);
|
||||
@@ -1097,7 +1095,7 @@ export class LlEmitter {
|
||||
this.tracedUnions = traced.unions;
|
||||
this.shapeHost = {
|
||||
sizeType: this.sizeType,
|
||||
cycleBufferedOffset: this.cycleBufferedOffset,
|
||||
cycleBufOffset: this.cycleBufOffset,
|
||||
tracedShapes: this.tracedShapes,
|
||||
tracedUnions: this.tracedUnions,
|
||||
nullableUnions: this.nullableUnions,
|
||||
|
||||
@@ -23,7 +23,7 @@ function declarationHost(
|
||||
},
|
||||
needOom() {},
|
||||
sizeType: pointerBits === 32 ? "i32" : "i64",
|
||||
cycleBufferedOffset: 6,
|
||||
cycleBufOffset: 4,
|
||||
tracedShapes: new Set(),
|
||||
tracedUnions: new Set(),
|
||||
nullableUnions: new NullableUnions({
|
||||
@@ -148,10 +148,11 @@ describe("LLVM runtime RC symbols", () => {
|
||||
expect(fn("sc_rc_release_map")).toContain("%ScrMapRc, ptr %o, i32 0, i32 8");
|
||||
expect(fn("sc_rc_release_map")).toContain("br i1 %headered, label %slow, label %dec");
|
||||
expect(fn("sc_rc_release_map")).not.toContain("store i32 1");
|
||||
// Unions: no color store, only the i16 `buffered` read at obj-6.
|
||||
// Unions: no color store, only the sign of the i32 `buf` at obj-4.
|
||||
expect(fn("sc_rc_release_union")).not.toContain("store i32");
|
||||
expect(fn("sc_rc_release_union")).toContain("getelementptr i8, ptr %o, i64 -6");
|
||||
expect(fn("sc_rc_release_union")).toContain("load i16, ptr %bufp");
|
||||
expect(fn("sc_rc_release_union")).toContain("getelementptr i8, ptr %o, i64 -4");
|
||||
expect(fn("sc_rc_release_union")).toContain("load i32, ptr %bufp");
|
||||
expect(fn("sc_rc_release_union")).toContain("icmp slt i32 %buf, 0");
|
||||
expect(fn("sc_rc_release_union")).toContain("call void @scr_cyc_on_release(ptr %o)");
|
||||
// Promises tolerate NULL and always carry a header.
|
||||
expect(fn("sc_rc_retain_promise")).toContain("icmp eq ptr %o, null");
|
||||
@@ -163,7 +164,7 @@ describe("LLVM runtime RC symbols", () => {
|
||||
releaseSym(host, { kind: "union", unionId: "u0" } as never);
|
||||
const ir = emitInlineRcHelpers(host).join("\n");
|
||||
expect(ir).toContain("load i32, ptr %o");
|
||||
expect(ir).toContain("getelementptr i8, ptr %o, i32 -6");
|
||||
expect(ir).toContain("getelementptr i8, ptr %o, i32 -4");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -182,7 +183,7 @@ describe("emitted object releases", () => {
|
||||
const start = defs.indexOf("root:");
|
||||
const root = defs.slice(start, defs.indexOf("done:", start));
|
||||
expect(root).not.toContain("store i32");
|
||||
expect(root).toContain("getelementptr i8, ptr %o, i64 -6");
|
||||
expect(root).toContain("getelementptr i8, ptr %o, i64 -4");
|
||||
expect(root).toContain("br i1 %queued, label %done, label %enqueue");
|
||||
expect(root.indexOf("enqueue:")).toBeLessThan(
|
||||
root.indexOf("call void @scr_cyc_on_release(ptr %o)"),
|
||||
|
||||
@@ -39,8 +39,8 @@ export interface ShapeHost {
|
||||
needOom(): void;
|
||||
/** The target C ABI's `size_t` / refcount integer type. */
|
||||
readonly sizeType: "i32" | "i64";
|
||||
/** Byte offset from an object pointer back to ScrCycHdr.buffered. */
|
||||
readonly cycleBufferedOffset: number;
|
||||
/** Byte offset from an object pointer back to ScrCycHdr.buf. */
|
||||
readonly cycleBufOffset: number;
|
||||
readonly tracedShapes: Set<string>;
|
||||
readonly tracedUnions: Set<string>;
|
||||
/** Unions represented as a nullable arm pointer (nullable-unions.ts):
|
||||
@@ -866,9 +866,9 @@ export function directChildReleaseLines(
|
||||
if (target.traced) {
|
||||
host.declare(`declare void @scr_cyc_on_dead(ptr)`);
|
||||
lines.push(
|
||||
` %${tag}.bufp = getelementptr i8, ptr ${value}, ${S} -${host.cycleBufferedOffset}`,
|
||||
` %${tag}.buf = load i16, ptr %${tag}.bufp`,
|
||||
` %${tag}.queued = icmp ne i16 %${tag}.buf, 0`,
|
||||
` %${tag}.bufp = getelementptr i8, ptr ${value}, ${S} -${host.cycleBufOffset}`,
|
||||
` %${tag}.buf = load i32, ptr %${tag}.bufp`,
|
||||
` %${tag}.queued = icmp slt i32 %${tag}.buf, 0`,
|
||||
` br i1 %${tag}.queued, label %${tag}.unbuffer, label %${tag}.teardown`,
|
||||
`${tag}.unbuffer:`,
|
||||
` call void @scr_cyc_on_dead(ptr ${value})`,
|
||||
@@ -1052,9 +1052,9 @@ export function releaseFastPath(
|
||||
}
|
||||
|
||||
/** The possible-cycle-root step of a release that left a headered object
|
||||
* alive: scr_cyc_on_release with its early return inlined. Read `buffered`
|
||||
* (the i16 at obj-6; nonzero for a queued candidate or a
|
||||
* tenured object) and call the runtime only to enqueue a new candidate.
|
||||
* 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 call the runtime only to enqueue a new candidate.
|
||||
* Every emitted release of a headered object shares this sequence
|
||||
* (scr_runtime.h asserts the offset); it ends by branching to `done`. */
|
||||
/** One traced field of an emitted trace function: load the slot and visit
|
||||
@@ -1084,9 +1084,9 @@ export function cycleRootLines(host: ShapeHost, done: string): string[] {
|
||||
const S = host.sizeType;
|
||||
host.declare(`declare void @scr_cyc_on_release(ptr)`);
|
||||
return [
|
||||
` %bufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufferedOffset}`,
|
||||
` %buf = load i16, ptr %bufp`,
|
||||
` %queued = icmp ne i16 %buf, 0`,
|
||||
` %bufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufOffset}`,
|
||||
` %buf = load i32, ptr %bufp`,
|
||||
` %queued = icmp slt i32 %buf, 0`,
|
||||
` br i1 %queued, label %${done}, label %enqueue`,
|
||||
`enqueue:`,
|
||||
` call void @scr_cyc_on_release(ptr %o) ; buffer the candidate; may collect`,
|
||||
@@ -1102,9 +1102,9 @@ 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.cycleBufferedOffset}`,
|
||||
` %dbuf = load i16, ptr %dbufp`,
|
||||
` %dqueued = icmp ne i16 %dbuf, 0`,
|
||||
` %dbufp = getelementptr i8, ptr %o, ${S} -${host.cycleBufOffset}`,
|
||||
` %dbuf = load i32, ptr %dbufp`,
|
||||
` %dqueued = icmp slt i32 %dbuf, 0`,
|
||||
` br i1 %dqueued, label %unbuffer, label %${next}`,
|
||||
`unbuffer:`,
|
||||
` call void @scr_cyc_on_dead(ptr %o)`,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* of the link contract: a program object keeps this symbol undefined and the
|
||||
* matching runtime defines it, so incompatible manual links fail before the
|
||||
* program can start. */
|
||||
export const RUNTIME_ABI_VERSION = 19 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v19" as const;
|
||||
export const RUNTIME_ABI_VERSION = 20 as const;
|
||||
export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v20" as const;
|
||||
|
||||
/** Object consumption is useful today, but the complete scr_* surface may
|
||||
* still change before 1.0. The versioned marker prevents accidental mixing;
|
||||
|
||||
@@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 19, marker: "scr_runtime_abi_v19" },
|
||||
runtime_abi: { version: 20, marker: "scr_runtime_abi_v20" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
|
||||
@@ -116,7 +116,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 19, marker: "scr_runtime_abi_v19" },
|
||||
runtime_abi: { version: 20, marker: "scr_runtime_abi_v20" },
|
||||
compiler: {
|
||||
command: "clang",
|
||||
identity: "fixture clang",
|
||||
|
||||
@@ -312,7 +312,7 @@ ${[0, 1, 2, 3, 4, 5].map((index) => ` call void @add${index}(i64 ${index + 2})`
|
||||
expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"]));
|
||||
expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"]));
|
||||
expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]);
|
||||
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v19");
|
||||
expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v20");
|
||||
|
||||
const clangExe = join(dir, "clang-program");
|
||||
const linkDriver = join(dir, "clang-driver.c");
|
||||
@@ -646,7 +646,7 @@ ${[0, 1, 2, 3, 4, 5].map((index) => ` call void @add${index}(i64 ${index + 2})`
|
||||
.split("\n");
|
||||
expect(undefinedSymbols).toContain("_sf_scale");
|
||||
expect(undefinedSymbols).toContain("_sf_callback_mix");
|
||||
expect(undefinedSymbols).toContain("_scr_runtime_abi_v19");
|
||||
expect(undefinedSymbols).toContain("_scr_runtime_abi_v20");
|
||||
});
|
||||
|
||||
test("helper and clang object paths retain the same outbound FFI C ABI", async () => {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5155-cycle-header-types.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -306,7 +306,7 @@ async function build() {
|
||||
package: packageManifest.name,
|
||||
version: packageManifest.version,
|
||||
target: RUNTIME_PACK_MATRIX.target,
|
||||
runtime_abi: { version: 19, marker: "scr_runtime_abi_v19" },
|
||||
runtime_abi: { version: 20, marker: "scr_runtime_abi_v20" },
|
||||
compiler: {
|
||||
command: compiler,
|
||||
identity: compilerVersion,
|
||||
|
||||
@@ -402,7 +402,7 @@ async function build() {
|
||||
package: packageManifest.name,
|
||||
version: packageManifest.version,
|
||||
target: matrix.target,
|
||||
runtime_abi: { version: 19, marker: "scr_runtime_abi_v19" },
|
||||
runtime_abi: { version: 20, marker: "scr_runtime_abi_v20" },
|
||||
compiler: {
|
||||
command: compiler,
|
||||
identity: compilerVersion,
|
||||
|
||||
@@ -887,7 +887,7 @@ static ScrArr *scr_arr_make_ops(const ScrArrOps *ops, size_t initial_cap, bool s
|
||||
small ? scr_arr_small_cap(SCR_ARR_INLINE_OFFSET + (cyc ? sizeof(ScrCycHdr) : 0), false) : 0;
|
||||
size_t size = inline_cap ? SCR_ARR_INLINE_OFFSET + inline_cap * SCR_ARR_SLOT_BYTES : sizeof(ScrArr);
|
||||
if (cyc) {
|
||||
static const ScrCycType cyc_type = { &scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY };
|
||||
static ScrCycType cyc_type = { &scr_arr_trace_v, &scr_arr_gc_free, SCR_CYC_KIND_ARRAY };
|
||||
a = scr_cyc_alloc_inline(size, &cyc_type);
|
||||
} else {
|
||||
a = scr_mem_alloc(size);
|
||||
|
||||
@@ -318,7 +318,7 @@ static void scr_promise_gcfree(void *o) {
|
||||
}
|
||||
|
||||
ScrPromise *scr_promise_new(void) {
|
||||
static const ScrCycType cyc_type = { &scr_promise_trace, &scr_promise_gcfree, SCR_CYC_KIND_PROMISE };
|
||||
static ScrCycType cyc_type = { &scr_promise_trace, &scr_promise_gcfree, SCR_CYC_KIND_PROMISE };
|
||||
ScrPromise *p = scr_cyc_alloc(sizeof *p, &cyc_type);
|
||||
p->rc = 1;
|
||||
#ifdef SCR_RC_AUDIT
|
||||
|
||||
@@ -70,7 +70,7 @@ static void scr_box_gcfree(void *o) {
|
||||
}
|
||||
|
||||
ScrBox *scr_box_new(ScrBoxKind kind) {
|
||||
static const ScrCycType cyc_type = { &scr_box_trace, &scr_box_gcfree, SCR_CYC_KIND_BOX };
|
||||
static ScrCycType cyc_type = { &scr_box_trace, &scr_box_gcfree, SCR_CYC_KIND_BOX };
|
||||
ScrBox *b = scr_cyc_alloc_inline(sizeof(ScrBox), &cyc_type);
|
||||
b->rc = 1;
|
||||
b->kind = kind;
|
||||
@@ -186,7 +186,7 @@ static void scr_closure_gcfree(void *o) {
|
||||
}
|
||||
|
||||
ScrClosure *scr_closure_new(void *fn, size_t ncaps) {
|
||||
static const ScrCycType cyc_type = { &scr_closure_trace, &scr_closure_gcfree, SCR_CYC_KIND_CLOSURE };
|
||||
static ScrCycType cyc_type = { &scr_closure_trace, &scr_closure_gcfree, SCR_CYC_KIND_CLOSURE };
|
||||
ScrClosure *c = scr_cyc_alloc_inline(sizeof(ScrClosure) + ncaps * sizeof(ScrBox *), &cyc_type);
|
||||
c->rc = 1;
|
||||
c->fn = fn;
|
||||
|
||||
@@ -126,7 +126,7 @@ static void scr_enable_utf8_console_output(void) {
|
||||
}
|
||||
#endif
|
||||
|
||||
void scr_runtime_abi_v19(void) {}
|
||||
void scr_runtime_abi_v20(void) {}
|
||||
|
||||
void scr_init(void) {
|
||||
#ifdef SCR_WORKERS
|
||||
|
||||
@@ -266,15 +266,37 @@ SCR_TL size_t scr_cyc_live = 0;
|
||||
SCR_TL size_t scr_cyc_old_freed = 0;
|
||||
#endif
|
||||
|
||||
void *scr_cyc_alloc(size_t size, const ScrCycType *type) {
|
||||
/* Descriptor copies by registry id. Id 0 stays empty (an unregistered type). */
|
||||
ScrCycType scr_cyc_types[SCR_CYC_MAX_TYPES];
|
||||
static uint32_t scr_cyc_ntypes = 1;
|
||||
|
||||
/* Assign `type` its id on its first allocation; any thread may get there
|
||||
* first, and every thread then sees the same id. A lost race leaves one
|
||||
* table slot unused. */
|
||||
uint32_t scr_cyc_register(ScrCycType *type) {
|
||||
uint32_t id = __atomic_load_n(&type->id, __ATOMIC_ACQUIRE);
|
||||
if (id) return id;
|
||||
uint32_t fresh = __atomic_fetch_add(&scr_cyc_ntypes, 1, __ATOMIC_RELAXED);
|
||||
if (fresh >= SCR_CYC_MAX_TYPES) scr_trap("scriptc: too many cycle-collected types\n");
|
||||
scr_cyc_types[fresh] = *type;
|
||||
scr_cyc_types[fresh].id = fresh;
|
||||
__atomic_thread_fence(__ATOMIC_RELEASE);
|
||||
uint32_t expected = 0;
|
||||
if (__atomic_compare_exchange_n(&type->id, &expected, fresh, false, __ATOMIC_ACQ_REL,
|
||||
__ATOMIC_ACQUIRE))
|
||||
return fresh;
|
||||
return expected;
|
||||
}
|
||||
|
||||
void *scr_cyc_alloc(size_t size, ScrCycType *type) {
|
||||
return scr_cyc_alloc_inline(size, type);
|
||||
}
|
||||
|
||||
void scr_cyc_free(void *obj) { scr_cyc_free_inline(obj); }
|
||||
|
||||
static void scr_cyc_grow(ScrVec *vec) {
|
||||
/* Candidate positions are 32-bit (ScrCycHdr.buf_index). */
|
||||
if (vec->cap > (size_t)UINT32_MAX / 2) scr_cyc_oom();
|
||||
/* Candidate positions are 31-bit (ScrCycHdr.buf). */
|
||||
if (vec->cap > (size_t)(SCR_CYC_BUFFERED_BIT - 1) / 2) scr_cyc_oom();
|
||||
vec->cap = vec->cap ? vec->cap * 2 : 64;
|
||||
void **grown = realloc(vec->v, vec->cap * sizeof *grown);
|
||||
if (!grown) scr_cyc_oom();
|
||||
@@ -723,20 +745,20 @@ static inline void scr_cyc_freed(const ScrCycType *type, unsigned gen) {
|
||||
|
||||
void scr_cyc_on_dead(void *obj) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
if (!h->buffered) return;
|
||||
if (!scr_cyc_is_buffered(h)) return;
|
||||
/* O(1) removal: swap the last entry into the hole. A buffered object's
|
||||
* generation never changes (promotion runs only on objects the pass has
|
||||
* already drained from the buffers), so `gen` names the right one. */
|
||||
ScrVec *b = &scr_roots[h->gen];
|
||||
size_t i = h->buf_index;
|
||||
ScrVec *b = &scr_roots[scr_cyc_gen(h)];
|
||||
size_t i = scr_cyc_buf_index(h);
|
||||
void *last = b->v[--b->n];
|
||||
b->v[i] = last;
|
||||
if (last != obj) scr_cyc_hdr(last)->buf_index = (uint32_t)i;
|
||||
h->buffered = 0;
|
||||
if (last != obj) scr_cyc_set_buffered(scr_cyc_hdr(last), (uint32_t)i);
|
||||
scr_cyc_clear_buffered(h);
|
||||
scr_cyc_nbuffered--;
|
||||
/* A death must not consume room reserved for a future candidate. */
|
||||
scr_cyc_trigger--;
|
||||
if (h->gen != SCR_CYC_NURSERY && !scr_cyc_older_backlog())
|
||||
if (scr_cyc_gen(h) != SCR_CYC_NURSERY && !scr_cyc_older_backlog())
|
||||
scr_cyc_scheduled_mature_age = 0;
|
||||
}
|
||||
|
||||
@@ -747,12 +769,11 @@ 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 (h->buffered) return;
|
||||
if (h->gen < SCR_CYC_OLD) h->gen = (uint8_t)scr_cyc_buffer_gen(h->type, h->gen);
|
||||
ScrVec *b = &scr_roots[h->gen];
|
||||
if (scr_cyc_is_buffered(h)) return;
|
||||
if (scr_cyc_gen(h) < SCR_CYC_OLD) scr_cyc_set_gen(h, scr_cyc_buffer_gen(scr_cyc_type(h), scr_cyc_gen(h)));
|
||||
ScrVec *b = &scr_roots[scr_cyc_gen(h)];
|
||||
if (b->n == b->cap) scr_cyc_grow(b);
|
||||
h->buffered = 1;
|
||||
h->buf_index = (uint32_t)b->n;
|
||||
scr_cyc_set_buffered(h, (uint32_t)b->n);
|
||||
b->v[b->n++] = obj;
|
||||
scr_cyc_nbuffered++;
|
||||
/* Never re-entered: a teardown's releases of untraced children can buffer
|
||||
@@ -770,7 +791,7 @@ void scr_cyc_on_release(void *obj) {
|
||||
* a generation this pass does not collect. */
|
||||
#define SCR_CYC_SKIP(child) \
|
||||
((child) == NULL || SCR_RC(child) == SIZE_MAX \
|
||||
|| scr_cyc_hdr(child)->gen > scr_gen_limit)
|
||||
|| scr_cyc_gen(scr_cyc_hdr(child)) > scr_gen_limit)
|
||||
|
||||
/* Trace callbacks forward an integer depth through their opaque context;
|
||||
* it is never dereferenced. The ABI already carries it through every edge. */
|
||||
@@ -782,31 +803,31 @@ static void scr_mg_visit(void *child, void *ctx) {
|
||||
}
|
||||
static void scr_mark_gray(void *obj, uintptr_t depth) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
if (h->color == SCR_CYC_GRAY) return;
|
||||
h->color = SCR_CYC_GRAY;
|
||||
if (scr_cyc_color(h) == SCR_CYC_GRAY) return;
|
||||
scr_cyc_set_color(h, SCR_CYC_GRAY);
|
||||
if (depth == SCR_CYC_WALK_DEPTH) {
|
||||
scr_cyc_push(&scr_pending, obj);
|
||||
return;
|
||||
}
|
||||
h->type->trace(obj, scr_mg_visit, (void *)(depth + 1));
|
||||
scr_cyc_type(h)->trace(obj, scr_mg_visit, (void *)(depth + 1));
|
||||
}
|
||||
|
||||
static void scr_scan_black(void *obj, uintptr_t depth);
|
||||
static void scr_sb_visit(void *child, void *ctx) {
|
||||
if (SCR_CYC_SKIP(child)) return;
|
||||
SCR_RC(child) += 1;
|
||||
if (scr_cyc_hdr(child)->color != SCR_CYC_BLACK)
|
||||
if (scr_cyc_color(scr_cyc_hdr(child)) != SCR_CYC_BLACK)
|
||||
scr_scan_black(child, (uintptr_t)ctx);
|
||||
}
|
||||
static void scr_scan_black(void *obj, uintptr_t depth) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
h->color = SCR_CYC_BLACK;
|
||||
if (h->gen < SCR_CYC_OLD) scr_cyc_push(&scr_promote, obj);
|
||||
scr_cyc_set_color(h, SCR_CYC_BLACK);
|
||||
if (scr_cyc_gen(h) < SCR_CYC_OLD) scr_cyc_push(&scr_promote, obj);
|
||||
if (depth == SCR_CYC_WALK_DEPTH) {
|
||||
scr_cyc_push(&scr_restore_pending, obj);
|
||||
return;
|
||||
}
|
||||
h->type->trace(obj, scr_sb_visit, (void *)(depth + 1));
|
||||
scr_cyc_type(h)->trace(obj, scr_sb_visit, (void *)(depth + 1));
|
||||
}
|
||||
|
||||
static void scr_scan(void *obj, uintptr_t depth);
|
||||
@@ -816,16 +837,16 @@ static void scr_scan_visit(void *child, void *ctx) {
|
||||
}
|
||||
static void scr_scan(void *obj, uintptr_t depth) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
if (h->color != SCR_CYC_GRAY) return;
|
||||
if (scr_cyc_color(h) != SCR_CYC_GRAY) return;
|
||||
if (SCR_RC(obj) > 0) {
|
||||
scr_scan_black(obj, 0);
|
||||
while (scr_restore_pending.n) {
|
||||
void *next = scr_restore_pending.v[--scr_restore_pending.n];
|
||||
scr_cyc_hdr(next)->type->trace(next, scr_sb_visit, NULL);
|
||||
scr_cyc_type(scr_cyc_hdr(next))->trace(next, scr_sb_visit, NULL);
|
||||
}
|
||||
return;
|
||||
}
|
||||
h->color = SCR_CYC_WHITE;
|
||||
scr_cyc_set_color(h, SCR_CYC_WHITE);
|
||||
/* Record each whitened object once. A later outside root can restore it;
|
||||
* filter the list only after every scan finishes. No gather trace needed. */
|
||||
scr_cyc_push(&scr_white, obj);
|
||||
@@ -833,7 +854,7 @@ static void scr_scan(void *obj, uintptr_t depth) {
|
||||
scr_cyc_push(&scr_pending, obj);
|
||||
return;
|
||||
}
|
||||
h->type->trace(obj, scr_scan_visit, (void *)(depth + 1));
|
||||
scr_cyc_type(h)->trace(obj, scr_scan_visit, (void *)(depth + 1));
|
||||
}
|
||||
|
||||
/* ── cross-generation edges ───────────────────────────────────────────── */
|
||||
@@ -870,7 +891,7 @@ static void scr_scan(void *obj, uintptr_t depth) {
|
||||
static SCR_CYC_COMPACT void scr_xg_pin_visit(void *child, void *ctx) {
|
||||
(void)ctx;
|
||||
if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
|
||||
if (scr_cyc_hdr(child)->gen <= scr_gen_limit) return; /* markGray had it */
|
||||
if (scr_cyc_gen(scr_cyc_hdr(child)) <= scr_gen_limit) return; /* markGray had it */
|
||||
SCR_RC(child) += 1; /* pin across the teardowns */
|
||||
scr_cyc_push(&scr_xgen, child);
|
||||
}
|
||||
@@ -890,8 +911,8 @@ static void scr_xg_child_visit(void *child, void *ctx) {
|
||||
/* One genuine release, generically: the header carries everything needed. */
|
||||
static void scr_xg_destroy(void *obj) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
h->type->trace(obj, scr_xg_child_visit, NULL);
|
||||
h->type->free_fn(obj);
|
||||
scr_cyc_type(h)->trace(obj, scr_xg_child_visit, NULL);
|
||||
scr_cyc_type(h)->free_fn(obj);
|
||||
scr_xg_freed++;
|
||||
}
|
||||
|
||||
@@ -919,10 +940,10 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) {
|
||||
for (size_t i = 0; i < scr_white.n; i++) {
|
||||
void *obj = scr_white.v[i];
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
if (h->color != SCR_CYC_WHITE) continue;
|
||||
h->color = SCR_CYC_DOOMED;
|
||||
if (gen_limit < SCR_CYC_OLD) scr_cyc_freed(h->type, gen_limit);
|
||||
else scr_cyc_mature_dead += h->gen == SCR_CYC_MATURE;
|
||||
if (scr_cyc_color(h) != SCR_CYC_WHITE) continue;
|
||||
scr_cyc_set_color(h, SCR_CYC_DOOMED);
|
||||
if (gen_limit < SCR_CYC_OLD) scr_cyc_freed(scr_cyc_type(h), gen_limit);
|
||||
else scr_cyc_mature_dead += scr_cyc_gen(h) == SCR_CYC_MATURE;
|
||||
scr_white.v[dead++] = obj;
|
||||
}
|
||||
scr_white.n = dead;
|
||||
@@ -934,7 +955,7 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) {
|
||||
if (gen_limit < SCR_CYC_OLD) {
|
||||
for (size_t i = 0; i < scr_white.n; i++) {
|
||||
void *obj = scr_white.v[i];
|
||||
scr_cyc_hdr(obj)->type->trace(obj, scr_xg_pin_visit, NULL);
|
||||
scr_cyc_type(scr_cyc_hdr(obj))->trace(obj, scr_xg_pin_visit, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -943,7 +964,7 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) {
|
||||
* referenced, so none of them is in the white set about to be freed. */
|
||||
for (size_t i = 0; i < scr_promote.n; i++) {
|
||||
ScrCycHdr *h = scr_cyc_hdr(scr_promote.v[i]);
|
||||
if (h->gen < SCR_CYC_OLD) h->gen++;
|
||||
if (scr_cyc_gen(h) < SCR_CYC_OLD) scr_cyc_set_gen(h, scr_cyc_gen(h) + 1);
|
||||
}
|
||||
scr_promote.n = 0;
|
||||
|
||||
@@ -966,15 +987,14 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) {
|
||||
for (size_t i = 0; i < scr_cands.n; i++) {
|
||||
void *obj = scr_cands.v[i];
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
if (h->color == SCR_CYC_DOOMED) continue; /* being freed below */
|
||||
if (!h->buffered) {
|
||||
if (scr_cyc_color(h) == SCR_CYC_DOOMED) continue; /* being freed below */
|
||||
if (!scr_cyc_is_buffered(h)) {
|
||||
/* A survivor promoted into a level its type bypasses moves past
|
||||
* it, as a release would buffer it. */
|
||||
if (h->gen == SCR_CYC_MATURE && scr_cyc_bypassed(h->type, SCR_CYC_MATURE))
|
||||
h->gen = SCR_CYC_OLD;
|
||||
h->buffered = 1;
|
||||
h->buf_index = (uint32_t)scr_roots[h->gen].n;
|
||||
scr_cyc_push(&scr_roots[h->gen], obj);
|
||||
if (scr_cyc_gen(h) == SCR_CYC_MATURE && scr_cyc_bypassed(scr_cyc_type(h), SCR_CYC_MATURE))
|
||||
scr_cyc_set_gen(h, SCR_CYC_OLD);
|
||||
scr_cyc_set_buffered(h, (uint32_t)scr_roots[scr_cyc_gen(h)].n);
|
||||
scr_cyc_push(&scr_roots[scr_cyc_gen(h)], obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -985,7 +1005,7 @@ static SCR_CYC_COMPACT size_t scr_cyc_settle(unsigned gen_limit) {
|
||||
size_t freed = scr_white.n;
|
||||
for (size_t i = 0; i < scr_white.n; i++) {
|
||||
void *obj = scr_white.v[i];
|
||||
scr_cyc_hdr(obj)->type->free_fn(obj);
|
||||
scr_cyc_type(scr_cyc_hdr(obj))->free_fn(obj);
|
||||
}
|
||||
scr_white.n = 0;
|
||||
|
||||
@@ -1033,8 +1053,8 @@ static size_t scr_cyc_pass(unsigned gen_limit) {
|
||||
for (size_t i = 0; i < scr_roots[g].n; i++) {
|
||||
void *obj = scr_roots[g].v[i];
|
||||
ScrCycHdr *h = scr_cyc_hdr(obj);
|
||||
h->buffered = 0;
|
||||
if (gen_limit < SCR_CYC_OLD) scr_cyc_walked(h->type, gen_limit);
|
||||
scr_cyc_clear_buffered(h);
|
||||
if (gen_limit < SCR_CYC_OLD) scr_cyc_walked(scr_cyc_type(h), gen_limit);
|
||||
scr_cyc_push(&scr_cands, obj);
|
||||
}
|
||||
scr_roots[g].n = 0;
|
||||
@@ -1042,7 +1062,7 @@ static size_t scr_cyc_pass(unsigned gen_limit) {
|
||||
for (size_t i = 0; i < scr_cands.n; i++) scr_mark_gray(scr_cands.v[i], 0);
|
||||
while (scr_pending.n) {
|
||||
void *obj = scr_pending.v[--scr_pending.n];
|
||||
scr_cyc_hdr(obj)->type->trace(obj, scr_mg_visit, NULL);
|
||||
scr_cyc_type(scr_cyc_hdr(obj))->trace(obj, scr_mg_visit, NULL);
|
||||
}
|
||||
|
||||
scr_white.n = 0;
|
||||
@@ -1051,8 +1071,8 @@ static size_t scr_cyc_pass(unsigned gen_limit) {
|
||||
void *obj = scr_pending.v[--scr_pending.n];
|
||||
/* A later outside root may have restored this deferred white node and
|
||||
* its descendants already. Never scan a black node's edges again. */
|
||||
if (scr_cyc_hdr(obj)->color == SCR_CYC_WHITE)
|
||||
scr_cyc_hdr(obj)->type->trace(obj, scr_scan_visit, NULL);
|
||||
if (scr_cyc_color(scr_cyc_hdr(obj)) == SCR_CYC_WHITE)
|
||||
scr_cyc_type(scr_cyc_hdr(obj))->trace(obj, scr_scan_visit, NULL);
|
||||
}
|
||||
|
||||
return scr_cyc_settle(gen_limit);
|
||||
|
||||
@@ -204,9 +204,9 @@ static ScrError *scr_error_alloc(int kind) {
|
||||
size_t size = kind == SCR_ERR_DOMEX ? sizeof(ScrDomException)
|
||||
: kind == SCR_ERR_AGGREGATE ? sizeof(ScrAggregateError)
|
||||
: sizeof(ScrError);
|
||||
static const ScrCycType error_type = { &scr_error_trace, &scr_error_gcfree, SCR_CYC_KIND_ERROR };
|
||||
static const ScrCycType domex_type = { &scr_error_trace, &scr_domex_gcfree, SCR_CYC_KIND_DOMEXCEPTION };
|
||||
static const ScrCycType aggregate_type = { &scr_error_trace, &scr_aggregate_gcfree, SCR_CYC_KIND_ERROR };
|
||||
static ScrCycType error_type = { &scr_error_trace, &scr_error_gcfree, SCR_CYC_KIND_ERROR };
|
||||
static ScrCycType domex_type = { &scr_error_trace, &scr_domex_gcfree, SCR_CYC_KIND_DOMEXCEPTION };
|
||||
static ScrCycType aggregate_type = { &scr_error_trace, &scr_aggregate_gcfree, SCR_CYC_KIND_ERROR };
|
||||
ScrError *e;
|
||||
if (scr_error_traced) {
|
||||
e = scr_cyc_alloc(size, kind == SCR_ERR_DOMEX ? &domex_type
|
||||
|
||||
@@ -284,7 +284,7 @@ ScrEmitter *scr_emitter_new(void) {
|
||||
/* Always collector-headered: the registry holds closures, so the
|
||||
* emitter hierarchy is unconditionally cycle-capable (the canonical
|
||||
* capture cycle is `e.on('x', () => e.emit(...))`). */
|
||||
static const ScrCycType cyc_type = { &scr_emitter_trace, &scr_emitter_gcfree, SCR_CYC_KIND_EMITTER };
|
||||
static ScrCycType cyc_type = { &scr_emitter_trace, &scr_emitter_gcfree, SCR_CYC_KIND_EMITTER };
|
||||
ScrEmitter *em = scr_cyc_alloc(sizeof *em, &cyc_type);
|
||||
em->rc = 1;
|
||||
em->vt = &scr_emitter_vt;
|
||||
|
||||
@@ -282,7 +282,7 @@ static void scr_caught_gcfree(void *object) {
|
||||
}
|
||||
|
||||
ScrCaught *scr_exc_take(void) {
|
||||
static const ScrCycType cyc_type = { &scr_caught_trace_v, &scr_caught_gcfree, SCR_CYC_KIND_CAUGHT };
|
||||
static ScrCycType cyc_type = { &scr_caught_trace_v, &scr_caught_gcfree, SCR_CYC_KIND_CAUGHT };
|
||||
ScrCaught *c = scr_cyc_alloc(sizeof *c, &cyc_type);
|
||||
c->rc = 1;
|
||||
c->kind = scr_exc_kind;
|
||||
|
||||
@@ -817,8 +817,8 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) {
|
||||
*list = (ScrDyn *)d->v.str; /* freelist link */
|
||||
scr_dyn_free_count--;
|
||||
ScrCycHdr *header = scr_cyc_hdr(d);
|
||||
header->color = SCR_CYC_BLACK;
|
||||
header->gen = SCR_CYC_NURSERY;
|
||||
scr_cyc_set_color(header, SCR_CYC_BLACK);
|
||||
scr_cyc_set_gen(header, SCR_CYC_NURSERY);
|
||||
d->rc = 1;
|
||||
d->kind = kind;
|
||||
d->buffer = false;
|
||||
@@ -850,7 +850,7 @@ static ScrDyn *scr_dyn_alloc(ScrDynKind kind) {
|
||||
return d;
|
||||
}
|
||||
#endif
|
||||
static const ScrCycType cyc_type = { &scr_dyn_trace_v, &scr_dyn_gcfree, SCR_CYC_KIND_DYN };
|
||||
static ScrCycType cyc_type = { &scr_dyn_trace_v, &scr_dyn_gcfree, SCR_CYC_KIND_DYN };
|
||||
ScrDyn *fresh = scr_cyc_alloc_inline(sizeof *fresh, &cyc_type);
|
||||
if (!fresh) scr_json_oom();
|
||||
fresh->rc = 1;
|
||||
@@ -9957,7 +9957,7 @@ static void scr_weak_remove_impl(ScrWeakEntry *entry, bool collected) {
|
||||
}
|
||||
|
||||
static void scr_weak_remove(ScrWeakEntry *entry) {
|
||||
scr_weak_remove_impl(entry, scr_cyc_hdr(entry->owner)->color == SCR_CYC_DOOMED);
|
||||
scr_weak_remove_impl(entry, scr_cyc_color(scr_cyc_hdr(entry->owner)) == SCR_CYC_DOOMED);
|
||||
}
|
||||
|
||||
static void scr_weak_map_trace(void *ptr, ScrTraceVisit visit, void *ctx) {
|
||||
@@ -10157,7 +10157,7 @@ static ScrDyn *scr_weak_collection_new(ScrDyn *seed, bool is_set) {
|
||||
ScrDynHandleTag tag = is_set ? SCR_DYNH_WEAK_SET : SCR_DYNH_WEAK_MAP;
|
||||
scr_weak_dispose_hook = &scr_weak_disposed;
|
||||
scr_dyn_handle_install(tag, is_set ? &set_ops : &map_ops);
|
||||
static const ScrCycType cyc_type = { &scr_weak_map_trace, &scr_weak_map_gcfree, SCR_CYC_KIND_WEAK_MAP };
|
||||
static ScrCycType cyc_type = { &scr_weak_map_trace, &scr_weak_map_gcfree, SCR_CYC_KIND_WEAK_MAP };
|
||||
ScrWeakMap *map = scr_cyc_alloc(sizeof *map, &cyc_type);
|
||||
if (!map) scr_json_oom();
|
||||
map->rc = 1;
|
||||
|
||||
@@ -286,7 +286,7 @@ static void checked_leaf_cycles(void) {
|
||||
ScrStr *text = scr_str_new("survives either edge", 20);
|
||||
ScrDyn *leaf = scr_dyn_new_str(text);
|
||||
scr_str_release(text);
|
||||
scr_cyc_hdr(leaf)->gen = gen;
|
||||
scr_cyc_set_gen(scr_cyc_hdr(leaf), gen);
|
||||
ScrDyn *first = scr_dyn_new_obj(), *second = scr_dyn_new_obj();
|
||||
ScrDyn *array = scr_dyn_new_arr();
|
||||
ScrClosure *closure = scr_closure_new(NULL, 1);
|
||||
|
||||
@@ -720,7 +720,7 @@ ScrMap *scr_map_new_typed(ScrMapKeyKind key_kind, ScrMapValKind val_kind,
|
||||
ScrMap *m;
|
||||
if (key_trace || val_trace) {
|
||||
/* Either side can point back: collector header + trace. */
|
||||
static const ScrCycType cyc_type = { &scr_map_trace, &scr_map_gcfree, SCR_CYC_KIND_MAP };
|
||||
static ScrCycType cyc_type = { &scr_map_trace, &scr_map_gcfree, SCR_CYC_KIND_MAP };
|
||||
m = scr_cyc_alloc(sizeof *m, &cyc_type);
|
||||
} else {
|
||||
m = scr_map_alloc_plain();
|
||||
|
||||
@@ -17,7 +17,7 @@ static void scr_classobj_gcfree(void *object) {
|
||||
ScrClassObj *scr_classobj_new(const ScrClassObj *template, size_t ncaps) {
|
||||
if (ncaps > (SIZE_MAX - sizeof(ScrClassObj)) / sizeof(ScrBox *))
|
||||
scr_trap("scriptc: class capture allocation overflow\n");
|
||||
static const ScrCycType cyc_type = { &scr_classobj_trace_v, &scr_classobj_gcfree, SCR_CYC_KIND_CLASS_OBJECT };
|
||||
static ScrCycType cyc_type = { &scr_classobj_trace_v, &scr_classobj_gcfree, SCR_CYC_KIND_CLASS_OBJECT };
|
||||
ScrClassObj *c = scr_cyc_alloc(sizeof(ScrClassObj) + ncaps * sizeof(ScrBox *), &cyc_type);
|
||||
c->rc = 1;
|
||||
c->pre = template->pre;
|
||||
|
||||
@@ -74,7 +74,7 @@ void scr_init(void);
|
||||
/* Program objects emitted by the bundled LLVM helper reference this symbol.
|
||||
* Its versioned spelling makes a mismatched manual runtime link fail before
|
||||
* the program can start. */
|
||||
void scr_runtime_abi_v19(void);
|
||||
void scr_runtime_abi_v20(void);
|
||||
void scr_runtime_workers_v8(void);
|
||||
|
||||
/* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ──────
|
||||
@@ -393,10 +393,11 @@ char **scr_worker_argv(void);
|
||||
* Reference counting alone cannot free cycles, so every object that can
|
||||
* participate in one — capture boxes, heap closures, unions, promises, and
|
||||
* compiler-emitted class/record shapes with cycle-capable fields — is
|
||||
* allocated with a hidden header directly BEFORE the object: a trace
|
||||
* function (enumerates the object's cycle-capable children), a teardown
|
||||
* function (frees the object WITHOUT releasing traced children), and the
|
||||
* trial-deletion bookkeeping (color + candidate-root buffer state). Types
|
||||
* allocated with a hidden 8-byte header directly BEFORE the object: the
|
||||
* registry id of its type's descriptor (a trace function enumerating the
|
||||
* object's cycle-capable children, and a teardown function freeing the
|
||||
* object WITHOUT releasing traced children) and the trial-deletion
|
||||
* bookkeeping (color, generation and candidate-root buffer state). Types
|
||||
* that can never be in a cycle — strings above all, plus arrays of scalar/
|
||||
* string/bytes elements, dyn trees, and shapes whose fields are all acyclic
|
||||
* — keep the lean 1-word `rc` header and pay nothing. Arrays and maps are
|
||||
@@ -448,9 +449,11 @@ enum { SCR_CYC_BLACK = 0, SCR_CYC_GRAY = 1, SCR_CYC_WHITE = 2, SCR_CYC_DOOMED =
|
||||
enum { SCR_CYC_NURSERY = 0, SCR_CYC_MATURE = 1, SCR_CYC_OLD = 2, SCR_CYC_NGENS = 3 };
|
||||
|
||||
/* A headered type's collector entry points, shared by all its objects: the
|
||||
* header names one constant descriptor instead of carrying both function
|
||||
* pointers, which keeps the header at 16 bytes. Emitted shapes define one
|
||||
* per traced type (llvm/shapes.ts); runtime families use SCR_CYC_TYPE. */
|
||||
* header names its descriptor by a registry id (`id`, assigned on the type's
|
||||
* first allocation) instead of carrying a pointer, which keeps the header at
|
||||
* 8 bytes. Emitted shapes define one per traced type (llvm/shapes.ts);
|
||||
* runtime families define their own. Descriptors are writable (the id) and
|
||||
* must outlive every object of the type. */
|
||||
typedef struct ScrCycType {
|
||||
ScrTraceFn trace;
|
||||
ScrCycFreeFn free_fn;
|
||||
@@ -462,6 +465,9 @@ typedef struct ScrCycType {
|
||||
* share an index (separately emitted code in one process) are merely
|
||||
* judged together. */
|
||||
uint32_t kind;
|
||||
/* Registry id (scr_cyc_types), 0 until scr_cyc_register assigns it.
|
||||
* Emitted allocation paths read it and take the slow path while it is 0. */
|
||||
uint32_t id;
|
||||
} ScrCycType;
|
||||
/* Runtime families' kinds. Emitted descriptors count from
|
||||
* SCR_CYC_KIND_EMITTED (llvm/alloc.ts FIRST_EMITTED_CYC_KIND). */
|
||||
@@ -484,46 +490,71 @@ enum {
|
||||
SCR_CYC_KIND_WORKER_VIEW,
|
||||
SCR_CYC_KIND_EMITTED = 32
|
||||
};
|
||||
#define SCR_CYC_TYPE(name, trace_fn, free) static const ScrCycType name = { (trace_fn), (free), 0 }
|
||||
|
||||
/* Eight bytes in front of the object: the descriptor's registry id, the
|
||||
* 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). */
|
||||
typedef struct ScrCycHdr {
|
||||
#if UINTPTR_MAX == UINT32_MAX
|
||||
/* calloc aligns the block for doubles; keep the payload aligned too.
|
||||
* Padding BEFORE the descriptor keeps every object-relative offset of the
|
||||
* fields below identical to 64-bit targets. */
|
||||
uint32_t payload_alignment;
|
||||
#endif
|
||||
const ScrCycType *type; /* trace and teardown */
|
||||
uint8_t color; /* SCR_CYC_* */
|
||||
uint8_t gen; /* SCR_CYC_NURSERY..SCR_CYC_OLD (the walk filter) */
|
||||
uint16_t buffered; /* 1 = sitting in its generation's candidate buffer */
|
||||
uint32_t buf_index; /* position there (O(1) removal when rc hits 0) */
|
||||
uint16_t id;
|
||||
uint8_t color;
|
||||
uint8_t gen;
|
||||
uint32_t buf;
|
||||
} ScrCycHdr;
|
||||
#define SCR_CYC_BUFFERED_BIT 0x80000000u
|
||||
|
||||
/* This layout is an ABI, not an implementation detail. 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:
|
||||
* it reads `buffered` as an i16 at obj-6 (the backend's cycleBufferedOffset)
|
||||
* and calls scr_cyc_on_release only when it is 0; the emitted inline free
|
||||
* reads the generation as an i8 at obj-7. Hence the assertions. */
|
||||
_Static_assert(sizeof(ScrCycHdr) == 16, "LLVM backend expects a 16-byte cycle header");
|
||||
_Static_assert(sizeof(ScrCycHdr) - offsetof(ScrCycHdr, buffered) == 6 &&
|
||||
sizeof(((ScrCycHdr *)0)->buffered) == 2,
|
||||
"the emitted release fast path reads `buffered` as an i16 at obj-6");
|
||||
_Static_assert(sizeof(ScrCycHdr) - offsetof(ScrCycHdr, gen) == 7,
|
||||
"LLVM backend's inline free reads the generation as an i8 at obj-7");
|
||||
_Static_assert(sizeof(ScrCycHdr) - offsetof(ScrCycHdr, type) == 16 ||
|
||||
UINTPTR_MAX == UINT32_MAX,
|
||||
"LLVM backend stores the descriptor at the block start on 64-bit targets");
|
||||
/* This layout is an ABI, not an implementation detail. The emitted
|
||||
* allocation stores the id and zeroes the rest; 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. */
|
||||
_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");
|
||||
_Static_assert(sizeof(ScrCycHdr) - offsetof(ScrCycHdr, buf) == 4,
|
||||
"the emitted release fast path tests `buf` as an i32 at obj-4");
|
||||
_Static_assert(sizeof(ScrCycHdr) - offsetof(ScrCycHdr, gen) == 5,
|
||||
"LLVM backend's inline free reads the generation as an i8 at obj-5");
|
||||
_Static_assert(sizeof(ScrCycHdr) % _Alignof(double) == 0,
|
||||
"cycle-headered payloads must preserve double alignment");
|
||||
|
||||
static inline ScrCycHdr *scr_cyc_hdr(void *obj) { return (ScrCycHdr *)obj - 1; }
|
||||
|
||||
/* Registry of descriptors by id (copies, so a header reaches its entry
|
||||
* points in one step); ids are assigned on first allocation and shared by
|
||||
* every thread. */
|
||||
#define SCR_CYC_MAX_TYPES ((uint32_t)1 << 15)
|
||||
extern ScrCycType scr_cyc_types[SCR_CYC_MAX_TYPES];
|
||||
uint32_t scr_cyc_register(ScrCycType *type);
|
||||
static inline uint32_t scr_cyc_type_id(ScrCycType *type) {
|
||||
/* Acquire pairs with scr_cyc_register's publishing compare-exchange, so
|
||||
* a thread that sees the id also sees its scr_cyc_types entry. */
|
||||
uint32_t id = __atomic_load_n(&type->id, __ATOMIC_ACQUIRE);
|
||||
return id ? id : scr_cyc_register(type);
|
||||
}
|
||||
|
||||
static inline const ScrCycType *scr_cyc_type(const ScrCycHdr *h) { return &scr_cyc_types[h->id]; }
|
||||
static inline unsigned scr_cyc_color(const ScrCycHdr *h) { return h->color; }
|
||||
static inline void scr_cyc_set_color(ScrCycHdr *h, unsigned color) { h->color = (uint8_t)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;
|
||||
}
|
||||
static inline uint32_t scr_cyc_buf_index(const ScrCycHdr *h) {
|
||||
return h->buf & ~SCR_CYC_BUFFERED_BIT;
|
||||
}
|
||||
/* Buffered at position `index` of its generation's buffer, or not at all. */
|
||||
static inline void scr_cyc_set_buffered(ScrCycHdr *h, uint32_t index) {
|
||||
h->buf = SCR_CYC_BUFFERED_BIT | index;
|
||||
}
|
||||
static inline void scr_cyc_clear_buffered(ScrCycHdr *h) { h->buf = 0; }
|
||||
|
||||
/* Zeroed allocation with a cycle header in front; returns the OBJECT
|
||||
* pointer (header at scr_cyc_hdr). `type` must outlive the object (a
|
||||
* static descriptor). Aborts on OOM. */
|
||||
void *scr_cyc_alloc(size_t size, const ScrCycType *type);
|
||||
void *scr_cyc_alloc(size_t size, ScrCycType *type);
|
||||
void scr_cyc_free(void *obj); /* frees the block, header included */
|
||||
|
||||
/* ── small-object allocator (scr_alloc.c) ───────────────────────────────
|
||||
@@ -714,26 +745,27 @@ static inline void scr_mem_free(void *p) { free(p); }
|
||||
* sites (union boxes, arrays, boxes, closures, maps, dyn nodes). With a
|
||||
* constant size the size class and the zeroing fold at compile time. The
|
||||
* live count is exported for these and for the emitted inline paths
|
||||
* (llvm/alloc.ts), which do the same three things: header words, live
|
||||
* count, block. Every other header word starts zero (SCR_CYC_BLACK,
|
||||
* SCR_CYC_NURSERY, unbuffered). */
|
||||
* (llvm/alloc.ts), which do the same three things: header (the registry
|
||||
* id; the rest starts zero: SCR_CYC_BLACK, SCR_CYC_NURSERY, unbuffered),
|
||||
* live count, block. */
|
||||
#ifndef SCR_WORKERS
|
||||
extern SCR_TL size_t scr_cyc_live;
|
||||
extern SCR_TL size_t scr_cyc_old_freed;
|
||||
extern SCR_TL void (*scr_weak_dispose_hook)(void *);
|
||||
#endif
|
||||
static inline void *scr_cyc_alloc_inline(size_t size, const ScrCycType *type) {
|
||||
static inline void *scr_cyc_alloc_inline(size_t size, ScrCycType *type) {
|
||||
if (size > SIZE_MAX - sizeof(ScrCycHdr)) scr_trap("scriptc: out of memory\n");
|
||||
uint32_t id = scr_cyc_type_id(type);
|
||||
ScrCycHdr *h = (ScrCycHdr *)scr_mem_calloc(sizeof(ScrCycHdr) + size);
|
||||
if (!h) scr_trap("scriptc: out of memory\n");
|
||||
h->type = type;
|
||||
h->id = (uint16_t)id;
|
||||
scr_cyc_live++;
|
||||
return h + 1;
|
||||
}
|
||||
static inline void scr_cyc_free_inline(void *obj) {
|
||||
if (scr_weak_dispose_hook) scr_weak_dispose_hook(obj);
|
||||
scr_cyc_live--;
|
||||
if (scr_cyc_hdr(obj)->gen == SCR_CYC_OLD) scr_cyc_old_freed++;
|
||||
if (scr_cyc_gen(scr_cyc_hdr(obj)) == SCR_CYC_OLD) scr_cyc_old_freed++;
|
||||
scr_mem_free(scr_cyc_hdr(obj));
|
||||
}
|
||||
_Static_assert(SCR_CYC_NURSERY == 0, "zeroed cycle headers start in the nursery");
|
||||
@@ -761,10 +793,10 @@ void scr_cyc_on_dead(void *obj);
|
||||
* object, and the death of an object that was never buffered, need no call.
|
||||
* Most releases of long-lived containers take the first early return. */
|
||||
static inline void scr_cyc_released(void *obj) {
|
||||
if (!scr_cyc_hdr(obj)->buffered) scr_cyc_on_release(obj);
|
||||
if (!scr_cyc_is_buffered(scr_cyc_hdr(obj))) scr_cyc_on_release(obj);
|
||||
}
|
||||
static inline void scr_cyc_dead(void *obj) {
|
||||
if (scr_cyc_hdr(obj)->buffered) scr_cyc_on_dead(obj);
|
||||
if (scr_cyc_is_buffered(scr_cyc_hdr(obj))) scr_cyc_on_dead(obj);
|
||||
}
|
||||
|
||||
/* Full sweep: trial-deletion passes over EVERY generation, to a fixpoint.
|
||||
|
||||
@@ -1230,7 +1230,7 @@ static ScrStreamState *scr_stream_state_new(bool has_r, bool has_w,
|
||||
static ScrStream *scr_stream_alloc(const ScrVt *vt, const char *cls, bool has_r, bool has_w,
|
||||
double rhwm, double whwm, bool auto_destroy,
|
||||
bool emit_close, bool allow_half_open) {
|
||||
static const ScrCycType cyc_type = { &scr_stream_trace, &scr_stream_gcfree, SCR_CYC_KIND_STREAM };
|
||||
static ScrCycType cyc_type = { &scr_stream_trace, &scr_stream_gcfree, SCR_CYC_KIND_STREAM };
|
||||
ScrStream *s = scr_cyc_alloc(sizeof *s, &cyc_type);
|
||||
s->rc = 1;
|
||||
s->vt = vt;
|
||||
|
||||
@@ -39,7 +39,7 @@ static void scr_union_gcfree(void *o) {
|
||||
}
|
||||
|
||||
static ScrUnion *scr_union_alloc(uint32_t tag) {
|
||||
static const ScrCycType cyc_type = { &scr_union_trace, &scr_union_gcfree, SCR_CYC_KIND_UNION };
|
||||
static ScrCycType cyc_type = { &scr_union_trace, &scr_union_gcfree, SCR_CYC_KIND_UNION };
|
||||
ScrUnion *u = scr_cyc_alloc_inline(sizeof(ScrUnion), &cyc_type);
|
||||
u->rc = 1;
|
||||
u->tag = tag;
|
||||
|
||||
@@ -77,7 +77,7 @@ static void scr_worker_port_listeners(void *ptr, const ScrStr *name, bool presen
|
||||
}
|
||||
|
||||
static ScrWorkerView *scr_worker_view_new(bool port) {
|
||||
static const ScrCycType cyc_type = { &scr_worker_view_trace, &scr_worker_view_gcfree, SCR_CYC_KIND_WORKER_VIEW };
|
||||
static ScrCycType cyc_type = { &scr_worker_view_trace, &scr_worker_view_gcfree, SCR_CYC_KIND_WORKER_VIEW };
|
||||
ScrWorkerView *view = scr_cyc_alloc(sizeof(*view), &cyc_type);
|
||||
view->rc = 1;
|
||||
view->port = port;
|
||||
|
||||
@@ -388,7 +388,7 @@ static void mock_cyc_release(void *p) {
|
||||
}
|
||||
|
||||
static MockRec *mock_cyc_new(int value) {
|
||||
static const ScrCycType cyc_type = { &mock_trace, &mock_gcfree, 0 };
|
||||
static ScrCycType cyc_type = { &mock_trace, &mock_gcfree, 0 };
|
||||
MockRec *r = scr_cyc_alloc(sizeof *r, &cyc_type);
|
||||
r->rc = 1;
|
||||
r->value = value;
|
||||
|
||||
@@ -95,20 +95,20 @@ int main(void) {
|
||||
&scr_str_release_v, NULL);
|
||||
scr_union_retain(su);
|
||||
scr_union_release(su);
|
||||
CHECK(!scr_cyc_hdr(su)->buffered);
|
||||
CHECK(!scr_cyc_is_buffered(scr_cyc_hdr(su)));
|
||||
scr_union_release(su);
|
||||
CHECK(scr_str_live_count() == 0);
|
||||
ScrUnion *fu = scr_union_new_f64(1, 2.5);
|
||||
scr_union_retain(fu);
|
||||
scr_union_release(fu);
|
||||
CHECK(!scr_cyc_hdr(fu)->buffered);
|
||||
CHECK(!scr_cyc_is_buffered(scr_cyc_hdr(fu)));
|
||||
scr_union_release(fu);
|
||||
ScrUnion *cu = scr_union_new_ref(0, scr_closure_new((void *)&dummy_fn, 0),
|
||||
&scr_closure_retain_v, &scr_closure_release_v,
|
||||
&scr_closure_trace_v);
|
||||
scr_union_retain(cu);
|
||||
scr_union_release(cu);
|
||||
CHECK(scr_cyc_hdr(cu)->buffered);
|
||||
CHECK(scr_cyc_is_buffered(scr_cyc_hdr(cu)));
|
||||
scr_union_release(cu); /* dies while buffered: leaves the buffer, frees */
|
||||
CHECK(scr_closure_live_count() == 0);
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ static Node *make_ring(size_t count) {
|
||||
Node **nodes = calloc(count, sizeof(*nodes));
|
||||
if (!nodes) scr_trap("cycle test: out of memory\n");
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
static const ScrCycType cyc_type = { &node_trace, &node_free, 0 };
|
||||
static ScrCycType cyc_type = { &node_trace, &node_free, 0 };
|
||||
nodes[i] = scr_cyc_alloc(sizeof(*nodes[i]), &cyc_type);
|
||||
nodes[i]->rc = 1; /* the ring edge that will point at this node */
|
||||
}
|
||||
@@ -119,7 +119,7 @@ static void check_age_reset_when_last_mature_root_dies(void) {
|
||||
ring = make_ring(32);
|
||||
ring->rc++;
|
||||
release_live(ring);
|
||||
static const ScrCycType cyc_type = { &node_trace, &node_free, 0 };
|
||||
static ScrCycType cyc_type = { &node_trace, &node_free, 0 };
|
||||
leaf = scr_cyc_alloc(sizeof(*leaf), &cyc_type);
|
||||
leaf->rc = 1;
|
||||
leaf->rc++; /* temporary release makes it a nursery candidate */
|
||||
@@ -155,7 +155,7 @@ static void check_dead_backlog_rearms_release_trigger(void) {
|
||||
Node *leaves[BACKLOG];
|
||||
|
||||
for (size_t i = 0; i < BACKLOG; i++) {
|
||||
static const ScrCycType cyc_type = { &node_trace, &node_free, 0 };
|
||||
static ScrCycType cyc_type = { &node_trace, &node_free, 0 };
|
||||
leaves[i] = scr_cyc_alloc(sizeof(*leaves[i]), &cyc_type);
|
||||
leaves[i]->rc = 2; /* external owner plus a temporary reference */
|
||||
release_live(leaves[i]);
|
||||
@@ -183,7 +183,7 @@ static void check_dead_backlog_rearms_release_trigger(void) {
|
||||
}
|
||||
|
||||
static Node *make_leaf(void) {
|
||||
static const ScrCycType cyc_type = { &node_trace, &node_free, 0 };
|
||||
static ScrCycType cyc_type = { &node_trace, &node_free, 0 };
|
||||
Node *leaf = scr_cyc_alloc(sizeof(*leaf), &cyc_type);
|
||||
leaf->rc = 1;
|
||||
return leaf;
|
||||
@@ -267,7 +267,7 @@ static void age_to_old(Node *node) {
|
||||
read_live(node);
|
||||
scr_collect_cycles();
|
||||
}
|
||||
check(scr_cyc_hdr(node)->gen == SCR_CYC_OLD, "survivor did not reach the old generation");
|
||||
check(scr_cyc_gen(scr_cyc_hdr(node)) == SCR_CYC_OLD, "survivor did not reach the old generation");
|
||||
}
|
||||
|
||||
/* Fresh live leaves, each released once so the release path keeps running
|
||||
@@ -328,7 +328,7 @@ static void check_older_rings_into_old(void) {
|
||||
}
|
||||
scr_cyc_collect_scheduled(); /* every ring survives at least one pass */
|
||||
for (size_t i = 0; i < RINGS; i++) {
|
||||
check(scr_cyc_hdr(rings[i])->gen != SCR_CYC_NURSERY, "ring was not promoted");
|
||||
check(scr_cyc_gen(scr_cyc_hdr(rings[i])) != SCR_CYC_NURSERY, "ring was not promoted");
|
||||
release_live(rings[i]);
|
||||
}
|
||||
size_t grown = grow_until(keep, LIMIT, before + RINGS * RING);
|
||||
@@ -435,8 +435,8 @@ static size_t grow_dead_cycle_until_full_pass(size_t old_garbage, bool mixed) {
|
||||
age_to_old(tie);
|
||||
garbage = make_ring(old_garbage);
|
||||
read_live(garbage); /* a release-triggered pass may promote it at once */
|
||||
if (scr_cyc_hdr(garbage)->gen == SCR_CYC_NURSERY) scr_collect_cycles();
|
||||
check(scr_cyc_hdr(garbage)->gen == SCR_CYC_MATURE, "survivor did not reach the mature generation");
|
||||
if (scr_cyc_gen(scr_cyc_hdr(garbage)) == SCR_CYC_NURSERY) scr_collect_cycles();
|
||||
check(scr_cyc_gen(scr_cyc_hdr(garbage)) == SCR_CYC_MATURE, "survivor did not reach the mature generation");
|
||||
tie->other = garbage;
|
||||
garbage->rc++;
|
||||
garbage->other = tie;
|
||||
@@ -648,7 +648,7 @@ static void check_deep_cross_generation_chain(void) {
|
||||
size_t before = freed;
|
||||
Node *chain = NULL;
|
||||
for (size_t i = 0; i < DEPTH; i++) {
|
||||
static const ScrCycType cyc_type = { &node_trace, &node_free, 0 };
|
||||
static ScrCycType cyc_type = { &node_trace, &node_free, 0 };
|
||||
Node *node = scr_cyc_alloc(sizeof(*node), &cyc_type);
|
||||
node->rc = 1;
|
||||
node->next = chain;
|
||||
@@ -719,7 +719,7 @@ static void bypass_free(void *obj) {
|
||||
bypass_freed++;
|
||||
node_free(obj);
|
||||
}
|
||||
static const ScrCycType bypass_type = { &bypass_trace, &bypass_free, 7 };
|
||||
static ScrCycType bypass_type = { &bypass_trace, &bypass_free, 7 };
|
||||
|
||||
static Node *make_bypass_node(void) {
|
||||
Node *node = scr_cyc_alloc(sizeof(*node), &bypass_type);
|
||||
@@ -818,7 +818,7 @@ static void mature_kind_free(void *obj) {
|
||||
mature_kind_freed++;
|
||||
node_free(obj);
|
||||
}
|
||||
static const ScrCycType mature_kind_type = { &node_trace, &mature_kind_free, 8 };
|
||||
static ScrCycType mature_kind_type = { &node_trace, &mature_kind_free, 8 };
|
||||
|
||||
static Node *make_mature_kind_node(void) {
|
||||
Node *node = scr_cyc_alloc(sizeof(*node), &mature_kind_type);
|
||||
@@ -834,7 +834,7 @@ static size_t probe_old_on_release(Node **owned, size_t probe) {
|
||||
for (size_t i = 0; i < probe; i++) {
|
||||
owned[i] = make_mature_kind_node();
|
||||
read_live(owned[i]);
|
||||
old += scr_cyc_hdr(owned[i])->gen == SCR_CYC_OLD;
|
||||
old += scr_cyc_gen(scr_cyc_hdr(owned[i])) == SCR_CYC_OLD;
|
||||
}
|
||||
return old;
|
||||
}
|
||||
@@ -855,7 +855,7 @@ static void check_mature_bypass(void) {
|
||||
* at that level: none dies. */
|
||||
scr_collect_cycles();
|
||||
for (size_t i = 0; i < nlive; i++) {
|
||||
scr_cyc_hdr(live[i])->gen = SCR_CYC_MATURE;
|
||||
scr_cyc_set_gen(scr_cyc_hdr(live[i]), SCR_CYC_MATURE);
|
||||
read_live(live[i]);
|
||||
}
|
||||
scr_cyc_pass_at(SCR_CYC_MATURE);
|
||||
@@ -924,7 +924,7 @@ static void paired_free(void *obj) {
|
||||
paired_freed++;
|
||||
node_free(obj);
|
||||
}
|
||||
static const ScrCycType paired_type = { &paired_trace, &paired_free, 10 };
|
||||
static ScrCycType paired_type = { &paired_trace, &paired_free, 10 };
|
||||
|
||||
static void check_probe_sees_candidate_rings(void) {
|
||||
/* Both members of a ring must be buffered before a pass walks either: with
|
||||
@@ -978,7 +978,7 @@ static void check_probe_sees_candidate_rings(void) {
|
||||
/* A nursery pass re-buffers its survivors past the mature level when
|
||||
* their type bypasses it, even if the type's nursery candidates are
|
||||
* walked (here half of them die young, in rings). */
|
||||
static const ScrCycType young_dying_type = { &node_trace, &node_free, 9 };
|
||||
static ScrCycType young_dying_type = { &node_trace, &node_free, 9 };
|
||||
|
||||
static void check_rebuffer_past_mature(void) {
|
||||
enum { BATCH = 1024, ROUNDS = 24, PROBE = 1024 };
|
||||
@@ -1011,7 +1011,7 @@ static void check_rebuffer_past_mature(void) {
|
||||
}
|
||||
scr_cyc_pass_at(SCR_CYC_NURSERY);
|
||||
size_t old = 0;
|
||||
for (size_t i = nlive - PROBE; i < nlive; i++) old += scr_cyc_hdr(live[i])->gen == SCR_CYC_OLD;
|
||||
for (size_t i = nlive - PROBE; i < nlive; i++) old += scr_cyc_gen(scr_cyc_hdr(live[i])) == SCR_CYC_OLD;
|
||||
check(old >= PROBE / 2, "nursery survivors were re-buffered into a level their type bypasses");
|
||||
for (size_t i = 0; i < nlive; i++) {
|
||||
live[i]->rc = 0;
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// Cycle-capable objects of many types share one compact header that names
|
||||
// each type by an id assigned on its first allocation. Several worker
|
||||
// threads allocate their first objects of the same types at the same time;
|
||||
// cycles of every shape are built, kept, broken and dropped across the
|
||||
// collector's generations; and the survivors are checked on every thread.
|
||||
import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
|
||||
|
||||
class Ring {
|
||||
next: Ring | null = null;
|
||||
readonly id: number;
|
||||
constructor(id: number) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
class Pair {
|
||||
other: Pair | null = null;
|
||||
value: number;
|
||||
constructor(value: number) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
class Graph {
|
||||
readonly nodes: GraphNode[] = [];
|
||||
}
|
||||
class GraphNode {
|
||||
readonly edges: GraphEdge[] = [];
|
||||
owner: Graph | null = null;
|
||||
readonly label: string;
|
||||
constructor(label: string) {
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
class GraphEdge {
|
||||
readonly to: GraphNode;
|
||||
readonly weight: number;
|
||||
constructor(to: GraphNode, weight: number) {
|
||||
this.to = to;
|
||||
this.weight = weight;
|
||||
}
|
||||
}
|
||||
interface Cell {
|
||||
value: number;
|
||||
next: Cell | null;
|
||||
link: (() => number) | null;
|
||||
}
|
||||
|
||||
function ring(size: number, base: number): Ring {
|
||||
const first = new Ring(base);
|
||||
let last = first;
|
||||
for (let i = 1; i < size; i++) {
|
||||
const node = new Ring(base + i);
|
||||
last.next = node;
|
||||
last = node;
|
||||
}
|
||||
last.next = first;
|
||||
return first;
|
||||
}
|
||||
|
||||
function ringSum(start: Ring): number {
|
||||
let sum = start.id;
|
||||
for (let at = start.next; at !== null && at !== start; at = at.next) sum += at.id;
|
||||
return sum;
|
||||
}
|
||||
|
||||
function graph(n: number, seed: number): Graph {
|
||||
const g = new Graph();
|
||||
for (let i = 0; i < n; i++) {
|
||||
const node = new GraphNode(`n${i}`);
|
||||
node.owner = g;
|
||||
g.nodes.push(node);
|
||||
}
|
||||
let x = seed;
|
||||
for (const node of g.nodes) {
|
||||
for (let k = 0; k < 3; k++) {
|
||||
x = (Math.imul(x, 1103515245) + 12345) >>> 0;
|
||||
node.edges.push(new GraphEdge(g.nodes[x % n]!, x % 17));
|
||||
}
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
function graphWeight(g: Graph): number {
|
||||
let total = 0;
|
||||
for (const node of g.nodes) for (const edge of node.edges) total += edge.weight + edge.to.label.length;
|
||||
return total;
|
||||
}
|
||||
|
||||
function cells(n: number): Cell {
|
||||
const head: Cell = { value: 0, next: null, link: null };
|
||||
let at = head;
|
||||
for (let i = 1; i < n; i++) {
|
||||
const cell: Cell = { value: i, next: null, link: null };
|
||||
cell.link = () => cell.value + head.value;
|
||||
at.next = cell;
|
||||
at = cell;
|
||||
}
|
||||
at.next = head;
|
||||
return head;
|
||||
}
|
||||
|
||||
function work(tag: number): string {
|
||||
const kept: Ring[] = [];
|
||||
const pairs: Pair[] = [];
|
||||
let dropped = 0;
|
||||
for (let round = 0; round < 40; round++) {
|
||||
// Garbage rings, pairs and graphs; every few rounds one survives.
|
||||
const r = ring(5 + (round % 7), round * 100 + tag);
|
||||
dropped += ringSum(r);
|
||||
if (round % 9 === 0) kept.push(r);
|
||||
const a = new Pair(round);
|
||||
const b = new Pair(-round);
|
||||
a.other = b;
|
||||
b.other = a;
|
||||
if (round % 13 === 0) pairs.push(a);
|
||||
dropped += graphWeight(graph(30 + round, round + tag));
|
||||
const c = cells(8);
|
||||
let links = 0;
|
||||
for (let at = c.next; at !== c && at !== null; at = at.next) links += at.link!();
|
||||
dropped += links;
|
||||
}
|
||||
// Break one kept ring and drop it; the rest must survive the passes.
|
||||
kept[0]!.next = null;
|
||||
kept.shift();
|
||||
let survivors = 0;
|
||||
for (const r of kept) survivors += ringSum(r);
|
||||
let pairSum = 0;
|
||||
for (const p of pairs) pairSum += p.value + p.other!.value + p.other!.other!.value;
|
||||
return `tag ${tag} dropped ${dropped} kept ${kept.length} survivors ${survivors} pairs ${pairSum}`;
|
||||
}
|
||||
|
||||
if (isMainThread) {
|
||||
const results: Promise<string>[] = [];
|
||||
for (let t = 1; t <= 3; t++) {
|
||||
results.push(
|
||||
new Promise<string>((resolve) => {
|
||||
const worker = new Worker(new URL(import.meta.url), { workerData: t });
|
||||
worker.on("message", (text: string) => resolve(text));
|
||||
}),
|
||||
);
|
||||
}
|
||||
console.log("main", work(0));
|
||||
for (const text of await Promise.all(results)) console.log("worker", text);
|
||||
console.log("main again", work(9));
|
||||
} else {
|
||||
parentPort!.postMessage(work(workerData as number));
|
||||
}
|
||||
+1
-1
@@ -52,7 +52,7 @@ try {
|
||||
return `@native_literal_${index}`;
|
||||
},
|
||||
sizeType: request.bits === 32 ? "i32" : "i64",
|
||||
cycleBufferedOffset: 6,
|
||||
cycleBufOffset: 4,
|
||||
// Collector statistics indices, numbered as LlEmitter numbers them.
|
||||
nextCycKind: () => FIRST_EMITTED_CYC_KIND + cycKinds++,
|
||||
tracedShapes: traced.shapes,
|
||||
|
||||
@@ -96,7 +96,7 @@ for (const backend of ["llvm"] as const) {
|
||||
object_format: target.objectFormat,
|
||||
minimum_os: target.minimumOs,
|
||||
},
|
||||
runtime_abi: { version: 19, marker: "scr_runtime_abi_v19" },
|
||||
runtime_abi: { version: 20, marker: "scr_runtime_abi_v20" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" },
|
||||
flavors: {
|
||||
|
||||
Reference in New Issue
Block a user