Keep the class id of hierarchy instances in the rc word (#1486)

This commit is contained in:
Chris Tate
2026-10-20 18:12:51 -05:00
committed by GitHub
28 changed files with 590 additions and 177 deletions
@@ -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 });
+151 -27
View File
@@ -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
+32 -27
View File
@@ -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;
+41 -16
View File
@@ -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)}`,
+15 -5
View File
@@ -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(
+7
View File
@@ -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
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/class-id-instances-as-streams.ts"
],
"diags": []
}
+9 -8
View File
@@ -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;
}
+1 -1
View File
@@ -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];
+26 -12
View File
@@ -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));
+39
View File
@@ -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);
+7 -7
View File
@@ -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++;
+1 -1
View File
@@ -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;
}
+14 -12
View File
@@ -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");
+7
View File
@@ -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 */
+5 -1
View File
@@ -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 }
+5 -3
View File
@@ -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/);
+5 -1
View File
@@ -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");