diff --git a/packages/compiler/src/backend/llvm/classes.test.ts b/packages/compiler/src/backend/llvm/classes.test.ts index be1c58941..8a2a4659a 100644 --- a/packages/compiler/src/backend/llvm/classes.test.ts +++ b/packages/compiler/src/backend/llvm/classes.test.ts @@ -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 }); diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index f5a882bbf..5ad29430d 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -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, + sizeType: "i32" | "i64" = "i64", ): Map { const metaMap = new Map(); 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(); 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 diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 3c0ab2378..00b4c59a8 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -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}`, ); diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts index b49c8895e..43dfce459 100644 --- a/packages/compiler/src/backend/llvm/expr-calls.ts +++ b/packages/compiler/src/backend/llvm/expr-calls.ts @@ -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}`, ); diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 575d8c195..da3e1bfa4 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -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); diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts index bbd0226fb..0952ea993 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -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(), diff --git a/packages/compiler/src/backend/llvm/narrow-guards.ts b/packages/compiler/src/backend/llvm/narrow-guards.ts index 910af6f79..4744bed7c 100644 --- a/packages/compiler/src/backend/llvm/narrow-guards.ts +++ b/packages/compiler/src/backend/llvm/narrow-guards.ts @@ -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; diff --git a/packages/compiler/src/backend/llvm/publish.ts b/packages/compiler/src/backend/llvm/publish.ts index 4f807424b..15221bbc3 100644 --- a/packages/compiler/src/backend/llvm/publish.ts +++ b/packages/compiler/src/backend/llvm/publish.ts @@ -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); diff --git a/packages/compiler/src/backend/llvm/regions.test.ts b/packages/compiler/src/backend/llvm/regions.test.ts index 51ceb21c8..f2fed6b8e 100644 --- a/packages/compiler/src/backend/llvm/regions.test.ts +++ b/packages/compiler/src/backend/llvm/regions.test.ts @@ -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). diff --git a/packages/compiler/src/backend/llvm/regions.ts b/packages/compiler/src/backend/llvm/regions.ts index 1ec46a5b1..e7ad39f7c 100644 --- a/packages/compiler/src/backend/llvm/regions.ts +++ b/packages/compiler/src/backend/llvm/regions.ts @@ -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)}`, diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 9cceaa443..40803a5ff 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -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)` : ""}`, diff --git a/packages/compiler/src/backend/llvm/thread-slots.test.ts b/packages/compiler/src/backend/llvm/thread-slots.test.ts index dba4453b3..b8d8dccf9 100644 --- a/packages/compiler/src/backend/llvm/thread-slots.test.ts +++ b/packages/compiler/src/backend/llvm/thread-slots.test.ts @@ -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( diff --git a/packages/compiler/src/backend/mangle.ts b/packages/compiler/src/backend/mangle.ts index 43c9cea71..da6cbe0f5 100644 --- a/packages/compiler/src/backend/mangle.ts +++ b/packages/compiler/src/backend/mangle.ts @@ -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 diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-id-instances-as-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-id-instances-as-streams.ts.json new file mode 100644 index 000000000..01d871cb3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-id-instances-as-streams.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-id-instances-as-streams.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_cycle.c b/packages/runtime/src/scr_cycle.c index fd5d64c81..bed185097 100644 --- a/packages/runtime/src/scr_cycle.c +++ b/packages/runtime/src/scr_cycle.c @@ -165,8 +165,9 @@ void scr_weak_dispose(void *object) { #include #include -/* 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); diff --git a/packages/runtime/src/scr_dyn_builtin_stream.c b/packages/runtime/src/scr_dyn_builtin_stream.c index b7295f59a..209c8c39a 100644 --- a/packages/runtime/src/scr_dyn_builtin_stream.c +++ b/packages/runtime/src/scr_dyn_builtin_stream.c @@ -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; } diff --git a/packages/runtime/src/scr_message.c b/packages/runtime/src/scr_message.c index 374b5469a..cb9bd8a23 100644 --- a/packages/runtime/src/scr_message.c +++ b/packages/runtime/src/scr_message.c @@ -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]; diff --git a/packages/runtime/src/scr_publish.c b/packages/runtime/src/scr_publish.c index 9dedc6844..b3ff2814d 100644 --- a/packages/runtime/src/scr_publish.c +++ b/packages/runtime/src/scr_publish.c @@ -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)); diff --git a/packages/runtime/src/scr_rc.h b/packages/runtime/src/scr_rc.h index 8611741da..10eb96d18 100644 --- a/packages/runtime/src/scr_rc.h +++ b/packages/runtime/src/scr_rc.h @@ -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); diff --git a/packages/runtime/src/scr_region.c b/packages/runtime/src/scr_region.c index cc094ef0b..a4025610c 100644 --- a/packages/runtime/src/scr_region.c +++ b/packages/runtime/src/scr_region.c @@ -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++; diff --git a/packages/runtime/src/scr_slots.c b/packages/runtime/src/scr_slots.c index 7c5bc7f88..84236c0d5 100644 --- a/packages/runtime/src/scr_slots.c +++ b/packages/runtime/src/scr_slots.c @@ -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; } diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 7c501f20c..7d7ec04ed 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -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"); diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 78290d8c4..99f58331c 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -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 */ diff --git a/packages/runtime/src/scr_stream_dyn.c b/packages/runtime/src/scr_stream_dyn.c index 0859c4e07..11dcf6ee2 100644 --- a/packages/runtime/src/scr_stream_dyn.c +++ b/packages/runtime/src/scr_stream_dyn.c @@ -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); diff --git a/tests/corpus/class-id-instances-as-streams.ts b/tests/corpus/class-id-instances-as-streams.ts new file mode 100644 index 000000000..813e54ed2 --- /dev/null +++ b/tests/corpus/class-id-instances-as-streams.ts @@ -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()); diff --git a/tests/harness/corpus-costs/class-id-instances-as-streams.ts.json b/tests/harness/corpus-costs/class-id-instances-as-streams.ts.json new file mode 100644 index 000000000..07189e390 --- /dev/null +++ b/tests/harness/corpus-costs/class-id-instances-as-streams.ts.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 4500 } diff --git a/tests/harness/inheritance.test.ts b/tests/harness/inheritance.test.ts index 27a176764..df79a6235 100644 --- a/tests/harness/inheritance.test.ts +++ b/tests/harness/inheritance.test.ts @@ -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/); diff --git a/tests/harness/self-hosting-emission-cases.ts b/tests/harness/self-hosting-emission-cases.ts index cf3babf6f..efba5e019 100644 --- a/tests/harness/self-hosting-emission-cases.ts +++ b/tests/harness/self-hosting-emission-cases.ts @@ -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");