Allocate ProgramLifetime classes in counted lifetime regions

This commit is contained in:
Chris Tate
2026-10-13 19:18:28 -05:00
parent 07e01d024e
commit 604a316ec3
8 changed files with 778 additions and 16 deletions
@@ -79,6 +79,15 @@ function threadLocalState(host: AllocHost, prefix: string, lines: string[]): str
return [...preamble, ...text.split("\n")];
}
/** Worker executables: the calling script thread's ScrThreadHot block into
* `dest`. Its address also identifies the thread (the owner word of a
* lifetime-region chunk; llvm/regions.ts). */
export function threadHotAddressLines(host: AllocHost, dest: string): string[] {
host.declare(`@scr_thread_hot = external thread_local global ${THREAD_HOT_TYPE}`);
host.declare(`declare nonnull ptr @llvm.threadlocal.address.p0(ptr nonnull)`);
return [` ${dest} = call ptr @llvm.threadlocal.address.p0(ptr @scr_thread_hot)`];
}
const ALLOC_NOTE = ` call void @scr_obj_alloc_note()`;
const FREE_NOTE = ` call void @scr_obj_free_note()`;
+17 -9
View File
@@ -40,6 +40,7 @@ import {
directDestroyTarget,
usedDirectTargets,
type DirectDestroy,
type RegionHooks,
llFieldType,
releaseBody,
releaseFastPath,
@@ -683,15 +684,22 @@ export function emitClassShapes(
);
// A program-lifetime instance is never freed: no collector header
// (nothing walks or frees it) and the program-lifetime count, which
// publication turns into SIZE_MAX (programLifetimeRc).
nw.push(
...emitObjectAlloc(
host,
sizeOf,
traced && !lifetime ? { type: cycType } : null,
meta.hierarchy ? 16 : 8,
),
);
// publication turns into SIZE_MAX (programLifetimeRc). Programs with
// lifetime regions allocate it in the current region, which frees it
// with the region (llvm/regions.ts).
const regions: RegionHooks | null = lifetime ? (host.regions ?? null) : null;
if (regions !== null) {
nw.push(...regions.newAlloc(cls.name, sizeOf));
defs.push(...regions.classDefinitions(cls, sizeOf));
} else
nw.push(
...emitObjectAlloc(
host,
sizeOf,
traced && !lifetime ? { type: cycType } : null,
meta.hierarchy ? 16 : 8,
),
);
nw.push(` store ${host.sizeType} ${lifetime ? programLifetimeRc(host.sizeType) : 1}, ptr %o`);
if (lifetime && audit) {
// --sanitize builds: the RC audit skips its leak report when the
@@ -273,8 +273,10 @@ import {
classEnvironmentIndex,
classFieldIndex,
classStructSym,
programLifetimeClass,
type LlClassMeta,
} from "./classes.js";
import { LifetimeRegions, REGION_TYPES } from "./regions.js";
import { LlDyn, type DynHost } from "./dyn.js";
import { VirtualBorrows } from "./virtual-borrows.js";
import { LlvmUnsupportedError } from "./unsupported.js";
@@ -488,6 +490,18 @@ export class LlEmitter {
this.fieldPointerTags.set(ptr, tag);
}
/** The alias tag of a runtime word that emitted code reads and writes
* outside every field (the lifetime-region count, owner and cursors). */
memoryAliasAttachment(category: string): string {
if (this.debug !== null) return "";
let tag = this.fieldAliasTags.get(category);
if (tag === undefined) {
tag = 2 + this.fieldAliasTags.size * 2;
this.fieldAliasTags.set(category, tag);
}
return `, !tbaa !${tag}`;
}
fieldAliasAttachment(ptr: string): string {
const tag = this.fieldPointerTags.get(ptr);
return tag === undefined ? "" : `, !tbaa !${tag}`;
@@ -1082,6 +1096,7 @@ export class LlEmitter {
pendingTestLines: (dest) => this.pendingTestLines(dest),
virtualEntry: (implFn) => this.virtualEntry(implFn),
programLifetime: this.programLifetime,
regionAliasTag: (word) => this.memoryAliasAttachment(`region.${word}`),
};
this.walkers = new LlWalkers(this.shapeHost);
this.dyn = new LlDyn(this.shapeHost);
@@ -1119,6 +1134,23 @@ export class LlEmitter {
// whole-program preorder numbering for instanceof over runtime and
// compiled error objects, and the per-hierarchy virtual slot lists.
this.classMeta = buildClassGraph(mod, this.fnByName);
if (mod.lifetimeRegions === true) {
this.regions = new LifetimeRegions(
this.shapeHost,
mod,
this.classMeta,
(meta) => programLifetimeClass(this.shapeHost, meta),
this.nullableFields,
);
const regions = this.regions;
// Member names differ from LifetimeRegions' own, which keeps the
// record from reading as that class's protocol.
this.shapeHost.regions = {
rcBody: (fnName, ret, comment) => regions.countBody(fnName, ret, comment),
newAlloc: (className, sizeOf) => regions.allocLines(className, sizeOf),
classDefinitions: (cls, sizeOf) => regions.classDefs(cls, sizeOf),
};
}
for (const [name, rec] of RUNTIME_ERROR_CLASSES) {
const meta = this.classMeta.get(name);
if (!meta) break; // hand-written IR without the builtin defs: no stamps
@@ -1872,6 +1904,7 @@ export class LlEmitter {
...emitClassMembershipHelper(this.classMeta, this.sizeType),
...(this.publishWalkersCache?.defs() ?? []),
...this.publishedClassNamesDefs(),
...(this.regions?.moduleDefs() ?? []),
];
if (this.constantNumericTables.size > 0)
this.declare(`declare double @scr_arr_get_number(ptr, double)`);
@@ -8022,9 +8055,49 @@ export class LlEmitter {
emitIntrinsicExpr(e: ExprOf<"intrinsic">): LlValue {
if (e.name === "threads.publish") return this.emitPublish(e);
if (
e.name === "region.create" ||
e.name === "region.enter" ||
e.name === "region.leave" ||
e.name === "region.release"
)
return this.emitRegionIntrinsic(e);
return emitIntrinsicExpr(this, e);
}
/** Lifetime regions in modules with lifetimeRegions (llvm/regions.ts).
* Created with the class graph; null elsewhere. */
private regions: LifetimeRegions | null = null;
/** LifetimeRegion's runtime calls: create answers the region id, enter
* and leave bracket run(), release condemns. */
private emitRegionIntrinsic(e: ExprOf<"intrinsic">): LlValue {
if (this.regions === null)
throw new InternalCompilerError(`llvm emitter bug: ${e.name} without lifetimeRegions`);
const args = e.args.map((a) => this.emitExpr(a));
if (this.B.isTerminated()) return { name: "", type: e.type };
switch (e.name) {
case "region.create": {
this.declare(`declare double @scr_region_create(ptr)`);
const id = this.B.tmp();
this.B.line(`${id} = call double @scr_region_create(ptr ${REGION_TYPES})`);
return { name: id, type: e.type };
}
case "region.enter":
this.declare(`declare void @scr_region_enter(double)`);
this.B.line(`call void @scr_region_enter(double ${args[0]!.name})`);
break;
case "region.leave":
this.declare(`declare void @scr_region_leave()`);
this.B.line(`call void @scr_region_leave()`);
break;
default:
this.declare(`declare void @scr_region_release(double)`);
this.B.line(`call void @scr_region_release(double ${args[0]!.name})`);
}
return { name: "", type: e.type };
}
private publishWalkersCache: PublishWalkers | null = null;
/** Per-type publication walkers, created on the first publish. */
@@ -0,0 +1,206 @@
import { expect, test } from "vitest";
import { F64, STRING, VOID, type IrExpr, type IrModule, type IrType } from "../../ir/ir.js";
import { validateModule } from "../../ir/validate.js";
import { emitLlvmModule } from "./emitter.js";
import {
mangleClassNew,
mangleClassRelease,
mangleClassRetain,
mangleClassStruct,
mangleRecordRelease,
mangleRecordStruct,
} from "../mangle.js";
const loc = { file: "regions.ts", start: 0, end: 1 };
const node: IrType = { kind: "object", className: "Node" };
const leaf: IrType = { kind: "object", className: "Leaf" };
const owner: IrType = { kind: "object", className: "Owner" };
const tagged: IrType = { kind: "object", className: "Tagged" };
const span: IrType = { kind: "record", shapeId: "span" };
/** A ProgramLifetime hierarchy (Node, Leaf), an ordinary hierarchy that
* holds instances of it (Owner, Tagged), a record that holds one, and a
* class that cannot reach any (Label). */
function module(body: IrExpr[] = [], lifetimeRegions = true): IrModule {
return {
irVersion: 16,
sourceFile: loc.file,
entry: "main",
...(lifetimeRegions ? { lifetimeRegions: true as const } : {}),
functions: [
{
name: "main",
params: [],
returnType: VOID,
locals: [],
body: body.map((expr) => ({ kind: "exprStmt" as const, expr, loc })),
loc,
},
],
classes: [
{
name: "Node",
fields: [
{ name: "parent", type: node },
{ name: "kids", type: { kind: "array", elem: node } },
{ name: "name", type: STRING },
{ name: "span", type: span },
],
methods: [],
loc,
},
{
name: "Leaf",
base: "Node",
fields: [
{ name: "parent", type: node },
{ name: "kids", type: { kind: "array", elem: node } },
{ name: "name", type: STRING },
{ name: "span", type: span },
{ name: "value", type: F64 },
],
methods: [],
loc,
},
{ name: "Owner", fields: [{ name: "label", type: STRING }], methods: [], loc },
{
name: "Tagged",
base: "Owner",
fields: [
{ name: "label", type: STRING },
{ name: "target", type: leaf },
],
methods: [],
loc,
},
{ name: "Label", fields: [{ name: "text", type: STRING }], methods: [], loc },
{ name: "Holder", fields: [{ name: "owner", type: owner }], methods: [], loc },
],
records: [
{
id: "span",
fields: [
{ name: "end", type: F64 },
{ name: "owner", type: tagged },
],
},
],
contracts: { programLifetime: ["Node"] },
};
}
function body(llvm: string, name: string): string {
return (
new RegExp(`^define internal [a-z]+ @${name.replace(/[.$]/g, "\\$&")}\\([^]*?^}`, "m").exec(
llvm,
)?.[0] ?? ""
);
}
test("programs without regions keep ProgramLifetime's empty counts and emit no region code", () => {
const llvm = emitLlvmModule(module([], false), { pointerBits: 64 });
expect(llvm).not.toMatch(/scr_region|sc_rgn_types|\.rcur|\.rcls|\.rvisit|\.rwalk|\.rown/);
expect(body(llvm, mangleClassRetain("Node"))).toMatch(/entry:\n {2}ret ptr %o\n}/);
expect(body(llvm, mangleClassRelease("Leaf"))).toMatch(/entry:\n {2}ret void\n}/);
});
test("ProgramLifetime instances live in the current region and count in their chunk", () => {
const llvm = emitLlvmModule(module(), { pointerBits: 64 });
for (const cls of ["Node", "Leaf"]) {
const struct = mangleClassStruct(cls);
// new: the cursor fast path, the chunk count of the first reference,
// the runtime's slow path, and the program-lifetime count publication
// still reads.
const nw = body(llvm, mangleClassNew(cls));
expect(nw, cls).toContain(`load ptr, ptr @${struct}.rcur`);
expect(nw, cls).toContain(
`call ptr @scr_region_alloc(ptr @${struct}.rcur, ptr @${struct}.rcls)`,
);
expect(nw, cls).toMatch(/and i64 %rgn\.i, 2097151/);
expect(nw, cls).toContain("store i64 -9223372036854775808, ptr %o");
expect(nw, cls).not.toContain("scr_rt_calloc");
// retain/release: the chunk count, NULL-tolerant.
expect(body(llvm, mangleClassRetain(cls)), cls).toMatch(/add i64 %n, 1/);
expect(body(llvm, mangleClassRelease(cls)), cls).toMatch(/sub i64 %n, 1/);
expect(body(llvm, mangleClassRelease(cls)), cls).toContain("icmp eq ptr %o, null");
expect(llvm, cls).toMatch(
new RegExp(`@${struct}\\.rcur = internal global \\{ ptr, ptr \\} zeroinitializer`),
);
}
// The visit reports region references by address, ordinary references
// with the release entry point the runtime knows them by; strings cannot
// reach the region and are not reported.
const visit = body(llvm, `${mangleClassStruct("Leaf")}.rvisit`);
expect(visit).toContain("call void @scr_region_ref(ptr %w, ptr %v0) ; parent");
expect(visit).toContain(
"call void @scr_region_slot(ptr %w, ptr %v1, ptr @scr_arr_release_v) ; kids",
);
expect(visit).toContain(
`call void @scr_region_slot(ptr %w, ptr %v3, ptr @${mangleRecordRelease("span")}) ; span`,
);
expect(visit).not.toContain("; name");
// The fields-release releases every reference field, strings included.
const release = body(llvm, `${mangleClassStruct("Leaf")}.rrelease`);
for (const field of ["parent", "kids", "name", "span"]) expect(release).toContain(`; ${field}`);
});
test("walkers cover the ordinary classes and records that can reach a region", () => {
const llvm = emitLlvmModule(module(), { pointerBits: 64 });
// Owner itself holds nothing, but a field typed Owner may hold a Tagged:
// its walker dispatches on the dynamic class.
const owner = body(llvm, `${mangleClassStruct("Owner")}.rwalk`);
expect(owner).toMatch(/switch i64 %pre, label %done \[ i64 \d+, label %c\d+ \]/);
expect(owner).toContain(`call void @${mangleClassStruct("Tagged")}.rown(ptr %o, ptr %w)`);
expect(llvm).not.toContain(`@${mangleClassStruct("Owner")}.rown(`);
expect(body(llvm, `${mangleClassStruct("Tagged")}.rown`)).toContain(
"call void @scr_region_ref(ptr %w, ptr %v1) ; target",
);
expect(body(llvm, `${mangleRecordStruct("span")}.rwalk`)).toContain(
`call void @scr_region_slot(ptr %w, ptr %v1, ptr @${mangleClassRelease("Tagged")}) ; owner`,
);
// Classes that cannot reach the region are not walked.
expect(llvm).not.toContain(`${mangleClassStruct("Label")}.rwalk`);
// The table: region classes by release, then walkers.
const table = /^@sc_rgn_types = internal constant .*$/m.exec(llvm)?.[0] ?? "";
expect(table).toContain(`{ ptr @${mangleClassRelease("Node")}, ptr null }`);
expect(table).toContain(`{ ptr @${mangleClassRelease("Leaf")}, ptr null }`);
expect(table).toContain(
`{ ptr @${mangleClassRelease("Owner")}, ptr @${mangleClassStruct("Owner")}.rwalk }`,
);
expect(table).toContain(
`{ ptr @${mangleClassRelease("Holder")}, ptr @${mangleClassStruct("Holder")}.rown }`,
);
expect(table).toContain(
`{ ptr @${mangleRecordRelease("span")}, ptr @${mangleRecordStruct("span")}.rwalk }`,
);
expect(table).not.toContain(mangleClassRelease("Label"));
expect(table).toMatch(/\{ ptr, ptr \} zeroinitializer \] ; region type table$/);
});
test("worker executables count only their own chunks", () => {
const llvm = emitLlvmModule({ ...module(), workers: true }, { pointerBits: 64 });
const retain = body(llvm, mangleClassRetain("Node"));
expect(retain).toContain("call ptr @llvm.threadlocal.address.p0(ptr @scr_thread_hot)");
expect(retain).toMatch(/icmp eq ptr %owner, %self\n {2}br i1 %mine, label %count, label %done/);
expect(llvm).toMatch(/@sc_o_Node\.rcur = internal thread_local(\(localexec\))? global/);
});
test("LifetimeRegion's intrinsics call the runtime", () => {
const id: IrExpr = { kind: "intrinsic", name: "region.create", args: [], type: F64, loc };
const calls: IrExpr[] = [
{ kind: "intrinsic", name: "region.enter", args: [id], type: VOID, loc },
{ kind: "intrinsic", name: "region.leave", args: [], type: VOID, loc },
{ kind: "intrinsic", name: "region.release", args: [id], type: VOID, loc },
];
expect(validateModule(module(calls))).toEqual([]);
const llvm = emitLlvmModule(module(calls), { pointerBits: 64 });
const main = body(llvm, "main") || llvm;
expect(main).toMatch(/call double @scr_region_create\(ptr @sc_rgn_types\)/);
expect(main).toMatch(/call void @scr_region_enter\(double %\S+\)/);
expect(main).toContain("call void @scr_region_leave()");
expect(main).toMatch(/call void @scr_region_release\(double %\S+\)/);
// Region intrinsics need a module that uses regions.
expect(validateModule(module(calls, false)).map((e) => e.message)).toContain(
"in main: region.enter outside a lifetimeRegions module",
);
});
@@ -0,0 +1,406 @@
/* Counted lifetime regions (@scriptc/experimental LifetimeRegion): the
* emitted half of scr_region.c, for programs that construct a region
* (IrModule.lifetimeRegions).
*
* In such a program every ProgramLifetime instance is allocated in the
* thread's current region: `new` bumps the class's cursor (a pair of
* pointers the runtime refills) and increments the count word at the base
* of the 2 MiB chunk it lands in, and the class's retain and release, which
* ProgramLifetime otherwise leaves empty, adjust that word. Worker
* executables count only their own chunks: a published instance other
* threads read is never counted by them (the runtime pins its region).
*
* The release walk needs three kinds of emitted code, all here:
* - per ProgramLifetime class, a visit that reports every reference field
* and a fields-release that releases them, in its ScrRgnClass descriptor;
* - per ordinary class and record whose instances can hold references to
* ProgramLifetime instances (directly or through what they own), a walker
* that reports its fields; a hierarchy walker dispatches on the dynamic
* class like the publication walkers;
* - the type table that maps each of those types' container release entry
* point (shapes.ts vAdapters) to its walker, or marks a ProgramLifetime
* class. The runtime walks its own containers, closures and dyn values
* through the entry points they store and this table.
* A field is reported only when its static type can reach a ProgramLifetime
* instance; not reporting a field leaves its references unexplained, which
* keeps the region, so the reachability below may over-approximate freely.
*
* Programs that never construct a region emit none of this. */
import type { IrClassDef, IrModule, IrType, IrUnionDef } from "../../ir/ir.js";
import { isRefCounted } from "../../ir/ir.js";
import {
mangleClassRelease,
mangleClassStruct,
mangleRecordRelease,
mangleRecordStruct,
} from "../mangle.js";
import { threadHotAddressLines } from "./alloc.js";
import type { LlClassMeta } from "./classes.js";
import { llvmCommentText } from "./common.js";
import type { NullableRefFields } from "./nullable-fields.js";
import { FN_ATTRS, rcMembers, releaseSym, vAdapters, type ShapeHost } from "./shapes.js";
/** log2 of SCR_RGN_CHUNK (scr_runtime.h). */
const CHUNK_SHIFT = 21;
/** The type table's symbol (scr_region_create's argument). */
export const REGION_TYPES = "@sc_rgn_types";
export interface RegionHost extends ShapeHost {
readonly unionsById: ReadonlyMap<string, IrUnionDef>;
cstr(text: string): string;
}
type Field = { name: string; type: IrType; index: number };
/** Region emission for one module, created only when it uses regions. */
export class LifetimeRegions {
/** class/record key → whether its instances can reach a ProgramLifetime instance. */
private readonly reach = new Map<string, boolean>();
constructor(
private readonly host: RegionHost,
private readonly mod: IrModule,
private readonly metaMap: ReadonlyMap<string, LlClassMeta>,
private readonly lifetime: (meta: LlClassMeta) => boolean,
private readonly nullable: NullableRefFields | undefined,
) {
this.computeReach();
}
private tag(word: "count" | "owner" | "cursor"): string {
return this.host.regionAliasTag?.(word) ?? "";
}
private get chunkMask(): string {
return String((1 << CHUNK_SHIFT) - 1);
}
/** Whether `className` is a ProgramLifetime class. */
private isLifetime(className: string): boolean {
const meta = this.metaMap.get(className);
return meta !== undefined && this.lifetime(meta);
}
private classFields(def: IrClassDef, meta: LlClassMeta): Field[] {
const base = meta.hierarchy ? 2 : 1;
return def.fields.map((f, i) => ({
name: f.name,
type: this.nullable?.storageType(def.name, f.name, f.type) ?? f.type,
index: base + i,
}));
}
/** Can a value of type `t` hold (or own something that holds) a
* reference to a ProgramLifetime instance the walk can explain? */
private reaches(t: IrType): boolean {
const arm = this.host.nullableUnions.of(t)?.arm;
if (arm !== undefined) return this.reaches(arm);
switch (t.kind) {
case "object":
return this.isLifetime(t.className) || this.reach.get(`object:${t.className}`) === true;
case "record":
return this.reach.get(`record:${t.shapeId}`) === true;
case "array":
case "set":
return this.reaches(t.elem);
case "map":
return this.reaches(t.key) || this.reaches(t.value);
case "union": {
const def = this.host.unionsById.get(t.unionId);
return def !== undefined && def.arms.some((a) => this.reaches(a));
}
// Closures capture anything, and dyn values box class instances.
case "func":
case "dyn":
return true;
default:
return false;
}
}
/** The least fixpoint of `reach` over the module's classes and records.
* A class's own walker reports its (flattened) fields, so it is emitted
* when one of them reaches (`own:`); a field of a hierarchy type holds
* any class of its subtree, so the type reaches when any of them does.
* Runtime-rooted classes keep runtime state the walk does not know, so
* they are never walked. */
private computeReach(): void {
const classes = (this.mod.classes ?? []).filter((c) => !c.runtime);
for (let changed = true; changed;) {
changed = false;
const set = (key: string, value: boolean): void => {
if (value && this.reach.get(key) !== true) {
this.reach.set(key, true);
changed = true;
}
};
for (const shape of this.mod.records ?? [])
set(
`record:${shape.id}`,
rcMembers(shape).some((m) => this.reaches(m.type)),
);
for (const c of classes) {
const meta = this.metaMap.get(c.name)!;
if (this.lifetime(meta) || meta.root.def.runtime === true) continue;
set(
`own:${c.name}`,
this.classFields(c, meta).some((f) => this.reaches(f.type)),
);
}
for (const c of classes) {
const meta = this.metaMap.get(c.name)!;
if (this.lifetime(meta) || meta.root.def.runtime === true) continue;
set(
`object:${c.name}`,
this.subtree(meta).some((m) => this.reach.get(`own:${m.def.name}`) === true),
);
}
}
}
private subtree(meta: LlClassMeta): LlClassMeta[] {
if (!meta.hierarchy) return [meta];
return [...this.metaMap.values()]
.filter((m) => m.pre >= meta.pre && m.pre <= meta.post && m.def.runtime !== true)
.sort((a, b) => a.pre - b.pre);
}
private cursor(className: string): string {
return `@${mangleClassStruct(className)}.rcur`;
}
private descriptor(className: string): string {
return `@${mangleClassStruct(className)}.rcls`;
}
/** A class's own-fields walker and the walker of its type. */
private ownWalker(className: string): string {
return `@${mangleClassStruct(className)}.rown`;
}
private classWalker(className: string): string {
return `@${mangleClassStruct(className)}.rwalk`;
}
/** The chunk base of `obj` into `%<p>.cp` (a pointer to its count word). */
private chunkLines(p: string, obj: string): string[] {
const S = this.host.sizeType;
return [
` %${p}.i = ptrtoint ptr ${obj} to ${S}`,
` %${p}.off = and ${S} %${p}.i, ${this.chunkMask}`,
` %${p}.neg = sub ${S} 0, %${p}.off`,
` %${p}.cp = getelementptr i8, ptr ${obj}, ${S} %${p}.neg`,
];
}
private countType(): string {
return "i64";
}
/** `new`'s allocation for a ProgramLifetime class: the zeroed instance in
* `%o`, its first reference counted. */
allocLines(className: string, sizeOf: string): string[] {
const S = this.host.sizeType;
const cur = this.cursor(className);
const tag = this.tag("cursor");
const count = this.tag("count");
this.host.declare(`declare ptr @scr_region_alloc(ptr, ptr)`);
this.host.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
return [
` %rgn.sz7 = add ${S} ${sizeOf}, 7`,
` %rgn.sz = and ${S} %rgn.sz7, -8`,
` %rgn.lp = getelementptr inbounds { ptr, ptr }, ptr ${cur}, i32 0, i32 1`,
` %rgn.p = load ptr, ptr ${cur}${tag}`,
` %rgn.l = load ptr, ptr %rgn.lp${tag}`,
` %rgn.np = getelementptr i8, ptr %rgn.p, ${S} %rgn.sz`,
` %rgn.fit0 = icmp ule ptr %rgn.np, %rgn.l`,
` %rgn.fit = call i1 @llvm.expect.i1(i1 %rgn.fit0, i1 true)`,
` br i1 %rgn.fit, label %rgn.fast, label %rgn.slow`,
`rgn.fast:`,
` store ptr %rgn.np, ptr ${cur}${tag}`,
...this.chunkLines("rgn", "%rgn.p"),
` %rgn.c = load ${this.countType()}, ptr %rgn.cp${count}`,
` %rgn.c1 = add ${this.countType()} %rgn.c, 1`,
` store ${this.countType()} %rgn.c1, ptr %rgn.cp${count}`,
` br label %rgn.done`,
`rgn.slow:`,
` %rgn.s = call ptr @scr_region_alloc(ptr ${cur}, ptr ${this.descriptor(className)})`,
` br label %rgn.done`,
`rgn.done:`,
` %o = phi ptr [ %rgn.p, %rgn.fast ], [ %rgn.s, %rgn.slow ]`,
];
}
/** The retain (`add`) or release (`sub`) body of a ProgramLifetime class:
* the count of the chunk holding the instance. */
countBody(fnName: string, ret: "ptr" | "void", comment: string): string[] {
const count = this.tag("count");
const own = this.host.threadLocalAlloc === true;
const lines = [
`define internal ${ret} @${fnName}(ptr %o) ${FN_ATTRS} {${comment ? ` ; ${comment} (region count)` : ""}`,
`entry:`,
` %isnull = icmp eq ptr %o, null`,
` br i1 %isnull, label %done, label %chunk`,
`chunk:`,
...this.chunkLines("c", "%o"),
];
if (own) {
// Another thread's chunk: a published instance, never counted here.
lines.push(
...threadHotAddressLines(this.host, "%self"),
` %ownp = getelementptr inbounds i8, ptr %c.cp, i64 8`,
` %owner = load ptr, ptr %ownp${this.tag("owner")}`,
` %mine = icmp eq ptr %owner, %self`,
` br i1 %mine, label %count, label %done`,
`count:`,
);
}
lines.push(
` %n = load ${this.countType()}, ptr %c.cp${count}`,
` %n1 = ${ret === "ptr" ? "add" : "sub"} ${this.countType()} %n, 1`,
` store ${this.countType()} %n1, ptr %c.cp${count}`,
` br label %done`,
`done:`,
ret === "ptr" ? ` ret ptr %o` : ` ret void`,
`}`,
);
return lines;
}
/** One field's report to the walk (none when it cannot reach a
* ProgramLifetime instance). */
private reportLines(struct: string, f: Field, i: number): string[] {
if (!isRefCounted(f.type) || !this.reaches(f.type)) return [];
const arm = this.host.nullableUnions.of(f.type)?.arm ?? f.type;
const load = [
` %f${i} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`,
` %v${i} = load ptr, ptr %f${i}`,
];
if (arm.kind === "object" && this.isLifetime(arm.className)) {
this.host.declare(`declare void @scr_region_ref(ptr, ptr)`);
return [
...load,
` call void @scr_region_ref(ptr %w, ptr %v${i}) ; ${llvmCommentText(f.name)}`,
];
}
this.host.declare(`declare void @scr_region_slot(ptr, ptr, ptr)`);
return [
...load,
` call void @scr_region_slot(ptr %w, ptr %v${i}, ptr ${vAdapters(this.host, f.type).release}) ; ${llvmCommentText(f.name)}`,
];
}
private reportBody(head: string, struct: string, fields: Field[]): string[] {
const lines = [head, `entry:`];
fields.forEach((f, i) => lines.push(...this.reportLines(struct, f, i)));
lines.push(` ret void`, `}`, ``);
return lines;
}
/** The cursor, descriptor, visit and fields-release of a ProgramLifetime
* class. */
classDefs(cls: IrClassDef, sizeOf: string): string[] {
const meta = this.metaMap.get(cls.name)!;
const struct = mangleClassStruct(cls.name);
const fields = this.classFields(cls, meta);
const visit = `@${struct}.rvisit`;
const release = `@${struct}.rrelease`;
const tl = this.host.threadLocalAlloc === true ? "thread_local " : "";
const rel = [
`define internal void ${release}(ptr %o) ${FN_ATTRS} { ; region fields release ${llvmCommentText(cls.name)}`,
`entry:`,
];
fields
.filter((f) => isRefCounted(f.type))
.forEach((f, i) =>
rel.push(
` %f${i} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`,
` %v${i} = load ptr, ptr %f${i}`,
` call void ${releaseSym(this.host, f.type)}(ptr %v${i}) ; ${llvmCommentText(f.name)}`,
),
);
rel.push(` ret void`, `}`, ``);
const S = this.host.sizeType;
return [
`${this.cursor(cls.name)} = internal ${tl}global { ptr, ptr } zeroinitializer`,
`${this.descriptor(cls.name)} = internal global { ${S}, ptr, ptr, ptr, i32 } { ${S} ${sizeOf}, ptr ${visit}, ptr ${release}, ptr ${this.host.cstr(cls.name.replace(/^%(m\d+\.)?/, ""))}, i32 -1 } ; region class ${llvmCommentText(cls.name)}`,
...this.reportBody(
`define internal void ${visit}(ptr %o, ptr %w) ${FN_ATTRS} { ; region visit ${llvmCommentText(cls.name)}`,
struct,
fields,
),
...rel,
];
}
/** The walkers of the ordinary classes and records that can reach a
* ProgramLifetime instance, and the type table. */
moduleDefs(): string[] {
const out: string[] = [];
const rows: string[] = [];
const classes = (this.mod.classes ?? []).filter((c) => c.runtime !== true);
for (const cls of classes) {
if (this.reach.get(`own:${cls.name}`) !== true) continue;
out.push(
...this.reportBody(
`define internal void ${this.ownWalker(cls.name)}(ptr %o, ptr %w) ${FN_ATTRS} { ; region walk ${llvmCommentText(cls.name)}`,
mangleClassStruct(cls.name),
this.classFields(cls, this.metaMap.get(cls.name)!),
),
);
}
for (const cls of classes) {
const meta = this.metaMap.get(cls.name)!;
if (this.lifetime(meta)) {
rows.push(`{ ptr, ptr } { ptr @${mangleClassRelease(cls.name)}, ptr null }`);
continue;
}
if (this.reach.get(`object:${cls.name}`) !== true) continue;
let walk = this.ownWalker(cls.name);
if (meta.hierarchy) {
// A field of this type holds any class of its subtree.
const S = this.host.sizeType;
const members = this.subtree(meta).filter(
(m) => this.reach.get(`own:${m.def.name}`) === true,
);
walk = this.classWalker(cls.name);
out.push(
`define internal void ${walk}(ptr %o, ptr %w) ${FN_ATTRS} { ; region walk ${llvmCommentText(cls.name)} and its subclasses`,
`entry:`,
` %vtp = getelementptr inbounds ptr, ptr %o, i64 1`,
` %vt = load ptr, ptr %vtp`,
` %pre = load ${S}, ptr %vt`,
` switch ${S} %pre, label %done [ ${members.map((m) => `${S} ${m.pre}, label %c${m.pre}`).join(" ")} ]`,
...members.flatMap((m) => [
`c${m.pre}: ; ${llvmCommentText(m.def.name)}`,
` call void ${this.ownWalker(m.def.name)}(ptr %o, ptr %w)`,
` br label %done`,
]),
`done:`,
` ret void`,
`}`,
``,
);
}
rows.push(`{ ptr, ptr } { ptr @${mangleClassRelease(cls.name)}, ptr ${walk} }`);
}
for (const shape of this.mod.records ?? []) {
if (this.reach.get(`record:${shape.id}`) !== true) continue;
const walk = `@${mangleRecordStruct(shape.id)}.rwalk`;
out.push(
...this.reportBody(
`define internal void ${walk}(ptr %o, ptr %w) ${FN_ATTRS} { ; region walk record ${shape.id}`,
mangleRecordStruct(shape.id),
rcMembers(shape),
),
);
rows.push(`{ ptr, ptr } { ptr @${mangleRecordRelease(shape.id)}, ptr ${walk} }`);
}
rows.push(`{ ptr, ptr } zeroinitializer`);
out.push(
`${REGION_TYPES} = internal constant [${rows.length} x { ptr, ptr }] [ ${rows.join(", ")} ] ; region type table`,
``,
);
return out;
}
}
+31 -6
View File
@@ -2,7 +2,7 @@
* Containers store erased adapters; shape helpers retain, release and trace
* their fields. Every allocation has a size_t reference count at offset zero
* and participates in the runtime allocation audit. */
import type { IrModule, IrRecordShape, IrType } from "../../ir/ir.js";
import type { IrClassDef, IrModule, IrRecordShape, IrType } from "../../ir/ir.js";
import {
isIdentityCollectionKey,
isRefCounted,
@@ -48,6 +48,14 @@ export interface ShapeHost {
readonly nullableUnions: NullableUnions;
readonly recordsById: Map<string, IrRecordShape>;
readonly recordCloneShapes: ReadonlySet<string>;
/** The program constructs lifetime regions: the ProgramLifetime classes
* allocate in regions and count references in their region chunk instead
* of being empty (llvm/regions.ts). */
regions?: RegionHooks | null;
/** The alias tag of a runtime word that lifetime-region code reads and
* writes outside every field: the chunk count, the chunk owner and the
* class cursors (", !tbaa !N", or "" in debug builds). */
regionAliasTag?(word: "count" | "owner" | "cursor"): string;
/** Emit the live-object audit notes (`scr_obj_alloc_note` /
* `scr_obj_free_note`) in emitted new/free helpers. Only runtimes built
* with SCR_RC_AUDIT (the sanitized lane) count them; release and dev
@@ -70,6 +78,15 @@ export interface ShapeHost {
nextCycKind?(): number;
}
/** What class emission asks of lifetime regions (LifetimeRegions in
* llvm/regions.ts): the retain and release bodies, the allocation lines of
* `new` (the instance in `%o`), and a class's region definitions. */
export interface RegionHooks {
rcBody(fnName: string, ret: "ptr" | "void", comment: string): string[];
newAlloc(className: string, sizeOf: string): string[];
classDefinitions(cls: IrClassDef, sizeOf: string): string[];
}
/** Every emitted function/helper carries #0 = { sanitize_address } — see
* the emitter header. */
export const FN_ATTRS = "#0";
@@ -712,7 +729,7 @@ export function mapValKindNum(value: IrType): number {
/** The RC-relevant members of a shape: every field, plus the overflow map
* on index-signature shapes (one more map-typed member). `index` is the
* member's field position in the emitted struct type (rc header at 0). */
function rcMembers(shape: IrRecordShape): { index: number; type: IrType; name: string }[] {
export function rcMembers(shape: IrRecordShape): { index: number; type: IrType; name: string }[] {
return [
...shape.fields.map((f, i) => ({ index: i + 1, type: f.type, name: f.name })),
...(shape.indexValue
@@ -737,7 +754,7 @@ export function retainBody(
immortal = false,
): string[] {
const S = host.sizeType;
if (immortal) return immortalRcBody(fnName, "ptr", comment);
if (immortal) return immortalRcBody(host, fnName, "ptr", comment);
return [
`define internal ptr @${fnName}(ptr %o) ${FN_ATTRS} {${comment ? ` ; ${comment}` : ""}`,
`entry:`,
@@ -760,8 +777,16 @@ export function retainBody(
/** The retain or release entry point of a class whose every instance lives
* until the process exits (no collector header; see classes.ts
* programLifetimeClass and programLifetimeRc): nothing to count, so every
* inlined call site folds away. */
function immortalRcBody(fnName: string, ret: "ptr" | "void", comment: string): string[] {
* inlined call site folds away. Programs with lifetime regions count the
* references in the instance's region chunk instead (llvm/regions.ts). */
function immortalRcBody(
host: ShapeHost,
fnName: string,
ret: "ptr" | "void",
comment: string,
): string[] {
const regions: RegionHooks | null = host.regions ?? null;
if (regions !== null) return regions.rcBody(fnName, ret, comment);
return [
`define internal ${ret} @${fnName}(ptr %o) ${FN_ATTRS} {${comment ? ` ; ${comment} (immortal)` : ""}`,
`entry:`,
@@ -981,7 +1006,7 @@ export function releaseFastPath(
immortal = false,
): string[] {
const S = host.sizeType;
if (immortal) return immortalRcBody(fnName, "void", comment);
if (immortal) return immortalRcBody(host, fnName, "void", comment);
return [
`define internal void @${fnName}(ptr %o) ${FN_ATTRS} {${comment ? ` ; ${comment} (fast path)` : ""}`,
`entry:`,
+14 -1
View File
@@ -927,6 +927,10 @@ export interface IrModule {
entry: string;
/** Executable roots run in isolated native worker contexts. */
workers?: boolean;
/** The program constructs LifetimeRegion (@scriptc/experimental): every
* ProgramLifetime instance lives in a counted region (the region.*
* intrinsics; backend/llvm/regions.ts). */
lifetimeRegions?: true;
/** The entry module is CommonJS under Node's classification. Node runs a
* CommonJS main synchronously and drains process.nextTick callbacks
* before promise jobs at the first checkpoint; an ES-module main's
@@ -3364,7 +3368,16 @@ export type IrExpr =
| "module.tdzCheck"
/* @scriptc/threads publish(value): one argument, the result is the
* same value (and type); its graph becomes immortal and immutable. */
| "threads.publish";
| "threads.publish"
/* LifetimeRegion (@scriptc/experimental), in modules with
* lifetimeRegions: region.create () → f64, a fresh region id;
* region.enter (f64 id) → void makes it the thread's current
* region until the matching region.leave () → void; region.release
* (f64 id) → void condemns it. None of them throws. */
| "region.create"
| "region.enter"
| "region.leave"
| "region.release";
args: IrExpr[];
type: IrType;
loc: SrcLoc;
+22
View File
@@ -131,6 +131,8 @@ export function validateModule(mod: IrModule): IrValidationError[] {
const moduleLoc: SrcLoc = { file: mod.sourceFile, start: 0, end: 0 };
if (mod.workers !== undefined && typeof mod.workers !== "boolean")
errors.push({ message: "worker execution mode must be a boolean", loc: moduleLoc });
if (mod.lifetimeRegions !== undefined && mod.lifetimeRegions !== true)
errors.push({ message: "lifetimeRegions must be true when present", loc: moduleLoc });
if (mod.workers && mod.lib !== undefined)
errors.push({
message: "worker execution requires a native static executable",
@@ -975,6 +977,7 @@ export function validateModule(mod: IrModule): IrValidationError[] {
classValidation,
errors,
mod.workers === true,
mod.lifetimeRegions === true,
);
}
return errors;
@@ -992,6 +995,7 @@ function validateFunction(
classValidation: ClassValidation,
errors: IrValidationError[],
workers: boolean,
lifetimeRegions: boolean,
): void {
const locals = new Map(fn.locals.map((l) => [l.id, l]));
const err = (message: string, loc: SrcLoc) =>
@@ -3715,6 +3719,24 @@ function validateFunction(
if (e.type.kind !== "void") err("module.tdzCheck must be void", e.loc);
return;
}
if (
e.name === "region.create" ||
e.name === "region.enter" ||
e.name === "region.leave" ||
e.name === "region.release"
) {
if (!lifetimeRegions) err(`${e.name} outside a lifetimeRegions module`, e.loc);
const takesId = e.name === "region.enter" || e.name === "region.release";
if (e.args.length !== (takesId ? 1 : 0))
err(`${e.name} takes ${takesId ? "one region id" : "no arguments"}`, e.loc);
for (const a of e.args) {
checkExpr(a);
if (a.type.kind !== "f64") err(`${e.name} id must be f64`, e.loc);
}
if (e.type.kind !== (e.name === "region.create" ? "f64" : "void"))
err(`${e.name} has type ${e.name === "region.create" ? "f64" : "void"}`, e.loc);
return;
}
if (e.name === "threads.publish") {
if (e.args.length !== 1) err("threads.publish takes exactly one argument", e.loc);
for (const a of e.args) {