mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Keep the class id of hierarchy instances in the rc word (#1486)
This commit is contained in:
@@ -75,17 +75,25 @@ test("releases split into an inlinable fast path and an out-of-line remainder",
|
||||
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
|
||||
const body = (name: string): string =>
|
||||
new RegExp(`^define internal void @${name}(?:\\.i32)?\\([^]*?^}`, "m").exec(llvm)?.[0] ?? "";
|
||||
// A hierarchy rooted in the program: a surviving release decrements and
|
||||
// takes the possible-root step without loading the vtable.
|
||||
// A hierarchy rooted in the program: a surviving release decrements the
|
||||
// count (the low half of the rc word, which keeps the class id in its high
|
||||
// half) and takes the possible-root step without loading the vtable.
|
||||
const fast = body(mangleClassRelease("Node"));
|
||||
expect(fast).toContain("%shared = icmp ult i64 %rcm2, -3");
|
||||
expect(fast).toContain("%shared = icmp ult i32 %rcm2, -3");
|
||||
expect(fast).toContain("load i32, ptr %bufp");
|
||||
expect(fast).toContain(`call void @${mangleClassRelease("Node")}_slow(ptr %o)`);
|
||||
expect(fast).not.toContain("%ScrVt");
|
||||
// Only the last reference dispatches, from a noinline remainder.
|
||||
const slow = body(`${mangleClassRelease("Node")}_slow`);
|
||||
expect(slow).toMatch(/noinline/);
|
||||
// The class id indexes the hierarchy's vtable array.
|
||||
expect(slow).toContain("%vt.id = load i32, ptr %vt.idp");
|
||||
expect(slow).toContain("ptr @sc_vta_Node, i64 0, i64 %vt.ix64");
|
||||
expect(slow).toContain("getelementptr inbounds %ScrVt, ptr %vt, i64 0, i32 2");
|
||||
// 32-bit targets keep the vtable word.
|
||||
const wasm = emitLlvmModule(mod, { pointerBits: 32 });
|
||||
expect(wasm).toContain("; class Node (vt at 1)");
|
||||
expect(wasm).not.toContain("@sc_vta_Node");
|
||||
});
|
||||
|
||||
test("ProgramLifetime hierarchies are never freed and take no reference counts", () => {
|
||||
@@ -121,9 +129,11 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts",
|
||||
expect(body(mangleClassRetain(cls)), cls).toMatch(/entry:\n {2}ret ptr %o\n}/);
|
||||
expect(body(mangleClassRelease(cls)), cls).toMatch(/entry:\n {2}ret void\n}/);
|
||||
// new: no collector header (no descriptor, no scr_cyc_alloc) and the
|
||||
// program-lifetime count 2^63, not SIZE_MAX (which means published).
|
||||
// program-lifetime count 2^31 under the class id (Node's preorder number
|
||||
// 0 + 1, Leaf's 1 + 1), not SIZE_MAX (which means published).
|
||||
const nw = body(mangleClassNew(cls));
|
||||
expect(nw, cls).toContain("store i64 -9223372036854775808, ptr %o");
|
||||
const id = cls === "Node" ? 1 : 2;
|
||||
expect(nw, cls).toContain(`store i64 ${id * 2 ** 32 + 2 ** 31}, ptr %o.vt`);
|
||||
expect(nw, cls).not.toContain("store i64 -1, ptr %o");
|
||||
expect(nw, cls).not.toContain("scr_cyc_alloc");
|
||||
expect(llvm, cls).not.toContain(`@${mangleClassTrace(cls)}.cyctype`);
|
||||
@@ -206,13 +216,15 @@ test("published ProgramLifetime instances use the published mark every other obj
|
||||
// a refusal restores the program-lifetime count); untraced, so it never
|
||||
// leaves a collector buffer.
|
||||
expect(llvm).toContain("call void @scr_publish(ptr");
|
||||
expect(llvm).toMatch(/@scr_pub_mark\(ptr %ctx, ptr %o, i1 false, i32 4\)/);
|
||||
// The frozen-object guard of the store tests the count against SIZE_MAX:
|
||||
// an unpublished instance (2^63) stays writable.
|
||||
// Kind 4 (an object) | 0x100 (the rc word keeps a class id: the mark
|
||||
// writes only the count).
|
||||
expect(llvm).toMatch(/@scr_pub_mark\(ptr %ctx, ptr %o, i1 false, i32 260\)/);
|
||||
// The frozen-object guard of the store tests the count against UINT32_MAX:
|
||||
// an unpublished instance (2^31) stays writable.
|
||||
const store =
|
||||
llvm.split(/^define /m).find((fn) => fn.includes("call void @scr_throw_published_field(")) ??
|
||||
"";
|
||||
expect(store).toMatch(/icmp eq i64 %\S+, -1/);
|
||||
expect(store).toMatch(/icmp eq i32 %\S+, -1/);
|
||||
expect(store).toContain("call void @scr_publish(ptr");
|
||||
// 32-bit targets use 2^31.
|
||||
const llvm32 = emitLlvmModule(mod, { pointerBits: 32 });
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
mangleCtorThunk,
|
||||
mangleFunction,
|
||||
mangleGlobal,
|
||||
mangleVtArray,
|
||||
mangleVtInstance,
|
||||
mangleVtStruct,
|
||||
} from "../mangle.js";
|
||||
@@ -81,6 +82,12 @@ export class LlClassMeta {
|
||||
* member's struct carries the i32 slot word before its fields
|
||||
* (classSlotWordIndex). */
|
||||
threadSlot = false;
|
||||
/** A member of a program-rooted hierarchy on a 64-bit target: instead of
|
||||
* a vtable word, the rc word holds the class id (classIdRcWord) in its
|
||||
* high 32 bits (CLASS_ID_OFFSET) and the reference count in its low 32
|
||||
* bits, and the hierarchy's vtables are one array indexed by the id
|
||||
* (mangleVtArray). */
|
||||
idInRc = false;
|
||||
|
||||
constructor(readonly def: IrClassDef) {
|
||||
this.root = this;
|
||||
@@ -96,6 +103,7 @@ export class LlClassMeta {
|
||||
export function buildClassGraph(
|
||||
mod: IrModule,
|
||||
fnByName: Map<string, IrFunction>,
|
||||
sizeType: "i32" | "i64" = "i64",
|
||||
): Map<string, LlClassMeta> {
|
||||
const metaMap = new Map<string, LlClassMeta>();
|
||||
for (const cls of mod.classes ?? []) {
|
||||
@@ -122,6 +130,11 @@ export function buildClassGraph(
|
||||
if (meta.base === null) number(meta, meta);
|
||||
meta.hierarchy = meta.base !== null || meta.children.length > 0;
|
||||
}
|
||||
// The runtime reads the vtable word of runtime-rooted hierarchies (Error,
|
||||
// EventEmitter, EventTarget, Event and the streams) and 32-bit targets
|
||||
// have no spare half in the rc word: those keep the vtable word.
|
||||
for (const meta of metaMap.values())
|
||||
meta.idInRc = sizeType === "i64" && meta.hierarchy && meta.root.def.runtime !== true;
|
||||
const slotRoots = new Set<LlClassMeta>();
|
||||
for (const key of mod.threadSlotKeys ?? []) {
|
||||
const meta = metaMap.get(key);
|
||||
@@ -336,11 +349,12 @@ function runtimePrefixOf(meta: LlClassMeta): RuntimePrefix | null {
|
||||
}
|
||||
|
||||
/** The GEP index after a class's header words: rc at 0, the vtable word
|
||||
* at 1 on hierarchy members, then a runtime prefix (state + display name)
|
||||
* at 1 on hierarchy members that do not keep their class id in the rc
|
||||
* word (LlClassMeta.idInRc), then a runtime prefix (state + display name)
|
||||
* on classes rooted at a runtime base, then the stream-state slot on
|
||||
* stream-rooted ones. */
|
||||
function headerEnd(meta: LlClassMeta): number {
|
||||
if (!meta.hierarchy) return 1;
|
||||
if (!meta.hierarchy || meta.idInRc) return 1;
|
||||
if (streamRooted(meta)) return 5;
|
||||
return runtimePrefixOf(meta) ? 4 : 2;
|
||||
}
|
||||
@@ -445,6 +459,69 @@ export function programLifetimeRc(sizeType: "i32" | "i64"): string {
|
||||
return sizeType === "i32" ? "-2147483648" : "-9223372036854775808";
|
||||
}
|
||||
|
||||
/** Lines that load the preorder number of the dynamic class of `obj`, an
|
||||
* instance of `meta`'s hierarchy whose struct is `struct`, into `pre` (a
|
||||
* `sizeType`): from the high half of the rc word (LlClassMeta.idInRc), or
|
||||
* from the vtable its vtable word points at. */
|
||||
export function classPreLines(
|
||||
meta: LlClassMeta,
|
||||
struct: string,
|
||||
obj: string,
|
||||
pre: string,
|
||||
sizeType: "i32" | "i64",
|
||||
): string[] {
|
||||
if (meta.idInRc)
|
||||
return [
|
||||
` ${pre}.idp = getelementptr inbounds i8, ptr ${obj}, i64 ${CLASS_ID_OFFSET}`,
|
||||
` ${pre}.id = load i32, ptr ${pre}.idp`,
|
||||
` ${pre}.n = add i32 ${pre}.id, -1 ; the class id is the preorder number + 1`,
|
||||
` ${pre} = zext i32 ${pre}.n to ${sizeType}`,
|
||||
];
|
||||
return [
|
||||
` ${pre}.vtp = getelementptr inbounds %${struct}, ptr ${obj}, i64 0, i32 1`,
|
||||
` ${pre}.vt = load ptr, ptr ${pre}.vtp`,
|
||||
` ${pre}.prep = getelementptr inbounds %ScrVt, ptr ${pre}.vt, i64 0, i32 0`,
|
||||
` ${pre} = load ${sizeType}, ptr ${pre}.prep`,
|
||||
];
|
||||
}
|
||||
|
||||
/** Lines that load the class id of the LlClassMeta.idInRc instance `obj`
|
||||
* (`group` adds !invariant.group to the load) and point `vt` at its class's
|
||||
* element of the hierarchy's vtable array. */
|
||||
export function classIdVtLines(
|
||||
meta: LlClassMeta,
|
||||
obj: string,
|
||||
vt: string,
|
||||
group: string,
|
||||
): string[] {
|
||||
const t = `${vt}.`;
|
||||
const root = meta.root;
|
||||
const vtt = `%${mangleVtStruct(root.def.name)}`;
|
||||
const n = root.post - root.pre + 1;
|
||||
return [
|
||||
` ${t}idp = getelementptr inbounds i8, ptr ${obj}, i64 ${CLASS_ID_OFFSET}`,
|
||||
` ${t}id = load i32, ptr ${t}idp${group}`,
|
||||
` ${t}ix = sub i32 ${t}id, ${root.pre + 1}`,
|
||||
` ${t}ix64 = zext i32 ${t}ix to i64`,
|
||||
` ${vt} = getelementptr inbounds [${n} x ${vtt}], ptr @${mangleVtArray(root.def.name)}, i64 0, i64 ${t}ix64`,
|
||||
];
|
||||
}
|
||||
|
||||
/** The byte offset of the class id in the rc word of an instance that
|
||||
* keeps it there (LlClassMeta.idInRc): the high half of the little-endian
|
||||
* 64-bit word. The count is the i32 at offset 0. */
|
||||
export const CLASS_ID_OFFSET = 4;
|
||||
|
||||
/** The rc word an LlClassMeta.idInRc instance is created with: the class
|
||||
* id, its preorder number plus 1, in the high 32 bits, and in the low 32 bits
|
||||
* a count of 1, or for a program-lifetime instance the midpoint of the 32-bit
|
||||
* count (as programLifetimeRc is of the 32-bit word). The id is never 0,
|
||||
* 0x80000000 or 0xffffffff, the high halves of other objects' rc words, so
|
||||
* the runtime can tell an object with a class id (scr_rc.h scr_rc_class_id). */
|
||||
export function classIdRcWord(meta: LlClassMeta, lifetime: boolean): string {
|
||||
return (BigInt(meta.pre + 1) * 2n ** 32n + (lifetime ? 2n ** 31n : 1n)).toString();
|
||||
}
|
||||
|
||||
function undefFieldInits(
|
||||
host: ClassHost,
|
||||
meta: LlClassMeta,
|
||||
@@ -524,17 +601,18 @@ export function emitClassShapes(
|
||||
// ScrEmitter* / ScrStream* / ScrEventTargetObj* / ScrEventObj* are the
|
||||
// usual pointer reinterprets.
|
||||
const runtimePrefix = runtimePrefixOf(meta);
|
||||
const prefix = meta.hierarchy
|
||||
? streamRooted(meta)
|
||||
? ["ptr", "ptr", "ptr", "ptr"]
|
||||
: runtimePrefix
|
||||
? ["ptr", "ptr", "ptr"]
|
||||
: ["ptr"]
|
||||
: [];
|
||||
const prefix =
|
||||
meta.hierarchy && !meta.idInRc
|
||||
? streamRooted(meta)
|
||||
? ["ptr", "ptr", "ptr", "ptr"]
|
||||
: runtimePrefix
|
||||
? ["ptr", "ptr", "ptr"]
|
||||
: ["ptr"]
|
||||
: [];
|
||||
const members = [...prefix, ...(meta.threadSlot ? ["i32"] : []), ...fieldTys];
|
||||
typeDefs.push(
|
||||
`%${mangleClassStruct(cls.name)} = type { ${host.sizeType}${members.length ? ", " + members.join(", ") : ""} } ` +
|
||||
`; class ${cls.name}${meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : runtimePrefix ? ` (${runtimePrefix.struct} prefix at 2)` : ""} { ${cls.fields.map((f) => llvmCommentText(f.name)).join("; ")} }`,
|
||||
`; class ${cls.name}${meta.idInRc ? " (class id in rc)" : meta.hierarchy ? " (vt at 1)" : ""}${streamRooted(meta) ? " (ScrStream prefix at 2)" : runtimePrefix ? ` (${runtimePrefix.struct} prefix at 2)` : ""} { ${cls.fields.map((f) => llvmCommentText(f.name)).join("; ")} }`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -559,18 +637,40 @@ export function emitClassShapes(
|
||||
// One constant vtable instance per emitted hierarchy class: interval,
|
||||
// direct release, and the class's dispatch entry for every slot — the
|
||||
// method functions THEMSELVES (no adapters; see the header comment).
|
||||
for (const cls of emitted) {
|
||||
const meta = metaMap.get(cls.name)!;
|
||||
if (!meta.hierarchy) continue;
|
||||
const vtInit = (meta: LlClassMeta): string => {
|
||||
const entries = vtEntriesFor(meta).map(({ slot, impl }) =>
|
||||
impl === null
|
||||
? `ptr null` // outside the declaring subtree / fully-abstract chain
|
||||
: `ptr @${host.virtualEntry?.(`%${impl.def.name}.${slot.method}`) ?? mangleFunction(`%${impl.def.name}.${slot.method}`)}`,
|
||||
);
|
||||
const head = `%ScrVt { ${host.sizeType} ${meta.pre}, ${host.sizeType} ${meta.post}, ptr @${mangleClassReleaseDirect(cls.name)} }`;
|
||||
const head = `%ScrVt { ${host.sizeType} ${meta.pre}, ${host.sizeType} ${meta.post}, ptr @${mangleClassReleaseDirect(meta.def.name)} }`;
|
||||
return `{ ${[head, ...entries].join(", ")} }`;
|
||||
};
|
||||
for (const cls of emitted) {
|
||||
const meta = metaMap.get(cls.name)!;
|
||||
if (!meta.hierarchy || meta.idInRc) continue;
|
||||
defs.push(
|
||||
`@${mangleVtInstance(cls.name)} = internal constant %${mangleVtStruct(meta.root.def.name)} ` +
|
||||
`{ ${[head, ...entries].join(", ")} } ; class ${cls.name}`,
|
||||
`${vtInit(meta)} ; class ${cls.name}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
// A hierarchy whose instances keep their class id in the rc word has
|
||||
// one array of its classes' vtables, in preorder: the vtable of an
|
||||
// instance is the element at its number minus the root's.
|
||||
for (const name of roots) {
|
||||
const root = metaMap.get(name)!;
|
||||
if (!root.idInRc) continue;
|
||||
const members = [...metaMap.values()]
|
||||
.filter((m) => m.root === root)
|
||||
.sort((a, b) => a.pre - b.pre);
|
||||
const vtt = `%${mangleVtStruct(root.def.name)}`;
|
||||
defs.push(
|
||||
`@${mangleVtArray(root.def.name)} = internal constant [${members.length} x ${vtt}] [`,
|
||||
...members.map(
|
||||
(m, i) => ` ${vtt} ${vtInit(m)}${i + 1 < members.length ? "," : ""} ; class ${m.def.name}`,
|
||||
),
|
||||
`]`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
@@ -605,6 +705,7 @@ export function emitClassShapes(
|
||||
release: `@${release}`,
|
||||
destroy: `${teardown}_destroy`,
|
||||
traced: host.tracedShapes.has(`object:${cls.name}`),
|
||||
...(meta.idInRc ? { count: "i32" as const } : {}),
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -658,7 +759,13 @@ export function emitClassShapes(
|
||||
|
||||
// retain: NULL-tolerant, immortal-skip — layout-generic (rc at 0), so
|
||||
// hierarchy members need no dispatch.
|
||||
defs.push(...retainBody(host, mangleClassRetain(cls.name), `retain ${cls.name}`, lifetime), ``);
|
||||
// An instance that keeps its class id in the rc word counts in the
|
||||
// low half (CLASS_ID_OFFSET).
|
||||
const count = meta.idInRc ? "i32" : host.sizeType;
|
||||
defs.push(
|
||||
...retainBody(host, mangleClassRetain(cls.name), `retain ${cls.name}`, lifetime, count),
|
||||
``,
|
||||
);
|
||||
|
||||
// An out-of-line teardown tears typed children down directly
|
||||
// (directChildReleaseLines). It takes the chain length when it has such
|
||||
@@ -714,6 +821,7 @@ export function emitClassShapes(
|
||||
traced,
|
||||
`release ${cls.name}`,
|
||||
lifetime,
|
||||
count,
|
||||
),
|
||||
``,
|
||||
);
|
||||
@@ -723,12 +831,16 @@ export function emitClassShapes(
|
||||
` %isnull = icmp eq ptr %o, null`,
|
||||
` br i1 %isnull, label %done, label %check`,
|
||||
`check:`,
|
||||
` %rc = load ${host.sizeType}, ptr %o`,
|
||||
` %imm = icmp eq ${host.sizeType} %rc, -1`,
|
||||
` %rc = load ${count}, ptr %o`,
|
||||
` %imm = icmp eq ${count} %rc, -1`,
|
||||
` br i1 %imm, label %done, label %disp`,
|
||||
`disp:`,
|
||||
` %vtp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 1`,
|
||||
` %vt = load ptr, ptr %vtp`,
|
||||
...(meta.idInRc
|
||||
? classIdVtLines(meta, "%o", "%vt", "")
|
||||
: [
|
||||
` %vtp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 1`,
|
||||
` %vt = load ptr, ptr %vtp`,
|
||||
]),
|
||||
` %relp = getelementptr inbounds %ScrVt, ptr %vt, i64 0, i32 2`,
|
||||
` %rel = load ptr, ptr %relp`,
|
||||
` call void %rel(ptr %o) ; the DYNAMIC class's teardown`,
|
||||
@@ -743,10 +855,10 @@ export function emitClassShapes(
|
||||
const reld: string[] = [
|
||||
`define internal void @${mangleClassReleaseDirect(cls.name)}(ptr %o) ${FN_ATTRS} { ; direct release ${cls.name}`,
|
||||
`entry:`,
|
||||
` %rc = load ${host.sizeType}, ptr %o`,
|
||||
` %n = sub ${host.sizeType} %rc, 1`,
|
||||
` store ${host.sizeType} %n, ptr %o`,
|
||||
` %dead = icmp eq ${host.sizeType} %n, 0`,
|
||||
` %rc = load ${count}, ptr %o`,
|
||||
` %n = sub ${count} %rc, 1`,
|
||||
` store ${count} %n, ptr %o`,
|
||||
` %dead = icmp eq ${count} %n, 0`,
|
||||
` br i1 %dead, label %free, label %${traced ? "root" : "done"}`,
|
||||
`free:`,
|
||||
];
|
||||
@@ -815,10 +927,22 @@ export function emitClassShapes(
|
||||
host,
|
||||
sizeOf,
|
||||
traced && !lifetime ? { type: cycType } : null,
|
||||
meta.hierarchy ? 16 : 8,
|
||||
meta.hierarchy && !meta.idInRc ? 16 : 8,
|
||||
),
|
||||
);
|
||||
nw.push(` store ${host.sizeType} ${lifetime ? programLifetimeRc(host.sizeType) : 1}, ptr %o`);
|
||||
if (meta.idInRc) {
|
||||
// The class id in the high half of the rc word, the count in the low
|
||||
// half (classIdRcWord). It never changes while the object lives:
|
||||
// retains, releases and the runtime write only the low half.
|
||||
host.declare(`declare ptr @llvm.launder.invariant.group.p0(ptr)`);
|
||||
nw.push(
|
||||
` %o.vt = call ptr @llvm.launder.invariant.group.p0(ptr %o)`,
|
||||
` store i64 ${classIdRcWord(meta, lifetime)}, ptr %o.vt ; class ${llvmCommentText(cls.name)} is ${meta.pre}`,
|
||||
);
|
||||
} else
|
||||
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
|
||||
// program keeps program-lifetime objects (scr_console.c), and
|
||||
@@ -826,7 +950,7 @@ export function emitClassShapes(
|
||||
host.declare(`declare void @scr_obj_lifetime_note(ptr)`);
|
||||
nw.push(` call void @scr_obj_lifetime_note(ptr %o)`);
|
||||
}
|
||||
if (meta.hierarchy) {
|
||||
if (meta.hierarchy && !meta.idInRc) {
|
||||
// The vtable word is written once, here, and never changes while the
|
||||
// object lives: its store and the virtual-call loads carry
|
||||
// !invariant.group. A recycled block may have held an object of
|
||||
|
||||
@@ -266,6 +266,7 @@ import {
|
||||
buildClassGraph,
|
||||
classMembershipIntervals,
|
||||
classFieldIndex,
|
||||
classPreLines,
|
||||
classStructSym,
|
||||
programLifetimeClass,
|
||||
type LlClassMeta,
|
||||
@@ -1471,7 +1472,7 @@ export class LlEmitter {
|
||||
// The class graph: base/children links, hierarchy membership, the
|
||||
// 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);
|
||||
this.classMeta = buildClassGraph(mod, this.fnByName, this.sizeType);
|
||||
if (mod.lifetimeRegions === true) {
|
||||
this.regions = new LifetimeRegions(
|
||||
this.shapeHost,
|
||||
@@ -2822,10 +2823,16 @@ export class LlEmitter {
|
||||
} else if (t.kind === "string") {
|
||||
this.declare(`declare void @scr_throw_str(ptr)`);
|
||||
B.line(`call void @scr_throw_str(ptr ${v.name})`);
|
||||
} else if (t.kind === "object" && this.classMeta.get(t.className)?.hierarchy === true) {
|
||||
} else if (
|
||||
t.kind === "object" &&
|
||||
this.classMeta.get(t.className)?.hierarchy === true &&
|
||||
this.classMeta.get(t.className)?.idInRc !== true
|
||||
) {
|
||||
// Hierarchy instances carry a vtable word: the OBJ kind keeps the
|
||||
// dynamic class inspectable (catch-binding instanceof, the uncaught
|
||||
// printer's "name: message" for Error instances).
|
||||
// printer's "name: message" for Error instances). Instances that keep
|
||||
// their class id in the rc word are never Errors and are thrown as
|
||||
// REF payloads (narrow-guards.ts emitCaughtClassIdInstanceof).
|
||||
const rc = vAdapters(this.shapeHost, t);
|
||||
this.declare(`declare void @scr_throw_obj(ptr, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
@@ -3158,20 +3165,20 @@ export class LlEmitter {
|
||||
return { ptr: p, type, inline: this.inlineUnions.classField(className, field) };
|
||||
}
|
||||
|
||||
/** `o->vt->pre` — the dynamic class's preorder number (borrowed object;
|
||||
* the static class names which struct spelling carries the vt word). */
|
||||
/** The dynamic class's preorder number (borrowed object; the static class
|
||||
* names the struct spelling and where the number is, classPreLines). */
|
||||
loadVtPre(objName: string, staticClassName: string): string {
|
||||
const B = this.B;
|
||||
const vtp = B.tmp();
|
||||
const vt = B.tmp();
|
||||
const prep = B.tmp();
|
||||
const pre = B.tmp();
|
||||
B.line(
|
||||
`${vtp} = getelementptr inbounds %${classStructSym(staticClassName)}, ptr ${objName}, i64 0, i32 1`,
|
||||
);
|
||||
B.line(`${vt} = load ptr, ptr ${vtp}`);
|
||||
B.line(`${prep} = getelementptr inbounds %ScrVt, ptr ${vt}, i64 0, i32 0`);
|
||||
B.line(`${pre} = load ${this.sizeType}, ptr ${prep}`);
|
||||
const meta = this.classMetaOf(staticClassName);
|
||||
for (const line of classPreLines(
|
||||
meta,
|
||||
classStructSym(staticClassName),
|
||||
objName,
|
||||
pre,
|
||||
this.sizeType,
|
||||
))
|
||||
B.line(line.trimStart());
|
||||
return pre;
|
||||
}
|
||||
|
||||
@@ -5103,6 +5110,7 @@ export class LlEmitter {
|
||||
pre: meta.pre,
|
||||
post: meta.post,
|
||||
runtimeRooted: meta.root.def.runtime === true,
|
||||
idInRc: meta.idInRc,
|
||||
// Inline scalar-union fields are plain bits: nothing to push.
|
||||
fields: meta.def.fields.flatMap((f) =>
|
||||
this.inlineUnions.classField(className, f.name)
|
||||
@@ -5141,15 +5149,17 @@ export class LlEmitter {
|
||||
return this.own({ name: value.name, type: e.type });
|
||||
}
|
||||
|
||||
/** Branches on "`obj` is published" (rc == SIZE_MAX); leaves the builder
|
||||
* in the cold block and answers the continuation label. */
|
||||
private emitPublishedTest(obj: string): string {
|
||||
/** Branches on "`obj` is published" (rc == SIZE_MAX, or a count of
|
||||
* UINT32_MAX in the low half for `count` i32: classes.ts
|
||||
* LlClassMeta.idInRc); leaves the builder in the cold block and answers
|
||||
* the continuation label. */
|
||||
private emitPublishedTest(obj: string, count: "i32" | "i64" = this.sizeType): string {
|
||||
const B = this.B;
|
||||
const rc = B.tmp(),
|
||||
frozen = B.tmp(),
|
||||
expected = B.tmp();
|
||||
B.line(`${rc} = load ${this.sizeType}, ptr ${obj}`);
|
||||
B.line(`${frozen} = icmp eq ${this.sizeType} ${rc}, -1`);
|
||||
B.line(`${rc} = load ${count}, ptr ${obj}`);
|
||||
B.line(`${frozen} = icmp eq ${count} ${rc}, -1`);
|
||||
this.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
|
||||
B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${frozen}, i1 false)`);
|
||||
const cold = B.newLabel("pub.frozen"),
|
||||
@@ -5189,22 +5199,17 @@ export class LlEmitter {
|
||||
if (published === null) return;
|
||||
if (className !== null && !published.classGuarded(className)) return;
|
||||
const B = this.B;
|
||||
const ok = this.emitPublishedTest(obj);
|
||||
let owner: string;
|
||||
const meta = className === null ? undefined : this.classMeta.get(className);
|
||||
const ok = this.emitPublishedTest(obj, meta?.idInRc === true ? "i32" : this.sizeType);
|
||||
let owner: string;
|
||||
if (meta === undefined) owner = this.cstr("Object");
|
||||
else if (!meta.hierarchy) owner = this.cstr(meta.def.jsName ?? meta.def.name);
|
||||
else {
|
||||
// The DYNAMIC class names the object, as in Node.
|
||||
this.publishedNamesSym ??= "@sc_pub_class_names";
|
||||
const vtp = B.tmp(),
|
||||
vt = B.tmp(),
|
||||
pre = B.tmp(),
|
||||
const pre = this.loadVtPre(obj, meta.def.name),
|
||||
slot = B.tmp(),
|
||||
name = B.tmp();
|
||||
B.line(`${vtp} = getelementptr inbounds ptr, ptr ${obj}, i64 1`);
|
||||
B.line(`${vt} = load ptr, ptr ${vtp}`);
|
||||
B.line(`${pre} = load ${this.sizeType}, ptr ${vt}`);
|
||||
B.line(
|
||||
`${slot} = getelementptr inbounds ptr, ptr ${this.publishedNamesSym}, ${this.sizeType} ${pre}`,
|
||||
);
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from "../mangle.js";
|
||||
import {
|
||||
classEnvironmentIndex,
|
||||
classIdVtLines,
|
||||
classStructSym,
|
||||
vtEntriesFor,
|
||||
type LlClassMeta,
|
||||
@@ -1193,15 +1194,20 @@ export function emitCallExpr(
|
||||
if (!borrowed.has(index)) host.toFrame(a);
|
||||
});
|
||||
const recv = args[0]!.name;
|
||||
const vtp = B.tmp();
|
||||
const vt = B.tmp();
|
||||
const fnp = B.tmp();
|
||||
const fn = B.tmp();
|
||||
B.line(
|
||||
`${vtp} = getelementptr inbounds %${classStructSym(e.className)}, ptr ${recv}, i64 0, i32 1`,
|
||||
);
|
||||
// Set once at allocation (classes.ts launders each new object).
|
||||
B.line(`${vt} = load ptr, ptr ${vtp}, !invariant.group !{}`);
|
||||
if (meta.idInRc)
|
||||
for (const line of classIdVtLines(meta, recv, vt, ", !invariant.group !{}"))
|
||||
B.line(line.trimStart());
|
||||
else {
|
||||
const vtp = B.tmp();
|
||||
B.line(
|
||||
`${vtp} = getelementptr inbounds %${classStructSym(e.className)}, ptr ${recv}, i64 0, i32 1`,
|
||||
);
|
||||
B.line(`${vt} = load ptr, ptr ${vtp}, !invariant.group !{}`);
|
||||
}
|
||||
B.line(
|
||||
`${fnp} = getelementptr inbounds %${mangleVtStruct(meta.root.def.name)}, ptr ${vt}, i64 0, i32 ${slotIdx + 1}`,
|
||||
);
|
||||
|
||||
@@ -1062,6 +1062,42 @@ export function emitDynamicExpr(
|
||||
host.declare(
|
||||
`declare ptr @scr_caught_check_obj(ptr, ${host.sizeType}, ${host.sizeType}, ptr)`,
|
||||
);
|
||||
if (target.idInRc) {
|
||||
// A REF payload (emitCaughtInstanceof): a match retains it through
|
||||
// its adapter; anything else gets the runtime's TypeError from the
|
||||
// check against an empty interval.
|
||||
const ok = emitCaughtInstanceof(host, c.name, e.className);
|
||||
const yes = B.newLabel("caught.cast"),
|
||||
no = B.newLabel("caught.cast.fail"),
|
||||
join = B.newLabel("caught.cast.done");
|
||||
B.condBr(ok, yes, no);
|
||||
B.startBlock(yes);
|
||||
const payloadSlot = B.tmp(),
|
||||
payload = B.tmp(),
|
||||
retainSlot = B.tmp(),
|
||||
retain = B.tmp(),
|
||||
retained = B.tmp();
|
||||
B.line(`${payloadSlot} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 4`);
|
||||
B.line(`${payload} = load ptr, ptr ${payloadSlot}`);
|
||||
B.line(`${retainSlot} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 5`);
|
||||
B.line(`${retain} = load ptr, ptr ${retainSlot}`);
|
||||
B.line(`${retained} = call ptr ${retain}(ptr ${payload})`);
|
||||
const fromYes = B.currentLabel();
|
||||
B.br(join);
|
||||
B.startBlock(no);
|
||||
const failed = B.tmp();
|
||||
B.line(
|
||||
`${failed} = call ptr @scr_caught_check_obj(ptr ${c.name}, ${host.sizeType} 1, ${host.sizeType} 0, ptr ${host.cstr(display)})`,
|
||||
);
|
||||
const fromNo = B.currentLabel();
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = phi ptr [ ${retained}, %${fromYes} ], [ ${failed}, %${fromNo} ]`);
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
let pre = String(target.pre);
|
||||
let post = String(target.post);
|
||||
const intervals = classMembershipIntervals(host.classMeta, target.def.name);
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from "../../ir/ir.js";
|
||||
import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js";
|
||||
import { BlockBuilder } from "./blocks.js";
|
||||
import { type LlClassMeta, classFieldIndex, classStructSym } from "./classes.js";
|
||||
import { type LlClassMeta, classFieldIndex, classPreLines, classStructSym } from "./classes.js";
|
||||
import { SCR_ARR_FIELD, llvmCommentText } from "./common.js";
|
||||
import {
|
||||
FN_ATTRS,
|
||||
@@ -232,10 +232,13 @@ export function streamTypedRefCommitAdapter(
|
||||
];
|
||||
if (meta.hierarchy && meta.children.length) {
|
||||
lines.push(
|
||||
` %derived_vt_slot = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 1`,
|
||||
` %derived_vt = load ptr, ptr %derived_vt_slot`,
|
||||
` %derived_pre_slot = getelementptr inbounds %ScrVt, ptr %derived_vt, i64 0, i32 0`,
|
||||
` %derived_pre = load ${host.sizeType}, ptr %derived_pre_slot`,
|
||||
...classPreLines(
|
||||
meta,
|
||||
classStructSym(t.className),
|
||||
"%target",
|
||||
"%derived_pre",
|
||||
host.sizeType,
|
||||
),
|
||||
);
|
||||
const descendants = (current: typeof meta): (typeof meta)[] =>
|
||||
current.children.flatMap((child) => [child, ...descendants(child)]);
|
||||
@@ -728,16 +731,9 @@ export function streamTypedRefMaterializeAdapter(
|
||||
}
|
||||
if (adapter.classOps !== undefined) liveClassOps(host, snapshot, meta);
|
||||
if (meta.hierarchy && meta.children.length) {
|
||||
const vtSlot = B.tmp(),
|
||||
vt = B.tmp(),
|
||||
preSlot = B.tmp(),
|
||||
pre = B.tmp();
|
||||
B.line(
|
||||
`${vtSlot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 1`,
|
||||
);
|
||||
B.line(`${vt} = load ptr, ptr ${vtSlot}`);
|
||||
B.line(`${preSlot} = getelementptr inbounds %ScrVt, ptr ${vt}, i64 0, i32 0`);
|
||||
B.line(`${pre} = load ${host.sizeType}, ptr ${preSlot}`);
|
||||
const pre = B.tmp();
|
||||
for (const line of classPreLines(meta, classStructSym(t.className), "%p", pre, host.sizeType))
|
||||
B.line(line.trimStart());
|
||||
const descendants = (current: typeof meta): (typeof meta)[] =>
|
||||
current.children.flatMap((child) => [child, ...descendants(child)]);
|
||||
for (const child of descendants(meta).reverse()) {
|
||||
@@ -1364,16 +1360,9 @@ function liveClassOps(host: LlvmEmitterContext, snapshot: string, meta: LlClassM
|
||||
if (subclasses.length > 0) {
|
||||
exact = `@${snapshot}_cexact`;
|
||||
const B = new BlockBuilder();
|
||||
const vtSlot = B.tmp(),
|
||||
vt = B.tmp(),
|
||||
preSlot = B.tmp(),
|
||||
pre = B.tmp();
|
||||
B.line(
|
||||
`${vtSlot} = getelementptr inbounds %${classStructSym(meta.def.name)}, ptr %p, i64 0, i32 1`,
|
||||
);
|
||||
B.line(`${vt} = load ptr, ptr ${vtSlot}`);
|
||||
B.line(`${preSlot} = getelementptr inbounds %ScrVt, ptr ${vt}, i64 0, i32 0`);
|
||||
B.line(`${pre} = load ${host.sizeType}, ptr ${preSlot}`);
|
||||
const pre = B.tmp();
|
||||
for (const line of classPreLines(meta, classStructSym(meta.def.name), "%p", pre, host.sizeType))
|
||||
B.line(line.trimStart());
|
||||
for (const child of subclasses.reverse()) {
|
||||
const childAdapter = host.liveDynRefAdapter({ kind: "object", className: child.def.name });
|
||||
const match = B.tmp(),
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* throw. The guards are cheap: one load and compare per site, which LLVM
|
||||
* folds where an identical test already dominates. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { classMembershipIntervals } from "./classes.js";
|
||||
import { classMembershipIntervals, type LlClassMeta } from "./classes.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { vAdapters } from "./shapes.js";
|
||||
|
||||
@@ -124,6 +124,7 @@ export function emitCaughtInstanceof(
|
||||
const B = host.B;
|
||||
const target = host.classMetaOf(className);
|
||||
const intervals = classMembershipIntervals(host.classMeta, target.def.name);
|
||||
if (target.idInRc) return emitCaughtClassIdInstanceof(host, caught, target, intervals);
|
||||
if (!target.hierarchy) {
|
||||
const adapterSlot = B.tmp();
|
||||
const adapter = B.tmp();
|
||||
@@ -169,6 +170,80 @@ export function emitCaughtInstanceof(
|
||||
return result;
|
||||
}
|
||||
|
||||
/** `e instanceof C` for a class whose instances keep their class id in
|
||||
* the rc word (classes.ts LlClassMeta.idInRc). They are thrown as REF
|
||||
* payloads, like standalone class instances: the retain adapter names the
|
||||
* hierarchy (the adapter of whichever static class threw), and the payload's
|
||||
* class id is tested against the intervals. */
|
||||
function emitCaughtClassIdInstanceof(
|
||||
host: LlvmEmitterContext,
|
||||
caught: string,
|
||||
target: LlClassMeta,
|
||||
intervals: { pre: number; post: number }[],
|
||||
): string {
|
||||
const B = host.B;
|
||||
const kp = B.tmp(),
|
||||
kind = B.tmp(),
|
||||
isRef = B.tmp(),
|
||||
adapterSlot = B.tmp(),
|
||||
adapter = B.tmp();
|
||||
B.line(`${kp} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 1`);
|
||||
B.line(`${kind} = load i32, ptr ${kp}`);
|
||||
B.line(`${isRef} = icmp eq i32 ${kind}, ${CAUGHT_REF_KIND}`);
|
||||
B.line(`${adapterSlot} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 5`);
|
||||
B.line(`${adapter} = load ptr, ptr ${adapterSlot}`);
|
||||
let anyAdapter: string | undefined;
|
||||
for (const m of host.classMeta.values()) {
|
||||
if (m.root !== target.root) continue;
|
||||
const rc = vAdapters(host.shapeHost, { kind: "object", className: m.def.name });
|
||||
const matches = B.tmp();
|
||||
B.line(`${matches} = icmp eq ptr ${adapter}, ${rc.retain}`);
|
||||
if (anyAdapter === undefined) anyAdapter = matches;
|
||||
else {
|
||||
const joined = B.tmp();
|
||||
B.line(`${joined} = or i1 ${anyAdapter}, ${matches}`);
|
||||
anyAdapter = joined;
|
||||
}
|
||||
}
|
||||
const inHierarchy = B.tmp();
|
||||
B.line(`${inHierarchy} = and i1 ${isRef}, ${anyAdapter!}`);
|
||||
const test = B.newLabel("caught.class"),
|
||||
join = B.newLabel("caught.class.done"),
|
||||
from = B.currentLabel();
|
||||
B.condBr(inHierarchy, test, join);
|
||||
B.startBlock(test);
|
||||
const payloadSlot = B.tmp(),
|
||||
payload = B.tmp();
|
||||
B.line(`${payloadSlot} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 4`);
|
||||
B.line(`${payload} = load ptr, ptr ${payloadSlot}`);
|
||||
const pre = host.loadVtPre(payload, target.def.name);
|
||||
let result: string | undefined;
|
||||
for (const interval of intervals) {
|
||||
const ge = B.tmp(),
|
||||
le = B.tmp(),
|
||||
t = B.tmp();
|
||||
B.line(`${ge} = icmp uge ${host.sizeType} ${pre}, ${interval.pre}`);
|
||||
B.line(`${le} = icmp ule ${host.sizeType} ${pre}, ${interval.post}`);
|
||||
B.line(`${t} = and i1 ${ge}, ${le}`);
|
||||
if (result === undefined) result = t;
|
||||
else {
|
||||
const combined = B.tmp();
|
||||
B.line(`${combined} = or i1 ${result}, ${t}`);
|
||||
result = combined;
|
||||
}
|
||||
}
|
||||
if (result === undefined) throw new InternalCompilerError("empty class membership");
|
||||
const tested = B.currentLabel();
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = phi i1 [ false, %${from} ], [ ${result}, %${tested} ]`);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** SCR_EXC_REF (scr_exception.h). */
|
||||
const CAUGHT_REF_KIND = 4;
|
||||
|
||||
/** SCR_EXC_F64 = 1, SCR_EXC_BOOL = 2, SCR_EXC_STR = 3 (scr_exception.h). */
|
||||
export const CAUGHT_SCALAR_KINDS = { f64: 1, bool: 2, string: 3 } as const;
|
||||
|
||||
|
||||
@@ -63,6 +63,9 @@ export interface PublishClassInfo {
|
||||
post: number;
|
||||
/** A runtime-provided root class (errors, emitters, streams). */
|
||||
runtimeRooted: boolean;
|
||||
/** Instances keep their class id in the rc word (classes.ts
|
||||
* LlClassMeta.idInRc). */
|
||||
idInRc: boolean;
|
||||
/** The class's own and inherited fields: GEP index and storage type. */
|
||||
fields: { name: string; index: number; type: IrType }[];
|
||||
}
|
||||
@@ -70,6 +73,12 @@ export interface PublishClassInfo {
|
||||
/** scr_pub_mark's object kinds (SCR_PUB_KIND_* in scr_runtime.h), for the RC audit. */
|
||||
const PUB_KIND_OBJECT = 4;
|
||||
const PUB_KIND_UNION = 5;
|
||||
/** SCR_PUB_CLASS_ID, or'ed into a kind: the object keeps its class id
|
||||
* in the high half of its rc word (classes.ts LlClassMeta.idInRc). */
|
||||
const PUB_CLASS_ID = 0x100;
|
||||
/** SCR_PUB_LOG_COUNT (scr_publish.c), in a log entry's `buf`: `rc` is the
|
||||
* count of the low half. */
|
||||
const PUB_LOG_COUNT = "-2147483648";
|
||||
|
||||
const RUNTIME_WALKERS = {
|
||||
string: "@scr_pub_str_fn",
|
||||
@@ -307,9 +316,18 @@ export class PublishWalkers {
|
||||
const lines = [
|
||||
head,
|
||||
`entry:`,
|
||||
` %vtp = getelementptr inbounds ptr, ptr %o, i64 1`,
|
||||
` %vt = load ptr, ptr %vtp`,
|
||||
` %pre = load ${this.host.sizeType}, ptr %vt`,
|
||||
...(info.idInRc
|
||||
? [
|
||||
` %idp = getelementptr inbounds i8, ptr %o, i64 4 ; the class id (classes.ts classIdRcWord)`,
|
||||
` %id = load i32, ptr %idp`,
|
||||
` %idpre = add i32 %id, -1`,
|
||||
` %pre = zext i32 %idpre to ${this.host.sizeType}`,
|
||||
]
|
||||
: [
|
||||
` %vtp = getelementptr inbounds ptr, ptr %o, i64 1`,
|
||||
` %vt = load ptr, ptr %vtp`,
|
||||
` %pre = load ${this.host.sizeType}, ptr %vt`,
|
||||
]),
|
||||
` switch ${this.host.sizeType} %pre, label %other [ ${cases.map((c) => `${this.host.sizeType} ${c.pre}, label %c${c.pre}`).join(" ")} ]`,
|
||||
];
|
||||
for (const c of cases)
|
||||
@@ -344,6 +362,7 @@ export class PublishWalkers {
|
||||
info.fields,
|
||||
path,
|
||||
loc,
|
||||
info.idInRc,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -355,20 +374,23 @@ export class PublishWalkers {
|
||||
* targets), and an object with a collector header leaves its candidate
|
||||
* buffer through scr_cyc_pub_take. Anything else calls scr_pub_mark, as
|
||||
* does every mark of an RC-audit build. */
|
||||
private markLines(cyc: boolean, kind: number): string[] {
|
||||
private markLines(cyc: boolean, kind: number, classId = false): string[] {
|
||||
const call = (name: string): string[] => [
|
||||
` %${name} = call zeroext i1 @scr_pub_mark(ptr %ctx, ptr %o, i1 ${cyc ? "true" : "false"}, i32 ${kind})`,
|
||||
` %${name} = call zeroext i1 @scr_pub_mark(ptr %ctx, ptr %o, i1 ${cyc ? "true" : "false"}, i32 ${classId ? kind | PUB_CLASS_ID : kind})`,
|
||||
` br i1 %${name}, label %walk, label %done`,
|
||||
];
|
||||
if (this.host.rcAudit === true) return call("m");
|
||||
const S = this.host.sizeType;
|
||||
// An object that keeps its class id in the rc word is marked and
|
||||
// logged through its count, the low half.
|
||||
const C = classId ? "i32" : S;
|
||||
const field = (name: string, slot: number): string[] => [
|
||||
` %${name}p = getelementptr inbounds ptr, ptr %ctx, i32 ${slot}`,
|
||||
` %${name} = load ${S === "i64" ? "i64" : "i32"}, ptr %${name}p`,
|
||||
];
|
||||
const lines = [
|
||||
` %mrc = load ${S}, ptr %o`,
|
||||
` %mimm = icmp eq ${S} %mrc, -1`,
|
||||
` %mrc = load ${C}, ptr %o`,
|
||||
` %mimm = icmp eq ${C} %mrc, -1`,
|
||||
` br i1 %mimm, label %done, label %mtest`,
|
||||
`mtest:`,
|
||||
` %mrefp = getelementptr inbounds ptr, ptr %ctx, i32 6`,
|
||||
@@ -380,13 +402,13 @@ export class PublishWalkers {
|
||||
` %mok1 = and i1 %mok0, %mroom`,
|
||||
];
|
||||
let ok = "%mok1";
|
||||
if (S === "i64") {
|
||||
if (C === "i64") {
|
||||
// 0xfffffffe and up are the log's escapes (SCR_PUB_RC_LIFETIME, _BIG).
|
||||
lines.push(` %msmall = icmp ult i64 %mrc, 4294967294`, ` %mok2 = and i1 %mok1, %msmall`);
|
||||
ok = "%mok2";
|
||||
}
|
||||
lines.push(` br i1 ${ok}, label %mfast, label %mslow`, `mslow:`, ...call("ms"), `mfast:`);
|
||||
let buf = "0";
|
||||
let buf = classId ? String(PUB_LOG_COUNT) : "0";
|
||||
if (cyc) {
|
||||
// ScrCycHdr.buf, the word before the object: its top bit while the
|
||||
// object is a buffered candidate.
|
||||
@@ -402,9 +424,10 @@ export class PublishWalkers {
|
||||
...field("mtaken", 7),
|
||||
` %mtaken1 = add ${S} %mtaken, 1`,
|
||||
` store ${S} %mtaken1, ptr %mtakenp`,
|
||||
...(classId ? [` %mix2 = or i32 %mix1, ${PUB_LOG_COUNT}`] : []),
|
||||
` br label %mput`,
|
||||
`mput:`,
|
||||
` %mbuf = phi i32 [ 0, %mfast ], [ %mix1, %mtake ]`,
|
||||
` %mbuf = phi i32 [ ${buf}, %mfast ], [ %mix${classId ? 2 : 1}, %mtake ]`,
|
||||
);
|
||||
buf = "%mbuf";
|
||||
}
|
||||
@@ -414,13 +437,13 @@ export class PublishWalkers {
|
||||
` %me = getelementptr inbounds { ptr, i32, i32 }, ptr %mlog, ${S} %mlogged`,
|
||||
` store ptr %o, ptr %me`,
|
||||
` %merc = getelementptr inbounds { ptr, i32, i32 }, ptr %me, i32 0, i32 1`,
|
||||
S === "i64" ? ` %mrc32 = trunc i64 %mrc to i32` : ` %mrc32 = add i32 %mrc, 0`,
|
||||
C === "i64" ? ` %mrc32 = trunc i64 %mrc to i32` : ` %mrc32 = add i32 %mrc, 0`,
|
||||
` store i32 %mrc32, ptr %merc`,
|
||||
` %mebuf = getelementptr inbounds { ptr, i32, i32 }, ptr %me, i32 0, i32 2`,
|
||||
` store i32 ${buf}, ptr %mebuf`,
|
||||
` %mlogged1 = add ${S} %mlogged, 1`,
|
||||
` store ${S} %mlogged1, ptr %mloggedp`,
|
||||
` store ${S} -1, ptr %o`,
|
||||
` store ${C} -1, ptr %o`,
|
||||
` br label %walk`,
|
||||
);
|
||||
return lines;
|
||||
@@ -431,13 +454,14 @@ export class PublishWalkers {
|
||||
* before) are the ones scr_pub_push skips: they are tested here, without
|
||||
* the call. The lines end in a fresh block labelled `pushed${id}`. */
|
||||
private push(v: string, fn: string, id: string): string[] {
|
||||
const size = this.host.sizeType;
|
||||
return [
|
||||
` %null${id} = icmp eq ptr ${v}, null`,
|
||||
` br i1 %null${id}, label %pushed${id}, label %live${id}`,
|
||||
`live${id}:`,
|
||||
` %rc${id} = load ${size}, ptr ${v}`,
|
||||
` %immortal${id} = icmp eq ${size} %rc${id}, -1`,
|
||||
// The count (the low half of a 64-bit rc word) is UINT32_MAX for an
|
||||
// immortal object of every kind (scr_rc.h SCR_RC_COUNT).
|
||||
` %rc${id} = load i32, ptr ${v}`,
|
||||
` %immortal${id} = icmp eq i32 %rc${id}, -1`,
|
||||
` br i1 %immortal${id}, label %pushed${id}, label %push${id}`,
|
||||
`push${id}:`,
|
||||
...this.pushLines(v, fn, id),
|
||||
@@ -480,8 +504,9 @@ export class PublishWalkers {
|
||||
members: { name: string; index: number; type: IrType }[],
|
||||
path: string,
|
||||
loc: SrcLoc,
|
||||
classId = false,
|
||||
): string[] {
|
||||
const lines = [head, `entry:`, ...this.markLines(traced, PUB_KIND_OBJECT), `walk:`];
|
||||
const lines = [head, `entry:`, ...this.markLines(traced, PUB_KIND_OBJECT, classId), `walk:`];
|
||||
let n = 0;
|
||||
for (const member of members) {
|
||||
const fn = this.walker(member.type, `${path}.${member.name}`, loc);
|
||||
|
||||
@@ -117,7 +117,13 @@ test("ProgramLifetime instances live in the current region and count in their ch
|
||||
);
|
||||
// The first reference is implied: allocation leaves the chunk count alone.
|
||||
expect(nw, cls).not.toContain("2097151");
|
||||
expect(nw, cls).toContain("store i64 -9223372036854775808, ptr %o");
|
||||
// The rc word: the class id in the high half (the hierarchy keeps it
|
||||
// there instead of a vtable word) over the program-lifetime count, the
|
||||
// midpoint of the 32-bit count in the low half.
|
||||
const word = /store i64 (\d+), ptr %o\.vt/.exec(nw)?.[1];
|
||||
expect(word, cls).toBeDefined();
|
||||
expect(BigInt(word!) % 2n ** 32n, cls).toBe(2n ** 31n);
|
||||
expect(BigInt(word!) / 2n ** 32n > 0n, cls).toBe(true);
|
||||
expect(nw, cls).not.toContain("scr_rt_calloc");
|
||||
// retain/release: the chunk count, NULL-tolerant, once a region
|
||||
// exists, and only in chunks that have an owner (freeable regions).
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
mangleRecordStruct,
|
||||
} from "../mangle.js";
|
||||
import { threadHotAddressLines } from "./alloc.js";
|
||||
import { classFieldBase, type LlClassMeta } from "./classes.js";
|
||||
import { classFieldBase, classPreLines, 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";
|
||||
@@ -389,9 +389,7 @@ export class LifetimeRegions {
|
||||
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`,
|
||||
...classPreLines(meta, mangleClassStruct(cls.name), "%o", "%pre", S),
|
||||
` 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)}`,
|
||||
|
||||
@@ -1249,6 +1249,11 @@ export function rcMembers(
|
||||
];
|
||||
}
|
||||
|
||||
/** The integer type a release or retain loads and stores the count as:
|
||||
* the rc word, or its low half (an i32 at offset 0) for class instances that
|
||||
* keep their class number in the high half (classes.ts LlClassMeta.idInRc). */
|
||||
export type RcCount = "i32" | "i64";
|
||||
|
||||
/** The NULL- and immortal-skipping retain body shared by every shape. A
|
||||
* retain never touches the cycle header: a buffered candidate that is
|
||||
* retained again stays a candidate (scr_cycle.c, markRoots). */
|
||||
@@ -1257,8 +1262,9 @@ export function retainBody(
|
||||
fnName: string,
|
||||
comment = "",
|
||||
immortal = false,
|
||||
count: RcCount = host.sizeType,
|
||||
): string[] {
|
||||
const S = host.sizeType;
|
||||
const S = count;
|
||||
if (immortal) return immortalRcBody(host, fnName, "ptr", comment);
|
||||
return [
|
||||
`define internal ptr @${fnName}(ptr %o) ${FN_ATTRS} {${comment ? ` ; ${comment}` : ""}`,
|
||||
@@ -1325,6 +1331,8 @@ export interface DirectDestroy {
|
||||
readonly destroy: string;
|
||||
/** The child carries a collector header. */
|
||||
readonly traced: boolean;
|
||||
/** The child's count type, when it is not the rc word (RcCount). */
|
||||
readonly count?: RcCount;
|
||||
}
|
||||
|
||||
/** The release of child `value` (a loaded field, possibly NULL) inside a
|
||||
@@ -1339,19 +1347,20 @@ export function directChildReleaseLines(
|
||||
comment: string,
|
||||
): string[] {
|
||||
const S = host.sizeType;
|
||||
const C = target.count ?? S;
|
||||
host.declare(`declare void @scr_rc_destroy(ptr, ptr)`);
|
||||
const lines = [
|
||||
` %${tag}.nn = icmp ne ptr ${value}, null`,
|
||||
` br i1 %${tag}.nn, label %${tag}.chk, label %${tag}.next`,
|
||||
`${tag}.chk:`,
|
||||
` %${tag}.rc = load ${S}, ptr ${value}`,
|
||||
` %${tag}.last = icmp eq ${S} %${tag}.rc, 1`,
|
||||
` %${tag}.rc = load ${C}, ptr ${value}`,
|
||||
` %${tag}.last = icmp eq ${C} %${tag}.rc, 1`,
|
||||
` br i1 %${tag}.last, label %${tag}.dead, label %${tag}.shared`,
|
||||
`${tag}.shared:`,
|
||||
` call void ${target.release}(ptr ${value}) ; ${comment}`,
|
||||
` br label %${tag}.next`,
|
||||
`${tag}.dead: ; ${comment}: the last reference`,
|
||||
` store ${S} 0, ptr ${value}`,
|
||||
` store ${C} 0, ptr ${value}`,
|
||||
];
|
||||
if (target.traced) {
|
||||
host.declare(`declare void @scr_cyc_on_dead(ptr)`);
|
||||
@@ -1514,8 +1523,9 @@ export function releaseFastPath(
|
||||
traced: boolean,
|
||||
comment = "",
|
||||
immortal = false,
|
||||
count: RcCount = host.sizeType,
|
||||
): string[] {
|
||||
const S = host.sizeType;
|
||||
const S = count;
|
||||
if (immortal) return immortalRcBody(host, fnName, "void", comment);
|
||||
return [
|
||||
`define internal void @${fnName}(ptr %o) ${FN_ATTRS} {${comment ? ` ; ${comment} (fast path)` : ""}`,
|
||||
|
||||
@@ -85,12 +85,11 @@ test("slots.get reads the slot word, the page and the entry inline", () => {
|
||||
];
|
||||
expect(validateModule(module(calls))).toEqual([]);
|
||||
const llvm = emitLlvmModule(module(calls), { pointerBits: 64 });
|
||||
// The slot word sits after the header words of every hierarchy member.
|
||||
// The slot word sits after the rc word of every hierarchy member, which
|
||||
// keeps its class id in the rc word instead of a vtable word.
|
||||
expect(llvm).toMatch(new RegExp(`%${mangleClassStruct("Node")} = type \\{ i64, i32, ptr \\}`));
|
||||
expect(llvm).toMatch(
|
||||
new RegExp(`%${mangleClassStruct("Node")} = type \\{ i64, ptr, i32, ptr \\}`),
|
||||
);
|
||||
expect(llvm).toMatch(
|
||||
new RegExp(`%${mangleClassStruct("Leaf")} = type \\{ i64, ptr, i32, ptr, (i32|double) \\}`),
|
||||
new RegExp(`%${mangleClassStruct("Leaf")} = type \\{ i64, i32, ptr, (i32|double) \\}`),
|
||||
);
|
||||
expect(llvm).toMatch(new RegExp(`%${mangleClassStruct("Plain")} = type \\{ i64, ptr \\}`));
|
||||
// One block per family name.
|
||||
@@ -102,7 +101,7 @@ test("slots.get reads the slot word, the page and the entry inline", () => {
|
||||
);
|
||||
const main = body(llvm, "sc_f_main");
|
||||
expect(main).toMatch(
|
||||
/getelementptr inbounds %\S+, ptr %\S+, i64 0, i32 2\n {2}(%\S+) = load atomic i32, ptr %\S+ monotonic, align 4\n {2}%\S+ = lshr i32 \1, 10/,
|
||||
/getelementptr inbounds %\S+, ptr %\S+, i64 0, i32 1\n {2}(%\S+) = load atomic i32, ptr %\S+ monotonic, align 4\n {2}%\S+ = lshr i32 \1, 10/,
|
||||
);
|
||||
expect(main).not.toMatch(/call ptr @scr_slots_get/);
|
||||
expect(main).toMatch(
|
||||
|
||||
@@ -109,6 +109,13 @@ export function mangleVtStruct(rootClassName: string): string {
|
||||
export function mangleVtInstance(className: string): string {
|
||||
return `sc_vtable_${sanitize(className)}`;
|
||||
}
|
||||
/** The per-HIERARCHY array of the vtables of its classes in preorder, for
|
||||
* hierarchies whose instances keep their class id in the rc word
|
||||
* (classes.ts LlClassMeta.idInRc). `sc_vta_` differs from `sc_vtable_` and
|
||||
* `sc_vtt_` before any user-controlled text. */
|
||||
export function mangleVtArray(rootClassName: string): string {
|
||||
return `sc_vta_${sanitize(rootClassName)}`;
|
||||
}
|
||||
/** A virtual slot's member name inside the vtable struct. Sibling
|
||||
* branches of one hierarchy can each declare a slot for the SAME method
|
||||
* name (root-most in their own subtree, overridden below — mixin layers
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/class-id-instances-as-streams.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -165,8 +165,9 @@ void scr_weak_dispose(void *object) {
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Every heap object's first member is `size_t rc`. */
|
||||
#define SCR_RC(obj) (*(size_t *)(obj))
|
||||
/* Every heap object's first member is `size_t rc`; the collector works on
|
||||
* its count (SCR_RC_COUNT). */
|
||||
#define SCR_RC(obj) SCR_RC_COUNT(obj)
|
||||
|
||||
static void scr_cyc_oom(void) {
|
||||
scr_trap("scriptc: out of memory\n");
|
||||
@@ -1171,7 +1172,7 @@ void scr_cyc_on_release(void *obj) {
|
||||
#define SCR_CYC_WALK_GEN(h) scr_cyc_gen(h)
|
||||
#endif
|
||||
#define SCR_CYC_SKIP(child) \
|
||||
((child) == NULL || SCR_RC(child) == SIZE_MAX \
|
||||
((child) == NULL || SCR_RC(child) == SCR_RC_IMMORTAL \
|
||||
|| SCR_CYC_WALK_GEN(scr_cyc_hdr(child)) > scr_gen_limit)
|
||||
|
||||
/* The walks are depth-first and recursive up to SCR_CYC_WALK_DEPTH, where
|
||||
@@ -1213,7 +1214,7 @@ SCR_CYC_ARRAY_WALK(scr_scan_array, scr_scan_visit)
|
||||
|
||||
static inline __attribute__((always_inline)) void scr_mark_gray(void *obj, uintptr_t depth);
|
||||
static void scr_mg_visit(void *child, void *ctx) {
|
||||
if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
|
||||
if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return;
|
||||
ScrCycHdr *h = scr_cyc_hdr(child);
|
||||
if (SCR_CYC_WALK_GEN(h) > scr_gen_limit) {
|
||||
scr_cyc_push_work(&scr_xgen_edges, scr_gray_holder);
|
||||
@@ -1372,7 +1373,7 @@ static SCR_TL size_t scr_xg_freed = 0;
|
||||
static void scr_xg_release(void *obj);
|
||||
static void scr_xg_child_visit(void *child, void *ctx) {
|
||||
(void)ctx;
|
||||
if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
|
||||
if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return;
|
||||
scr_xg_release(child);
|
||||
}
|
||||
|
||||
@@ -1597,7 +1598,7 @@ static void scr_yin_leave(ScrCycHdr *h, void *obj) {
|
||||
|
||||
static void scr_yin_count_visit(void *child, void *ctx) {
|
||||
(void)ctx;
|
||||
if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
|
||||
if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return;
|
||||
ScrCycHdr *h = scr_cyc_hdr(child);
|
||||
if (scr_cyc_gen(h) == SCR_CYC_YOUNG) scr_yin_counts[scr_cyc_buf_index(h)]++;
|
||||
}
|
||||
@@ -1605,7 +1606,7 @@ static void scr_yin_count_visit(void *child, void *ctx) {
|
||||
/* Marked young objects are GRAY (young objects are otherwise BLACK). */
|
||||
static void scr_yin_mark_visit(void *child, void *ctx) {
|
||||
(void)ctx;
|
||||
if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
|
||||
if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return;
|
||||
ScrCycHdr *h = scr_cyc_hdr(child);
|
||||
if (scr_cyc_gen(h) != SCR_CYC_YOUNG || scr_cyc_color(h) == SCR_CYC_GRAY) return;
|
||||
scr_cyc_set_color(h, SCR_CYC_GRAY);
|
||||
@@ -1617,7 +1618,7 @@ static void scr_yin_mark_visit(void *child, void *ctx) {
|
||||
* the count until every teardown has run. */
|
||||
static void scr_yin_dead_visit(void *child, void *ctx) {
|
||||
(void)ctx;
|
||||
if (child == NULL || SCR_RC(child) == SIZE_MAX) return;
|
||||
if (child == NULL || SCR_RC(child) == SCR_RC_IMMORTAL) return;
|
||||
ScrCycHdr *h = scr_cyc_hdr(child);
|
||||
if (scr_cyc_gen(h) == SCR_CYC_YOUNG && scr_cyc_color(h) != SCR_CYC_GRAY) return;
|
||||
scr_yin_push(&scr_yin_xrel, child);
|
||||
|
||||
@@ -140,9 +140,11 @@ static void *bcs_capsule_retain(void *p) { return scr_stream_retain(p); }
|
||||
/* Also the capsule's tag. */
|
||||
static void bcs_capsule_release(void *p) { scr_stream_release(p); }
|
||||
|
||||
static bool bcs_stream_vt(const ScrVt *vt) {
|
||||
return (vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
|
||||
(vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post);
|
||||
/* Whether `stream`, boxed as an "object:" typed ref, is a runtime stream. */
|
||||
static bool bcs_stream_vt(const ScrStream *stream) {
|
||||
const ScrVt *vt = scr_stream_vt_of(stream);
|
||||
return vt != NULL && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
|
||||
(vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post));
|
||||
}
|
||||
|
||||
static void bcs_rec_release(void *p);
|
||||
@@ -154,7 +156,7 @@ static ScrStream *bcs_of(const ScrDyn *value) {
|
||||
if (value != NULL && value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 &&
|
||||
memcmp(value->v.typed_ref.type_key, "object:", 7) == 0) {
|
||||
ScrStream *stream = value->v.typed_ref.ptr;
|
||||
return bcs_stream_vt(stream->vt) && scr_stream_native(stream, bcs_rec_release) ? stream : NULL;
|
||||
return bcs_stream_vt(stream) && scr_stream_native(stream, bcs_rec_release) ? stream : NULL;
|
||||
}
|
||||
if (value == NULL || value->kind != SCR_DYN_OBJ || value->symbol_properties == NULL) return NULL;
|
||||
ScrDyn *capsule = scr_dyn_obj_get(value->symbol_properties, bcs_hidden_key, BCS_HIDDEN_LEN);
|
||||
@@ -184,7 +186,7 @@ static ScrStream *bcs_native_of(const ScrDyn *value) {
|
||||
memcmp(value->v.typed_ref.type_key, "object:", 7) != 0)
|
||||
return NULL;
|
||||
ScrStream *stream = scr_dyn_typed_ref_unbox(value);
|
||||
if (bcs_stream_vt(stream->vt)) return stream;
|
||||
if (bcs_stream_vt(stream)) return stream;
|
||||
value->v.typed_ref.release(stream);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ static size_t scr_message_value(ScrMessageEncoder *encoder, const ScrDyn *input)
|
||||
/* Published values (immortal) travel by pointer: a typed value as a
|
||||
* capsule over the same object, a dyn graph as itself. */
|
||||
if (input->kind == SCR_DYN_TYPED_REF && input->v.typed_ref.ptr &&
|
||||
*(const size_t *)input->v.typed_ref.ptr == SIZE_MAX) {
|
||||
SCR_RC_COUNT(input->v.typed_ref.ptr) == SCR_RC_IMMORTAL) {
|
||||
scr_sa_thread_shares();
|
||||
encoder->visits[id].view = (ScrDyn *)input;
|
||||
ScrMessageNode *node = &encoder->message->nodes[id];
|
||||
|
||||
@@ -37,9 +37,14 @@
|
||||
typedef struct {
|
||||
uintptr_t obj;
|
||||
uint32_t rc;
|
||||
uint32_t buf; /* a candidate taken out of its buffer: its index + 1, else 0 */
|
||||
uint32_t buf; /* a candidate taken out of its buffer: its index + 1, else 0;
|
||||
* SCR_PUB_LOG_COUNT: `rc` is the count of the low half */
|
||||
} ScrPubLog;
|
||||
|
||||
/* In ScrPubLog.buf: the object keeps its class id in the high half of
|
||||
* its rc word (SCR_PUB_CLASS_ID), and `rc` is the count of the low half. */
|
||||
#define SCR_PUB_LOG_COUNT UINT32_C(0x80000000)
|
||||
|
||||
#define SCR_PUB_DYN ((uintptr_t)1)
|
||||
#define SCR_PUB_RC_LIFETIME UINT32_C(0xfffffffe)
|
||||
#define SCR_PUB_RC_BIG UINT32_C(0xffffffff)
|
||||
@@ -103,7 +108,7 @@ static void *scr_pub_grow(void *p, size_t *cap, size_t size, void *inline_buf) {
|
||||
void scr_pub_str_fn(void *v, ScrPub *ctx);
|
||||
|
||||
void scr_pub_push(ScrPub *ctx, void *v, ScrPubFn fn) {
|
||||
if (!v || *(size_t *)v == SIZE_MAX || ctx->refusal) return;
|
||||
if (!v || SCR_RC_COUNT(v) == SCR_RC_IMMORTAL || ctx->refusal) return;
|
||||
if (fn == scr_pub_str_fn) { /* a leaf: mark it now */
|
||||
scr_pub_str_fn(v, ctx);
|
||||
return;
|
||||
@@ -120,7 +125,9 @@ void scr_pub_push(ScrPub *ctx, void *v, ScrPubFn fn) {
|
||||
* touch the objects again. */
|
||||
bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind) {
|
||||
size_t *rc = obj;
|
||||
if (*rc == SIZE_MAX || ctx->refusal) return false;
|
||||
if (SCR_RC_COUNT(obj) == SCR_RC_IMMORTAL || ctx->refusal) return false;
|
||||
bool class_id = (kind & SCR_PUB_CLASS_ID) != 0;
|
||||
kind &= ~SCR_PUB_CLASS_ID;
|
||||
if (ctx->logged == ctx->log_cap)
|
||||
ctx->log = scr_pub_grow(ctx->log, &ctx->log_cap, sizeof *ctx->log, ctx->log_inline);
|
||||
#ifdef SCR_RC_AUDIT
|
||||
@@ -135,10 +142,11 @@ bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind) {
|
||||
buf = scr_cyc_pub_take(obj) + 1;
|
||||
ctx->taken++;
|
||||
}
|
||||
size_t count = *rc;
|
||||
size_t count = class_id ? SCR_RC_COUNT(obj) : *rc;
|
||||
uint32_t logged = (uint32_t)count;
|
||||
if (class_id) buf |= SCR_PUB_LOG_COUNT;
|
||||
#if SIZE_MAX > UINT32_MAX
|
||||
if (count >= SCR_PUB_RC_LIFETIME) {
|
||||
if (!class_id && count >= SCR_PUB_RC_LIFETIME) {
|
||||
if (count == (size_t)1 << 63) logged = SCR_PUB_RC_LIFETIME;
|
||||
else {
|
||||
if (ctx->nbig == ctx->big_cap) ctx->big = scr_pub_grow(ctx->big, &ctx->big_cap, sizeof *ctx->big, NULL);
|
||||
@@ -148,7 +156,8 @@ bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind) {
|
||||
}
|
||||
#endif
|
||||
*e = (ScrPubLog){(uintptr_t)obj, logged, buf};
|
||||
*rc = SIZE_MAX;
|
||||
if (class_id) SCR_RC_COUNT(obj) = SCR_RC_IMMORTAL;
|
||||
else *rc = SIZE_MAX;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -372,7 +381,7 @@ static bool scr_pub_keep_ok(size_t bytes) {
|
||||
}
|
||||
|
||||
void scr_publish(void *root, ScrPubFn fn) {
|
||||
if (!root || *(size_t *)root == SIZE_MAX) return;
|
||||
if (!root || SCR_RC_COUNT(root) == SCR_RC_IMMORTAL) return;
|
||||
ScrPub ctx;
|
||||
if (scr_pub_keep.stack) {
|
||||
ctx.stack = scr_pub_keep.stack;
|
||||
@@ -417,13 +426,18 @@ void scr_publish(void *root, ScrPubFn fn) {
|
||||
for (size_t i = ctx.logged; i-- > 0;) {
|
||||
const ScrPubLog *e = &ctx.log[i];
|
||||
void *obj = scr_pub_log_obj(e);
|
||||
size_t rc = e->rc;
|
||||
uint32_t buf = e->buf & ~SCR_PUB_LOG_COUNT;
|
||||
if (e->buf & SCR_PUB_LOG_COUNT) {
|
||||
SCR_RC_COUNT(obj) = (ScrRcCount)e->rc;
|
||||
} else {
|
||||
size_t rc = e->rc;
|
||||
#if SIZE_MAX > UINT32_MAX
|
||||
if (e->rc == SCR_PUB_RC_LIFETIME) rc = (size_t)1 << 63;
|
||||
else if (e->rc == SCR_PUB_RC_BIG) rc = ctx.big[--ctx.nbig];
|
||||
if (e->rc == SCR_PUB_RC_LIFETIME) rc = (size_t)1 << 63;
|
||||
else if (e->rc == SCR_PUB_RC_BIG) rc = ctx.big[--ctx.nbig];
|
||||
#endif
|
||||
*(size_t *)obj = rc;
|
||||
if (e->buf) scr_cyc_pub_restore(obj, e->buf - 1);
|
||||
*(size_t *)obj = rc;
|
||||
}
|
||||
if (buf) scr_cyc_pub_restore(obj, buf - 1);
|
||||
}
|
||||
if (ctx.taken) scr_cyc_pub_fill();
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, ctx.refusal, strlen(ctx.refusal));
|
||||
|
||||
@@ -6,6 +6,41 @@
|
||||
|
||||
#include "scr_core.h"
|
||||
|
||||
/* ── reference counts ─────────────────────────────────────────────────
|
||||
* Every heap object's first member is its `size_t rc` word. On 64-bit
|
||||
* targets the reference count is the word's low 32 bits: an emitted
|
||||
* instance of a class in a program-rooted hierarchy keeps its class id (its
|
||||
* class's preorder number + 1) in the high 32 bits (llvm/classes.ts
|
||||
* classIdRcWord), and no other object's count reaches 2^32. A count of SCR_RC_IMMORTAL marks
|
||||
* an immortal object; for every object but those class instances the whole
|
||||
* word is then SIZE_MAX. Code that reads or writes the count of an object of
|
||||
* any kind goes through SCR_RC_COUNT. */
|
||||
#if SIZE_MAX > UINT32_MAX
|
||||
typedef uint32_t ScrRcCount;
|
||||
#ifdef __BYTE_ORDER__
|
||||
_Static_assert(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__,
|
||||
"the reference count is the low half of the rc word, at offset 0");
|
||||
#endif
|
||||
#else
|
||||
typedef size_t ScrRcCount;
|
||||
#endif
|
||||
#define SCR_RC_IMMORTAL ((ScrRcCount)-1)
|
||||
#define SCR_RC_COUNT(obj) (*(ScrRcCount *)(obj))
|
||||
|
||||
/* The class id in the high half of `obj`'s rc word, or 0 for an object
|
||||
* without one: the high half of any other object's word is 0, or 0xffffffff
|
||||
* (immortal) or 0x80000000 (program-lifetime), and a class id is never one of
|
||||
* these. */
|
||||
static inline uint32_t scr_rc_class_id(const void *obj) {
|
||||
#if SIZE_MAX > UINT32_MAX
|
||||
uint32_t high = (uint32_t)(*(const size_t *)obj >> 32);
|
||||
return high == UINT32_MAX || high == UINT32_C(0x80000000) ? 0 : high;
|
||||
#else
|
||||
(void)obj;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ── cycle collection (scr_cycle.c) ───────────────────────────────────
|
||||
* Reference counting alone cannot free cycles, so every object that can
|
||||
* participate in one — capture boxes, heap closures, unions, promises, and
|
||||
@@ -591,6 +626,10 @@ enum {
|
||||
SCR_PUB_KIND_OBJECT = 4,
|
||||
SCR_PUB_KIND_UNION = 5,
|
||||
SCR_PUB_KIND_DYN = 6,
|
||||
/* Or'ed into a kind: an object whose rc word keeps its class id in
|
||||
* the high half (SCR_RC_COUNT). The mark, and a refusal's restore, write
|
||||
* only the count. */
|
||||
SCR_PUB_CLASS_ID = 0x100,
|
||||
};
|
||||
void scr_publish(void *root, ScrPubFn fn);
|
||||
void scr_pub_push(ScrPub *ctx, void *v, ScrPubFn fn);
|
||||
|
||||
@@ -666,11 +666,11 @@ static void scr_rgn_queue(ScrRgnWalk *w, size_t entry, void *p, int kind, void (
|
||||
* region counted as explained. RC-audit builds still enter it, and stop if
|
||||
* the walk reaches it twice. */
|
||||
static void scr_rgn_enter(ScrRgnWalk *w, void *p, int kind, void (*walk)(void *, ScrRgnWalk *)) {
|
||||
if (!p || *(size_t *)p == SIZE_MAX) return; /* immortal: never freed, not walked */
|
||||
if (!p || SCR_RC_COUNT(p) == SCR_RC_IMMORTAL) return; /* immortal: never freed, not walked */
|
||||
#ifdef SCR_RGN_DEBUG
|
||||
bool single = w->src == 0 && *(size_t *)p == 1 && !w->debug;
|
||||
bool single = w->src == 0 && SCR_RC_COUNT(p) == 1 && !w->debug;
|
||||
#else
|
||||
bool single = w->src == 0 && *(size_t *)p == 1;
|
||||
bool single = w->src == 0 && SCR_RC_COUNT(p) == 1;
|
||||
#endif
|
||||
#ifndef SCR_RC_AUDIT
|
||||
if (single) {
|
||||
@@ -922,7 +922,7 @@ static int64_t scr_rgn_settle(ScrRgnWalk *w, size_t *black_out) {
|
||||
size_t top = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
ScrRgnEntry *x = &w->ents[i];
|
||||
if (*(size_t *)x->obj != (size_t)x->found) {
|
||||
if (SCR_RC_COUNT(x->obj) != (ScrRcCount)x->found) {
|
||||
x->black = true;
|
||||
stack[top++] = i;
|
||||
}
|
||||
@@ -1041,7 +1041,7 @@ static void scr_rgn_report(const ScrRegion *r, const ScrRgnWalk *w, int64_t unex
|
||||
}
|
||||
for (size_t i = 0; i < w->nents; i++) {
|
||||
const ScrRgnEntry *x = &w->ents[i];
|
||||
if (*(size_t *)x->obj == (size_t)x->found) continue; /* not referenced from outside itself */
|
||||
if (SCR_RC_COUNT(x->obj) == (ScrRcCount)x->found) continue; /* not referenced from outside itself */
|
||||
if (printed == lines) {
|
||||
more++;
|
||||
continue;
|
||||
@@ -1055,7 +1055,7 @@ static void scr_rgn_report(const ScrRegion *r, const ScrRgnWalk *w, int64_t unex
|
||||
}
|
||||
const ScrRgnFrom *f = &w->from[at];
|
||||
fprintf(stderr, " held by %s.%s, with %zu references, %lld of them from what the region owns\n",
|
||||
f->cls ? f->cls->name : "?", f->cls ? scr_rgn_field_name(f->cls, f->field) : "?", *(size_t *)x->obj,
|
||||
f->cls ? f->cls->name : "?", f->cls ? scr_rgn_field_name(f->cls, f->field) : "?", (size_t)SCR_RC_COUNT(x->obj),
|
||||
(long long)x->found);
|
||||
}
|
||||
if (more) fprintf(stderr, " %zu more\n", more);
|
||||
@@ -1111,7 +1111,7 @@ static bool scr_rgn_try_free(ScrRgnThread *t, ScrRegion *r, bool tentative) {
|
||||
for (ScrRgnChunk *c = r->chunks; c; c = c->next) c->found = 0;
|
||||
size_t objects = 0;
|
||||
SCR_RGN_EACH(r, s, o) {
|
||||
if (*(size_t *)o == SIZE_MAX) {
|
||||
if (SCR_RC_COUNT(o) == SCR_RC_IMMORTAL) {
|
||||
/* Published: other threads may hold it, uncounted. */
|
||||
r->state = SCR_RGN_PINNED;
|
||||
t->stats.pinned++;
|
||||
|
||||
@@ -29,7 +29,7 @@ static uint32_t scr_slots_index(void *obj, uint32_t *word) {
|
||||
if (index != 0) return index;
|
||||
index = atomic_fetch_add_explicit(&scr_slots_next, 1, memory_order_relaxed);
|
||||
if (index == 0) scr_trap("scriptc: ThreadSlots ran out of slot indices\n");
|
||||
if (*(size_t *)obj != SIZE_MAX) {
|
||||
if (SCR_RC_COUNT(obj) != SCR_RC_IMMORTAL) {
|
||||
__atomic_store_n(word, index, __ATOMIC_RELAXED);
|
||||
return index;
|
||||
}
|
||||
|
||||
@@ -4740,12 +4740,14 @@ ScrStream *scr_stream_rl_from_dyn(const ScrDyn *value, bool output) {
|
||||
if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 &&
|
||||
!memcmp(value->v.typed_ref.type_key, "object:", 7)) {
|
||||
ScrStream *stream = scr_dyn_typed_ref_unbox(value);
|
||||
const ScrVt *vt = stream->vt;
|
||||
bool duplex = vt == &scr_duplex_vt || vt == &scr_transform_vt || vt == &scr_passthrough_vt ||
|
||||
scr_vt_within(vt, &scr_duplex_vt);
|
||||
bool ok = duplex || (output ? vt == &scr_writable_vt || scr_vt_within(vt, &scr_writable_vt)
|
||||
: vt == &scr_readable_vt || scr_vt_within(vt, &scr_readable_vt));
|
||||
if (ok) return stream;
|
||||
const ScrVt *vt = scr_stream_vt_of(stream);
|
||||
if (vt) {
|
||||
bool duplex = vt == &scr_duplex_vt || vt == &scr_transform_vt || vt == &scr_passthrough_vt ||
|
||||
scr_vt_within(vt, &scr_duplex_vt);
|
||||
bool ok = duplex || (output ? vt == &scr_writable_vt || scr_vt_within(vt, &scr_writable_vt)
|
||||
: vt == &scr_readable_vt || scr_vt_within(vt, &scr_readable_vt));
|
||||
if (ok) return stream;
|
||||
}
|
||||
value->v.typed_ref.release(stream);
|
||||
}
|
||||
static const char in_m[] = "createInterface with an input that is not a readable stream has no lowering";
|
||||
@@ -6411,9 +6413,9 @@ static ScrStream *scr_stream_view_receiver(const ScrDyn *value) {
|
||||
if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 &&
|
||||
!memcmp(value->v.typed_ref.type_key, "object:", 7)) {
|
||||
ScrStream *stream = scr_dyn_typed_ref_unbox(value);
|
||||
const ScrVt *vt = stream->vt;
|
||||
if ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
|
||||
(vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post)) return stream;
|
||||
const ScrVt *vt = scr_stream_vt_of(stream);
|
||||
if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
|
||||
(vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) return stream;
|
||||
value->v.typed_ref.release(stream);
|
||||
}
|
||||
scr_dyn_arg_type_fail("destination", "an instance of Stream", value);
|
||||
@@ -6716,9 +6718,9 @@ ScrStream *scr_stream_unbox_dyn(const ScrDyn *value, const ScrStr *message) {
|
||||
if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 &&
|
||||
!memcmp(value->v.typed_ref.type_key, "object:", 7)) {
|
||||
ScrStream *stream = scr_dyn_typed_ref_unbox(value);
|
||||
const ScrVt *vt = stream->vt;
|
||||
if ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
|
||||
(vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post)) return stream;
|
||||
const ScrVt *vt = scr_stream_vt_of(stream);
|
||||
if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) ||
|
||||
(vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) return stream;
|
||||
value->v.typed_ref.release(stream);
|
||||
}
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020");
|
||||
|
||||
@@ -48,6 +48,13 @@ typedef struct ScrStream {
|
||||
ScrStreamState *st;
|
||||
} ScrStream;
|
||||
|
||||
/* The vtable of an object boxed as an "object:" typed ref, which may be any
|
||||
* program class instance: NULL for one with a class id in its rc word
|
||||
* (scr_rc.h), which has no vtable word and is no stream. */
|
||||
static inline const ScrVt *scr_stream_vt_of(const ScrStream *s) {
|
||||
return scr_rc_class_id(s) == 0 ? s->vt : NULL;
|
||||
}
|
||||
|
||||
extern SCR_TL ScrVt scr_readable_vt, scr_writable_vt, scr_duplex_vt,
|
||||
scr_transform_vt, scr_passthrough_vt; /* main() stamps pre/post */
|
||||
|
||||
|
||||
@@ -23,7 +23,11 @@ ScrStream *scr_stream_dyn_native(const ScrDyn *value, double side) {
|
||||
memcmp(value->v.typed_ref.type_key, "object:", 7) != 0)
|
||||
return NULL;
|
||||
ScrStream *stream = scr_dyn_typed_ref_unbox(value);
|
||||
const ScrVt *vt = stream->vt;
|
||||
const ScrVt *vt = scr_stream_vt_of(stream);
|
||||
if (vt == NULL) {
|
||||
value->v.typed_ref.release(stream);
|
||||
return NULL;
|
||||
}
|
||||
bool duplex = scr_stream_dyn_within(vt, &scr_duplex_vt) ||
|
||||
scr_stream_dyn_within(vt, &scr_transform_vt) ||
|
||||
scr_stream_dyn_within(vt, &scr_passthrough_vt);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Instances of a class hierarchy, held as untyped values, where the stream
|
||||
// adapters probe for a native stream: pipe, pipeline, readline, and
|
||||
// instanceof checks against the stream classes. Their first field is a
|
||||
// number, which sits where a stream's vtable word would be.
|
||||
import { Duplex, PassThrough, Readable, Transform, Writable, pipeline } from "node:stream";
|
||||
import { createInterface } from "node:readline";
|
||||
|
||||
class Shape {
|
||||
size = 3.5;
|
||||
area(): number {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
class Square extends Shape {
|
||||
override area(): number {
|
||||
return this.size * this.size;
|
||||
}
|
||||
}
|
||||
|
||||
function attempt(label: string, run: () => unknown): void {
|
||||
try {
|
||||
run();
|
||||
console.log(label, "ok");
|
||||
} catch (error) {
|
||||
console.log(label, "throws", error instanceof Error);
|
||||
}
|
||||
}
|
||||
|
||||
const shape: any = new Square();
|
||||
try {
|
||||
Readable.from(["a"]).pipe(shape);
|
||||
} catch (error) {
|
||||
console.log("pipe", (error as Error).name);
|
||||
}
|
||||
attempt("pipeline", () => pipeline(shape, new PassThrough(), () => {}));
|
||||
attempt("readline-input", () => createInterface({ input: shape }));
|
||||
console.log("instanceof", shape instanceof Readable, shape instanceof Writable, shape instanceof Duplex);
|
||||
console.log("instanceof-transform", shape instanceof Transform, shape instanceof PassThrough);
|
||||
console.log("area", (shape as Square).area());
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4000, "san": 4500 }
|
||||
@@ -100,7 +100,7 @@ console.log(a.id(), a.speak(), b.speak());
|
||||
// base-typed call site dispatches through it: both classes are
|
||||
// constructed, so the loaded slot selects a direct call of each.
|
||||
expect(llvm).toMatch(/%sc_vtt_Animal = type \{ %ScrVt, ptr \}/);
|
||||
expect(llvm).toMatch(/@sc_vtable_Dog = [^\n]*ptr @sc_b?f__x25_Dog_speak\b/);
|
||||
expect(llvm).toMatch(/%sc_vtt_Animal \{[^\n]*ptr @sc_b?f__x25_Dog_speak\b[^\n]*; class Dog\n/);
|
||||
expect(llvm).toMatch(
|
||||
/getelementptr inbounds %sc_vtt_Animal, ptr %\w+, i64 0, i32 1[\s\S]*icmp eq ptr %\w+, @sc_b?f__x25_(?:Animal|Dog)_speak\n/,
|
||||
);
|
||||
@@ -168,7 +168,9 @@ console.log(c.v, c.label, plain.label);
|
||||
);
|
||||
// The overridden getter dispatches through its slot...
|
||||
expect(llvm).toMatch(/%sc_vtt_Cell = type \{ %ScrVt, ptr \}/);
|
||||
expect(llvm).toMatch(/@sc_vtable_UpperCell = [^\n]*ptr @sc_b?f__x25_UpperCell_get_x3a_label\b/);
|
||||
expect(llvm).toMatch(
|
||||
/%sc_vtt_Cell \{[^\n]*ptr @sc_b?f__x25_UpperCell_get_x3a_label\b[^\n]*; class UpperCell\n/,
|
||||
);
|
||||
expect(llvm).toMatch(
|
||||
/getelementptr inbounds %sc_vtt_Cell, ptr %\w+, i64 0, i32 1[\s\S]*icmp eq ptr %\w+, @sc_b?f__x25_(?:Cell|UpperCell)_get_x3a_label\n/,
|
||||
);
|
||||
@@ -265,7 +267,7 @@ console.log(b.v, plain.v);
|
||||
// dispatch dynamically here.
|
||||
expect(llvm).toMatch(/%sc_vtt_Box = type \{ %ScrVt, ptr, ptr \}/);
|
||||
expect(llvm).toMatch(
|
||||
/@sc_vtable_SealedBox = [^\n]*ptr @sc_b?f__x25_SealedBox_get_x3a_v(\.virtual)?, ptr @sc_b?f__x25_SealedBox_set_x3a_v\b/,
|
||||
/%sc_vtt_Box \{[^\n]*ptr @sc_b?f__x25_SealedBox_get_x3a_v(\.virtual)?, ptr @sc_b?f__x25_SealedBox_set_x3a_v\b[^\n]*; class SealedBox\n/,
|
||||
);
|
||||
expect(llvm).toMatch(/getelementptr inbounds %sc_vtt_Box, ptr %\w+, i64 0, i32 2/);
|
||||
expect(llvm).toMatch(/sc_f__x25_SealedBox_set_x3a_v/);
|
||||
|
||||
@@ -243,7 +243,11 @@ export function emissionCases(): EmissionCase[] {
|
||||
const forest = structuredClone(hierarchy);
|
||||
forest.classes!.push(cls("Unrelated"), cls("Leaf", "Child"));
|
||||
forest.classes![3]!.fields = [...child.fields];
|
||||
add("class forest numbering", forest, ["class Leaf (vt at 1)", "class Unrelated { value }"]);
|
||||
add("class forest numbering", forest, [
|
||||
"class Leaf (class id in rc)",
|
||||
"class Unrelated { value }",
|
||||
"@sc_vta_Base",
|
||||
]);
|
||||
|
||||
const abstract = emissionModule();
|
||||
const abstractRoot = cls("Abstract");
|
||||
|
||||
Reference in New Issue
Block a user