mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
Share virtual-slot borrowing with the self-hosted layout stage
Vtables store the borrowing body of a method whose slot borrows its parameters, but that decision lived in private emitter state, so the self-hosting layout fixture kept emitting owned entries and no longer matched production (`@sc_f_...` vs `@sc_bf_...`). Move the decision into VirtualBorrows (virtual-borrows.ts), used by the emitter and by the fixture, which now runs the same call-lifetimes analysis. The slot lookup narrows explicitly instead of combining a union with `&&`/`||`, which the self-hosted subset rejects (SC1090). The abstract-slot emission case now expects the borrowing body, since its implementation borrows its receiver.
This commit is contained in:
@@ -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<string, LlClassMeta>;
|
||||
/** 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<string, ReadonlySet<number>>();
|
||||
private readonly implSlot = new Map<string, string>();
|
||||
/** 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<string, Set<string>>();
|
||||
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<number> {
|
||||
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<number> {
|
||||
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 {
|
||||
|
||||
@@ -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<number> = 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<string, ReadonlySet<number>>();
|
||||
private readonly implSlot = new Map<string, string>();
|
||||
|
||||
constructor(
|
||||
mod: IrModule,
|
||||
classMeta: ReadonlyMap<string, LlClassMeta>,
|
||||
private readonly functions: ReadonlyMap<string, IrFunction>,
|
||||
private readonly borrowed: ReadonlyMap<string, ReadonlySet<number>>,
|
||||
) {
|
||||
const impls = new Map<string, Set<string>>();
|
||||
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<number> {
|
||||
return this.slotBorrowed.get(`${rootName}\0${slotIndex}`) ?? NO_BORROWED;
|
||||
}
|
||||
|
||||
/** The borrowed parameters of the slot an implementation fills. */
|
||||
implSlotBorrowed(implFn: string): ReadonlySet<number> {
|
||||
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`;
|
||||
}
|
||||
}
|
||||
+9
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user