diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 0dfed5496..b232bbb30 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -284,9 +284,9 @@ import { classFieldIndex, classStructSym, type LlClassMeta, - vtEntriesFor, } from "./classes.js"; import { LlDyn, type DynHost } from "./dyn.js"; +import { VirtualBorrows } from "./virtual-borrows.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import { LlWalkers } from "./walkers.js"; import { isObjectArm, NULLABLE_ABSENT, NULLABLE_NULL, NullableUnions } from "./nullable-unions.js"; @@ -627,11 +627,8 @@ export class LlEmitter { /** The class graph (buildClassGraph): preorder numbering, hierarchy * membership, virtual slot lists. */ readonly classMeta: Map; - /** Parameters every implementation of a vtable slot borrows, keyed by - * `root\0slot index`, and each implementation's slot key (see - * virtualSlotBorrowed). */ - private readonly slotBorrowed = new Map>(); - private readonly implSlot = new Map(); + /** Which vtable slots borrow which parameters (virtual-borrows.ts). */ + private virtualBorrows!: VirtualBorrows; /** Class objects (classes as first-class values): className → the * interned .name literal ref — registered during body emission, the * statics and construct thunks assemble around the bodies. */ @@ -917,68 +914,26 @@ export class LlEmitter { lib: rec.lib, }); } - this.computeVirtualBorrowing(mod); - } - - /** Virtual dispatch normally keeps the owned ABI: the caller passes +1 - * and the method's owned entry releases what its body borrows. When every - * implementation of a slot has a borrowing body for the same parameter - * (call-lifetimes proves this per function, independent of the caller), - * the slot borrows that parameter instead: call sites pass it like a - * direct call to a borrowing body, so a receiver that survives the call - * is neither retained nor released (and, being cycle-capable, not - * buffered as a candidate either). Implementations that borrow more - * parameters than the slot get an adapter that releases the difference. */ - private computeVirtualBorrowing(mod: IrModule): void { - const impls = new Map>(); - const emitted = new Set((mod.classes ?? []).filter((c) => !c.runtime).map((c) => c.name)); - for (const meta of this.classMeta.values()) { - if (!meta.hierarchy || !emitted.has(meta.def.name)) continue; - vtEntriesFor(meta).forEach(({ slot, impl }, index) => { - if (impl === null) return; - const key = `${meta.root.def.name}\0${index}`; - const name = `%${impl.def.name}.${slot.method}`; - let set = impls.get(key); - if (!set) impls.set(key, (set = new Set())); - set.add(name); - this.implSlot.set(name, key); - }); - } - for (const [key, names] of impls) { - let common: number[] | null = null; - for (const name of names) { - const fn = this.fnByName.get(name); - const borrowed = this.callLifetimes.borrowed.get(name); - if (!fn || !borrowed || fn.captures !== undefined || fn.async || fn.generator) { - common = []; - break; - } - common = (common ?? [...borrowed]).filter((index) => borrowed.has(index)); - } - if (common !== null && common.length > 0) this.slotBorrowed.set(key, new Set(common)); - } + this.virtualBorrows = new VirtualBorrows( + mod, + this.classMeta, + this.fnByName, + this.callLifetimes.borrowed, + ); } /** The borrowed parameters of a hierarchy's vtable slot (empty: owned). */ virtualSlotBorrowed(rootName: string, slotIndex: number): ReadonlySet { - return this.slotBorrowed.get(`${rootName}\0${slotIndex}`) ?? NO_BORROWED; + return this.virtualBorrows.slot(rootName, slotIndex); } - /** The vtable entry for one implementation: its owned entry for an owned - * slot, its borrowing body when that borrows exactly the slot's - * parameters, otherwise its virtual adapter (emitOwnedCallAdapter). */ + /** The vtable entry for one implementation (VirtualBorrows.entry). */ virtualEntry(implFn: string): string { - const slot = this.implSlotBorrowed(implFn); - if (slot.size === 0) return mangleFunction(implFn); - const own = this.callLifetimes.borrowed.get(implFn)!; - return own.size === slot.size - ? mangleBorrowedFunction(implFn) - : `${mangleBorrowedFunction(implFn)}.virtual`; + return this.virtualBorrows.entry(implFn); } private implSlotBorrowed(implFn: string): ReadonlySet { - const key = this.implSlot.get(implFn); - return (key !== undefined && this.slotBorrowed.get(key)) || NO_BORROWED; + return this.virtualBorrows.implSlotBorrowed(implFn); } abiOffset(native64: number, wasm32: number): number { diff --git a/packages/compiler/src/backend/llvm/virtual-borrows.ts b/packages/compiler/src/backend/llvm/virtual-borrows.ts new file mode 100644 index 000000000..e57e4304e --- /dev/null +++ b/packages/compiler/src/backend/llvm/virtual-borrows.ts @@ -0,0 +1,83 @@ +import type { IrFunction, IrModule } from "../../ir/ir.js"; +import { mangleBorrowedFunction, mangleFunction } from "../mangle.js"; +import { vtEntriesFor, type LlClassMeta } from "./classes.js"; + +const NO_BORROWED: ReadonlySet = new Set(); + +/** Virtual dispatch normally keeps the owned ABI: the caller passes +1 and + * the method's owned entry releases what its body borrows. When every + * implementation of a slot has a borrowing body for the same parameter + * (call-lifetimes proves this per function, independent of the caller), the + * slot borrows that parameter instead: call sites pass it like a direct call + * to a borrowing body, so a receiver that survives the call is neither + * retained nor released (and, being cycle-capable, not buffered as a + * candidate either). Implementations that borrow more parameters than the + * slot get an adapter that releases the difference. + * + * Shared by the emitter and the self-hosted layout stage, so both store the + * same vtable entries. */ +export class VirtualBorrows { + /** Parameters every implementation of a vtable slot borrows, keyed by + * `root\0slot index`, and each implementation's slot key. */ + private readonly slotBorrowed = new Map>(); + private readonly implSlot = new Map(); + + constructor( + mod: IrModule, + classMeta: ReadonlyMap, + private readonly functions: ReadonlyMap, + private readonly borrowed: ReadonlyMap>, + ) { + const impls = new Map>(); + const emitted = new Set((mod.classes ?? []).filter((c) => !c.runtime).map((c) => c.name)); + for (const meta of classMeta.values()) { + if (!meta.hierarchy || !emitted.has(meta.def.name)) continue; + vtEntriesFor(meta).forEach(({ slot, impl }, index) => { + if (impl === null) return; + const key = `${meta.root.def.name}\0${index}`; + const name = `%${impl.def.name}.${slot.method}`; + let set = impls.get(key); + if (!set) impls.set(key, (set = new Set())); + set.add(name); + this.implSlot.set(name, key); + }); + } + for (const [key, names] of impls) { + let common: number[] | null = null; + for (const name of names) { + const fn = this.functions.get(name); + const own = this.borrowed.get(name); + if (!fn || !own || fn.captures !== undefined || fn.async || fn.generator) { + common = []; + break; + } + common = (common ?? [...own]).filter((index) => own.has(index)); + } + if (common !== null && common.length > 0) this.slotBorrowed.set(key, new Set(common)); + } + } + + /** The borrowed parameters of a hierarchy's vtable slot (empty: owned). */ + slot(rootName: string, slotIndex: number): ReadonlySet { + return this.slotBorrowed.get(`${rootName}\0${slotIndex}`) ?? NO_BORROWED; + } + + /** The borrowed parameters of the slot an implementation fills. */ + implSlotBorrowed(implFn: string): ReadonlySet { + const key = this.implSlot.get(implFn); + if (key === undefined) return NO_BORROWED; + return this.slotBorrowed.get(key) ?? NO_BORROWED; + } + + /** The vtable entry for one implementation: its owned entry for an owned + * slot, its borrowing body when that borrows exactly the slot's + * parameters, otherwise its virtual adapter (emitOwnedCallAdapter). */ + entry(implFn: string): string { + const slot = this.implSlotBorrowed(implFn); + if (slot.size === 0) return mangleFunction(implFn); + const own = this.borrowed.get(implFn)!; + return own.size === slot.size + ? mangleBorrowedFunction(implFn) + : `${mangleBorrowedFunction(implFn)}.virtual`; + } +} diff --git a/tests/fixtures/self-hosting/llvm-layouts.ts b/tests/fixtures/self-hosting/llvm-layouts.ts index 44e6ecf89..d5685fcb6 100644 --- a/tests/fixtures/self-hosting/llvm-layouts.ts +++ b/tests/fixtures/self-hosting/llvm-layouts.ts @@ -10,6 +10,9 @@ import { NULLABLE_ABSENT, NullableUnions } from "../../../packages/compiler/src/ import { emitFieldAbsentTest } from "../../../packages/compiler/src/backend/llvm/common.js"; import { undefinedArmTag } from "../../../packages/compiler/src/ir/analysis.js"; import { analyzeInt32Slots } from "../../../packages/compiler/src/ir/int32-slots.js"; +import { analyzeCallLifetimes } from "../../../packages/compiler/src/backend/llvm/call-lifetimes.js"; +import { NullableRefFields } from "../../../packages/compiler/src/backend/llvm/nullable-fields.js"; +import { VirtualBorrows } from "../../../packages/compiler/src/backend/llvm/virtual-borrows.js"; import { f64Lit } from "../../../packages/compiler/src/backend/llvm/common.js"; import { mangleClassObj } from "../../../packages/compiler/src/backend/mangle.js"; import { type IrType } from "../../../packages/compiler/src/ir/ir.js"; @@ -36,6 +39,7 @@ try { const traced = computeTraced(mod); const nullableUnions = new NullableUnions(mod); const unionsById = new Map((mod.unions ?? []).map((union) => [union.id, union])); + let virtualBorrows: VirtualBorrows | undefined; const host: ClassHost & WalkerHost = { declare: (decl) => { declarations.add(decl); }, needOom: () => { needsOom = true; }, @@ -71,6 +75,8 @@ try { B.line(`${absent} = icmp eq ptr ${value}, ${NULLABLE_ABSENT}`); return absent; }, + // Mirrors the emitter: borrowing vtable slots store the borrowing body. + virtualEntry: (implFn) => virtualBorrows!.entry(implFn), cstr: (text) => { let index = strings.indexOf(text); if (index === -1) { index = strings.length; strings.push(text); } @@ -79,6 +85,9 @@ try { }; const functions = new Map(mod.functions.map((fn) => [fn.name, fn])); const graph = buildClassGraph(mod, functions); + const nullableFields = new NullableRefFields(mod.classes ?? [], nullableUnions.boxedUnions(unionsById)); + const lifetimes = analyzeCallLifetimes(functions, (className, field) => nullableFields.get(className, field) !== null); + virtualBorrows = new VirtualBorrows(mod, graph, functions, lifetimes.borrowed); const classObjects = new Map(request.classObjects.map((name) => [name, { nameSym: `@name_${mangleClassObj(name)}` }])); const typeName = (type: IrType): string => type.kind === "void" ? "void" : type.kind === "f64" || type.kind === "date" ? "double" : type.kind === "bool" ? "i1" : "ptr"; // Mirrors the emitter: whole-program int32 fields store the exact i32. diff --git a/tests/harness/self-hosting-emission-cases.ts b/tests/harness/self-hosting-emission-cases.ts index cb0b6b7e2..ad846d0c3 100644 --- a/tests/harness/self-hosting-emission-cases.ts +++ b/tests/harness/self-hosting-emission-cases.ts @@ -238,7 +238,8 @@ export function emissionCases(): EmissionCase[] { concrete.methods = ["read"]; abstract.classes = [abstractRoot, concrete]; abstract.functions.push(method("Concrete", "read")); - add("abstract slot implementation", abstract, ["ptr null", "@sc_f__x25_Concrete_read"]); + // The implementation borrows its receiver, so the slot stores the borrowing body. + add("abstract slot implementation", abstract, ["ptr null", "@sc_bf__x25_Concrete_read"]); const incomplete = emissionModule(); const abstractChild = cls("AbstractChild", "Abstract");