mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Allocate ProgramLifetime classes in counted lifetime regions
This commit is contained in:
@@ -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()`;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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:`,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user