From b452a06425b1aa3c3355367aacb8e43a5f827e7b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 00:15:31 -0500 Subject: [PATCH 1/2] Keep the class id of hierarchy instances in the rc word On 64-bit targets, an instance of a class in a program-rooted hierarchy has no vtable word. The rc word holds the class id (the class's preorder number + 1) in its high 32 bits and the reference count in its low 32 bits. Retain and release update the count as an i32 at offset 0; instanceof and the other preorder tests read the id at offset 4; virtual calls and the last release through a base-typed reference index the hierarchy's constant vtable array by the id. The runtime reads every object's count through SCR_RC_COUNT. Runtime-rooted hierarchies (Error, EventEmitter, EventTarget, Event and the streams), standalone classes and 32-bit targets keep the vtable word. --- .../compiler/src/backend/llvm/classes.test.ts | 30 ++- packages/compiler/src/backend/llvm/classes.ts | 178 +++++++++++++++--- packages/compiler/src/backend/llvm/emitter.ts | 59 +++--- .../compiler/src/backend/llvm/expr-calls.ts | 16 +- .../compiler/src/backend/llvm/expr-dynamic.ts | 36 ++++ .../src/backend/llvm/expr-stream-bridges.ts | 39 ++-- .../src/backend/llvm/narrow-guards.ts | 77 +++++++- packages/compiler/src/backend/llvm/publish.ts | 57 ++++-- packages/compiler/src/backend/llvm/regions.ts | 6 +- packages/compiler/src/backend/llvm/shapes.ts | 20 +- packages/compiler/src/backend/mangle.ts | 7 + packages/runtime/src/scr_cycle.c | 17 +- packages/runtime/src/scr_message.c | 2 +- packages/runtime/src/scr_publish.c | 38 ++-- packages/runtime/src/scr_rc.h | 39 ++++ packages/runtime/src/scr_region.c | 14 +- packages/runtime/src/scr_slots.c | 2 +- packages/runtime/src/scr_stream.c | 8 +- tests/harness/inheritance.test.ts | 8 +- tests/harness/self-hosting-emission-cases.ts | 6 +- 20 files changed, 504 insertions(+), 155 deletions(-) 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 64a535027..e80d3b8f1 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -257,6 +257,7 @@ import { buildClassGraph, classMembershipIntervals, classFieldIndex, + classPreLines, classStructSym, programLifetimeClass, type LlClassMeta, @@ -1431,7 +1432,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, @@ -2782,10 +2783,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( @@ -3118,20 +3125,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; } @@ -5021,6 +5028,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) @@ -5059,15 +5067,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"), @@ -5107,22 +5117,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 8a24fcf23..0d56a1f6b 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -1017,6 +1017,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 8a53b5fa8..9281a882b 100644 --- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts +++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts @@ -17,7 +17,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, @@ -229,10 +229,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)]); @@ -725,16 +728,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()) { @@ -1313,16 +1309,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.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/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/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_message.c b/packages/runtime/src/scr_message.c index 1d01e5573..b1a74caa6 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 eb28fb2e7..3ffe5a25a 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 @@ -587,6 +622,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 c2d27e7ce..731d7c523 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -6384,9 +6384,11 @@ 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; + /* A class instance with a class id in its rc word (scr_rc.h) has no + * vtable word: its class is no stream. */ + const ScrVt *vt = scr_rc_class_id(stream) == 0 ? stream->vt : NULL; + 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); diff --git a/tests/harness/inheritance.test.ts b/tests/harness/inheritance.test.ts index c0ce3d7d4..057ab7d7f 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/, ); @@ -269,7 +271,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"); From fa098689f1236c97dc9de5e58b196003299fc72b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 18:09:22 -0500 Subject: [PATCH 2/2] Check for a class id before reading a stream's vtable word The stream probes for untyped values (pipe destinations, pipeline stages, readline input and output, and the checks against the stream classes) read the vtable word of any boxed object. Instances with a class id in the rc word have no vtable word, so a hierarchy instance whose first field is a number crashed those probes. scr_stream_vt_of answers NULL for them, and each probe treats that as no stream. --- .../class-id-instances-as-streams.ts.json | 6 +++ packages/runtime/src/scr_dyn_builtin_stream.c | 12 +++--- packages/runtime/src/scr_stream.c | 24 ++++++------ packages/runtime/src/scr_stream.h | 7 ++++ packages/runtime/src/scr_stream_dyn.c | 6 ++- tests/corpus/class-id-instances-as-streams.ts | 39 +++++++++++++++++++ .../class-id-instances-as-streams.ts.json | 1 + 7 files changed, 77 insertions(+), 18 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-id-instances-as-streams.ts.json create mode 100644 tests/corpus/class-id-instances-as-streams.ts create mode 100644 tests/harness/corpus-costs/class-id-instances-as-streams.ts.json 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_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_stream.c b/packages/runtime/src/scr_stream.c index da28cec7a..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,7 @@ 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); - /* A class instance with a class id in its rc word (scr_rc.h) has no - * vtable word: its class is no stream. */ - const ScrVt *vt = scr_rc_class_id(stream) == 0 ? stream->vt : NULL; + 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); @@ -6718,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 }