From 604a316ec3a71c867b8c1dcdc299542db677466e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 13 Oct 2026 19:18:28 -0500 Subject: [PATCH] Allocate ProgramLifetime classes in counted lifetime regions --- packages/compiler/src/backend/llvm/alloc.ts | 9 + packages/compiler/src/backend/llvm/classes.ts | 26 +- packages/compiler/src/backend/llvm/emitter.ts | 73 ++++ .../compiler/src/backend/llvm/regions.test.ts | 206 +++++++++ packages/compiler/src/backend/llvm/regions.ts | 406 ++++++++++++++++++ packages/compiler/src/backend/llvm/shapes.ts | 37 +- packages/compiler/src/ir/ir.ts | 15 +- packages/compiler/src/ir/validate.ts | 22 + 8 files changed, 778 insertions(+), 16 deletions(-) create mode 100644 packages/compiler/src/backend/llvm/regions.test.ts create mode 100644 packages/compiler/src/backend/llvm/regions.ts diff --git a/packages/compiler/src/backend/llvm/alloc.ts b/packages/compiler/src/backend/llvm/alloc.ts index 6ea2a2b91..08a7e07ae 100644 --- a/packages/compiler/src/backend/llvm/alloc.ts +++ b/packages/compiler/src/backend/llvm/alloc.ts @@ -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()`; diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index fdba121fd..9ae245447 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -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 diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 29bacf1c1..fefe2df3c 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -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. */ diff --git a/packages/compiler/src/backend/llvm/regions.test.ts b/packages/compiler/src/backend/llvm/regions.test.ts new file mode 100644 index 000000000..253b5aac0 --- /dev/null +++ b/packages/compiler/src/backend/llvm/regions.test.ts @@ -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", + ); +}); diff --git a/packages/compiler/src/backend/llvm/regions.ts b/packages/compiler/src/backend/llvm/regions.ts new file mode 100644 index 000000000..adbb90885 --- /dev/null +++ b/packages/compiler/src/backend/llvm/regions.ts @@ -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; + 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(); + + constructor( + private readonly host: RegionHost, + private readonly mod: IrModule, + private readonly metaMap: ReadonlyMap, + 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 `%

.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; + } +} diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index ce54e5290..c1b2b87c8 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -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; readonly recordCloneShapes: ReadonlySet; + /** 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:`, diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 3b4f9623c..f36301aa0 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -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; diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index bb6731471..ad6ec8450 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -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) {