mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Give ProgramLifetime class instances a 32-bit header and packed fields (#1651)
This commit is contained in:
@@ -269,8 +269,9 @@ export function cycTypeDescriptor(name: string, trace: string, free: string, kin
|
||||
* expression) in `%o`, aborting on OOM, with the RC-audit allocation note
|
||||
* on the out-of-line path. With `cyc`, the object carries a collector
|
||||
* header (scr_cyc_alloc's contract). `stored` is the number of leading
|
||||
* object bytes the caller stores unconditionally right after (at least the
|
||||
* refcount word), which a recycled block need not clear. Labels use the
|
||||
* object bytes the caller stores unconditionally right after (the refcount
|
||||
* word, or the 4-byte header of a compact class instance), which a recycled
|
||||
* block need not clear; it is a multiple of 4 and at most `size`. Labels use the
|
||||
* `sa.` prefix; the lines end in an open block.
|
||||
*/
|
||||
export function emitObjectAlloc(
|
||||
@@ -350,7 +351,7 @@ function allocLines(
|
||||
` store ptr %sa.next, ptr %sa.fp`,
|
||||
` %sa.z = getelementptr inbounds i8, ptr %sa.b, i64 ${keep}`,
|
||||
` %sa.zn = sub i64 %sa.n, ${keep}`,
|
||||
` call void @llvm.memset.p0.i64(ptr align 8 %sa.z, i8 0, i64 %sa.zn, i1 false)`,
|
||||
` call void @llvm.memset.p0.i64(ptr align ${keep % 8 === 0 ? 8 : 4} %sa.z, i8 0, i64 %sa.zn, i1 false)`,
|
||||
` br label %sa.fast`,
|
||||
`sa.bump: ; fresh reservation memory is already zero`,
|
||||
` %sa.c1 = add i64 %sa.c, 1`,
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { DYN, F64, STRING, VOID, type IrModule } from "../../ir/ir.js";
|
||||
import { buildClassGraph, classMembershipIntervals, emitClassMembershipHelper } from "./classes.js";
|
||||
import {
|
||||
buildClassGraph,
|
||||
classIdLoadLines,
|
||||
classMembershipIntervals,
|
||||
emitClassMembershipHelper,
|
||||
type LlClassMeta,
|
||||
} from "./classes.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
import {
|
||||
mangleClassNew,
|
||||
mangleClassReleaseDirect,
|
||||
mangleClassRelease,
|
||||
mangleClassRetain,
|
||||
mangleClassStruct,
|
||||
mangleClassTrace,
|
||||
mangleRecordRelease,
|
||||
} from "../mangle.js";
|
||||
@@ -129,18 +136,19 @@ 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^31 under the class id (Node's preorder number
|
||||
// 0 + 1, Leaf's 1 + 1), not SIZE_MAX (which means published).
|
||||
// 32-bit header: the tag 0x70000000 and the class id (Node's preorder
|
||||
// number 0 + 1, Leaf's 1 + 1), without a count.
|
||||
const nw = body(mangleClassNew(cls));
|
||||
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).toContain(`store i32 ${0x70000000 + id}, ptr %o.vt`);
|
||||
expect(nw, cls).not.toContain("store i64");
|
||||
expect(nw, cls).not.toContain("scr_cyc_alloc");
|
||||
expect(llvm, cls).not.toContain(`@${mangleClassTrace(cls)}.cyctype`);
|
||||
// The RC audit (sanitize builds) learns that the program keeps
|
||||
// program-lifetime objects; other builds emit nothing for it.
|
||||
expect(nw, cls).toContain("call void @scr_obj_lifetime_note(ptr %o)");
|
||||
}
|
||||
expect(llvm).toContain(`%${mangleClassStruct("Leaf")} = type { i32, ptr }`);
|
||||
const plain = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false });
|
||||
expect(plain).not.toContain("scr_obj_lifetime_note");
|
||||
// The mortal Owner keeps its header, counts and possible-root step.
|
||||
@@ -149,6 +157,84 @@ test("ProgramLifetime hierarchies are never freed and take no reference counts",
|
||||
expect(body(mangleClassRelease("Owner"))).toContain("load i32, ptr %bufp");
|
||||
});
|
||||
|
||||
test("ProgramLifetime classes pack their fields after a 32-bit header", () => {
|
||||
const node = { kind: "object" as const, className: "Node" };
|
||||
const leaf = { kind: "object" as const, className: "Leaf" };
|
||||
const BOOL = { kind: "bool" as const };
|
||||
const nodeFields = [
|
||||
{ name: "parent", type: node },
|
||||
{ name: "flag", type: BOOL },
|
||||
{ name: "weight", type: F64 },
|
||||
];
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }],
|
||||
classes: [
|
||||
{ name: "Node", fields: nodeFields, methods: [], loc },
|
||||
{
|
||||
name: "Leaf",
|
||||
base: "Node",
|
||||
fields: [...nodeFields, { name: "next", type: leaf }, { name: "mark", type: BOOL }],
|
||||
methods: [],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
contracts: { programLifetime: ["Node"] },
|
||||
};
|
||||
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
|
||||
// Fields go widest first, each at the first offset that fits it. Node's
|
||||
// weight (an int32 slot: nothing stores a fraction) takes the word the
|
||||
// header shares (24 bytes, not 32); Leaf keeps Node's offsets and puts its
|
||||
// bool next to Node's (32 bytes, not 48).
|
||||
expect(llvm).toContain(`%${mangleClassStruct("Node")} = type { i32, i32, ptr, i8 }`);
|
||||
expect(llvm).toContain(`%${mangleClassStruct("Leaf")} = type { i32, i32, ptr, i8, i8, ptr }`);
|
||||
// Without the contract, the layout is the declared one.
|
||||
const mortal = emitLlvmModule({ ...mod, contracts: {} }, { pointerBits: 64 });
|
||||
expect(mortal).toContain(`%${mangleClassStruct("Leaf")} = type { i64, ptr, i8, i32, ptr, i8 }`);
|
||||
});
|
||||
|
||||
test("a recycled block is cleared from the end of a ProgramLifetime instance's 32-bit header", () => {
|
||||
const BOOL = { kind: "bool" as const };
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }],
|
||||
classes: [
|
||||
{ name: "Marker", fields: [], methods: [], loc },
|
||||
{ name: "Toggle", fields: [{ name: "on", type: BOOL }], methods: [], loc },
|
||||
],
|
||||
contracts: { programLifetime: ["Marker", "Toggle"] },
|
||||
};
|
||||
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
|
||||
for (const name of ["Marker", "Toggle"]) {
|
||||
const start = llvm.indexOf(`define internal ptr @${mangleClassNew(name)}(`);
|
||||
const body = llvm.slice(start, llvm.indexOf("\n}\n", start));
|
||||
// The constructor stores the 4-byte header itself; a 4-byte instance
|
||||
// (Marker) clears nothing, and Toggle's field at offset 4 is cleared.
|
||||
expect(body).toContain("%sa.z = getelementptr inbounds i8, ptr %sa.b, i64 4");
|
||||
expect(body).toContain("%sa.zn = sub i64 %sa.n, 4");
|
||||
expect(body).toContain("call void @llvm.memset.p0.i64(ptr align 4 %sa.z,");
|
||||
}
|
||||
});
|
||||
|
||||
test("invariant class id reads of compact instances leave out the published bit", () => {
|
||||
const group = ", !invariant.group !{}";
|
||||
const compact = (post: number) => ({ compact: true, root: { post } }) as unknown as LlClassMeta;
|
||||
// Publication sets the header word's top bit; ids below 2^16 are read
|
||||
// from the low 16 bits, which never change after construction.
|
||||
expect(classIdLoadLines(compact(40), "%o", "%id", group)).toEqual([
|
||||
" %id.h = load i16, ptr %o, !invariant.group !{}",
|
||||
" %id = zext i16 %id.h to i32",
|
||||
]);
|
||||
// Wider ids, and reads without the metadata, mask the whole word.
|
||||
const masked = [" %id.w = load i32, ptr %o", " %id = and i32 %id.w, 268435455"];
|
||||
expect(classIdLoadLines(compact(0xffff), "%o", "%id", group)).toEqual(masked);
|
||||
expect(classIdLoadLines(compact(40), "%o", "%id", "")).toEqual(masked);
|
||||
});
|
||||
|
||||
test("published ProgramLifetime instances use the published mark every other object uses", () => {
|
||||
const node = { kind: "object" as const, className: "Node" };
|
||||
const leaf = { kind: "object" as const, className: "Leaf" };
|
||||
@@ -212,19 +298,25 @@ test("published ProgramLifetime instances use the published mark every other obj
|
||||
contracts: { programLifetime: ["Node"] },
|
||||
};
|
||||
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
|
||||
// The walker marks each instance like a mortal object (SIZE_MAX, logged so
|
||||
// a refusal restores the program-lifetime count); untraced, so it never
|
||||
// leaves a collector buffer.
|
||||
// The walker marks each instance by the top bit of its 32-bit header,
|
||||
// logging the word so a refusal restores it; untraced, so it never leaves
|
||||
// a collector buffer.
|
||||
expect(llvm).toContain("call void @scr_publish(ptr");
|
||||
// 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.
|
||||
// Kind 4 (an object) | 0x200 (the 32-bit header: the mark sets its top
|
||||
// bit).
|
||||
expect(llvm).toMatch(/@scr_pub_mark\(ptr %ctx, ptr %o, i1 false, i32 516\)/);
|
||||
const plain = emitLlvmModule(mod, { pointerBits: 64, objectAudit: false });
|
||||
expect(plain).toContain("%mpub = or i32 %mrc, -2147483648");
|
||||
// Pushing a reference skips one published before: a set top bit.
|
||||
expect(llvm).toMatch(/%immortal0 = icmp slt i32 %rc0, 0/);
|
||||
// The walker dispatches on the class id in the header's low 28 bits.
|
||||
expect(llvm).toContain("%id = and i32 %idw, 268435455");
|
||||
// The frozen-object guard of the store tests the top bit: an unpublished
|
||||
// instance stays writable.
|
||||
const store =
|
||||
llvm.split(/^define /m).find((fn) => fn.includes("call void @scr_throw_published_field(")) ??
|
||||
"";
|
||||
expect(store).toMatch(/icmp eq i32 %\S+, -1/);
|
||||
expect(store).toMatch(/icmp slt i32 %\S+, 0/);
|
||||
expect(store).toContain("call void @scr_publish(ptr");
|
||||
// 32-bit targets use 2^31.
|
||||
const llvm32 = emitLlvmModule(mod, { pointerBits: 32 });
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from "../mangle.js";
|
||||
import { cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js";
|
||||
import { llvmCommentText } from "./common.js";
|
||||
import { INLINE_UNION_FIELD } from "./inline-unions.js";
|
||||
import { NullableRefFields } from "./nullable-fields.js";
|
||||
import type { Int32Slots } from "../../ir/int32-slots.js";
|
||||
import {
|
||||
@@ -88,6 +89,15 @@ export class LlClassMeta {
|
||||
* bits, and the hierarchy's vtables are one array indexed by the id
|
||||
* (mangleVtArray). */
|
||||
idInRc = false;
|
||||
/** A ProgramLifetime class with the 32-bit header (assignCompactLayouts):
|
||||
* the i32 at offset 0 is compactHeaderWord, with the class id in place of
|
||||
* the rc word's high half, and the fields are packed after it. */
|
||||
compact = false;
|
||||
/** Compact classes: the struct's elements by offset (the header, field
|
||||
* -1, then the fields by their index in def.fields). */
|
||||
compactElems: { offset: number; size: number; align: number; type: string; field: number }[] = [];
|
||||
/** Compact classes: each field's GEP index, by its index in def.fields. */
|
||||
compactSlots: number[] = [];
|
||||
|
||||
constructor(readonly def: IrClassDef) {
|
||||
this.root = this;
|
||||
@@ -371,6 +381,10 @@ export function classSlotWordIndex(meta: LlClassMeta): number {
|
||||
/** The GEP index where a class's own fields start: after the header words
|
||||
* and the slot word. */
|
||||
export function classFieldBase(meta: LlClassMeta): number {
|
||||
if (meta.compact)
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: compact class ${meta.def.name} packs its fields`,
|
||||
);
|
||||
return headerEnd(meta) + (meta.threadSlot ? 1 : 0);
|
||||
}
|
||||
|
||||
@@ -387,7 +401,12 @@ export function classFieldIndex(meta: LlClassMeta, field: string): { index: numb
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: unknown field ${field} on class ${meta.def.name}`,
|
||||
);
|
||||
return { index: classFieldBase(meta) + idx, type: meta.def.fields[idx]!.type };
|
||||
return { index: classFieldSlot(meta, idx), type: meta.def.fields[idx]!.type };
|
||||
}
|
||||
|
||||
/** The GEP index of the field at `idx` in `meta.def.fields`. */
|
||||
export function classFieldSlot(meta: LlClassMeta, idx: number): number {
|
||||
return meta.compact ? meta.compactSlots[idx]! : classFieldBase(meta) + idx;
|
||||
}
|
||||
|
||||
/** Lines that release the ThreadSlots values of the object `%o` before it
|
||||
@@ -472,8 +491,7 @@ export function classPreLines(
|
||||
): string[] {
|
||||
if (meta.idInRc)
|
||||
return [
|
||||
` ${pre}.idp = getelementptr inbounds i8, ptr ${obj}, i64 ${CLASS_ID_OFFSET}`,
|
||||
` ${pre}.id = load i32, ptr ${pre}.idp`,
|
||||
...classIdLoadLines(meta, obj, `${pre}.id`, ""),
|
||||
` ${pre}.n = add i32 ${pre}.id, -1 ; the class id is the preorder number + 1`,
|
||||
` ${pre} = zext i32 ${pre}.n to ${sizeType}`,
|
||||
];
|
||||
@@ -499,14 +517,39 @@ export function classIdVtLines(
|
||||
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}`,
|
||||
...classIdLoadLines(meta, `${obj}`, `${t}id`, 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`,
|
||||
];
|
||||
}
|
||||
|
||||
/** Lines that load the class id of the LlClassMeta.idInRc instance `obj`
|
||||
* into the i32 `id`: the high half of the rc word, or the low 28 bits of a
|
||||
* compact instance's header word (compactHeaderWord). `group` is the
|
||||
* metadata of the load (`, !invariant.group !{}` for a load LLVM may treat
|
||||
* as invariant), and it only goes on bytes that never change: publication
|
||||
* sets the top bit of a compact header word, so a compact instance's id is
|
||||
* then read as the word's low 16 bits, which hold the whole id when the
|
||||
* hierarchy's ids are below 2^16, and otherwise as the masked word without
|
||||
* the metadata. */
|
||||
export function classIdLoadLines(
|
||||
meta: LlClassMeta,
|
||||
obj: string,
|
||||
id: string,
|
||||
group: string,
|
||||
): string[] {
|
||||
if (meta.compact) {
|
||||
if (group !== "" && meta.root.post + 1 <= 0xffff)
|
||||
return [` ${id}.h = load i16, ptr ${obj}${group}`, ` ${id} = zext i16 ${id}.h to i32`];
|
||||
return [` ${id}.w = load i32, ptr ${obj}`, ` ${id} = and i32 ${id}.w, ${COMPACT_ID_MASK}`];
|
||||
}
|
||||
return [
|
||||
` ${id}p = getelementptr inbounds i8, ptr ${obj}, i64 ${CLASS_ID_OFFSET}`,
|
||||
` ${id} = load i32, ptr ${id}p${group}`,
|
||||
];
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
@@ -522,6 +565,134 @@ export function classIdRcWord(meta: LlClassMeta, lifetime: boolean): string {
|
||||
return (BigInt(meta.pre + 1) * 2n ** 32n + (lifetime ? 2n ** 31n : 1n)).toString();
|
||||
}
|
||||
|
||||
/** The 32-bit header of a compact class instance (LlClassMeta.compact):
|
||||
* COMPACT_HEADER_TAG, the class id (classIdRcWord's preorder number + 1, 0
|
||||
* outside a hierarchy) in the low 28 bits, and COMPACT_PUBLISHED once
|
||||
* publication marks the object. No other object's count reaches
|
||||
* 0x70000000, so the runtime reads a word of 0xf0000000 and up as published
|
||||
* for every object (scr_rc.h SCR_RC_PUBLISHED). */
|
||||
export const COMPACT_HEADER_TAG = 0x70000000;
|
||||
export const COMPACT_PUBLISHED = 0x80000000;
|
||||
export const COMPACT_ID_MASK = 0x0fffffff;
|
||||
|
||||
/** The leading bytes of a new instance that its allocation function stores
|
||||
* right after allocating it (emitObjectAlloc's `stored`), which a recycled
|
||||
* block therefore need not clear: a compact instance's 4-byte header word
|
||||
* (its fields start at offset 4), the 8-byte rc word, or the rc word and
|
||||
* the vtable word of a hierarchy member that keeps its class in a vtable. */
|
||||
export function storedHeaderBytes(meta: LlClassMeta): number {
|
||||
if (meta.compact) return 4;
|
||||
return meta.hierarchy && !meta.idInRc ? 16 : 8;
|
||||
}
|
||||
|
||||
/** The header word a compact instance is created with. */
|
||||
export function compactHeaderWord(meta: LlClassMeta): number {
|
||||
return COMPACT_HEADER_TAG | (meta.hierarchy ? meta.pre + 1 : 0);
|
||||
}
|
||||
|
||||
/** Byte size and alignment of a class field's LLVM storage type. */
|
||||
function fieldStorage(type: string): { size: number; align: number } {
|
||||
switch (type) {
|
||||
case "i8":
|
||||
return { size: 1, align: 1 };
|
||||
case "i32":
|
||||
return { size: 4, align: 4 };
|
||||
case "ptr":
|
||||
case "double":
|
||||
return { size: 8, align: 8 };
|
||||
case INLINE_UNION_FIELD:
|
||||
return { size: 16, align: 8 };
|
||||
default:
|
||||
throw new InternalCompilerError(`llvm emitter bug: class field storage ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** The LLVM storage type of each of a class's flattened fields. */
|
||||
export function classFieldTypes(host: ShapeHost, cls: IrClassDef, int32?: Int32Slots): string[] {
|
||||
// Whole-program int32 fields (int32-slots.ts) store the exact i32.
|
||||
return cls.fields.map((f) =>
|
||||
int32?.isField(cls.name, f.name)
|
||||
? "i32"
|
||||
: llFieldType(f.type, host.inlineUnions.classField(cls.name, f.name)),
|
||||
);
|
||||
}
|
||||
|
||||
/** Gives the classes `compact` admits (whole hierarchies: the test is the
|
||||
* root's) the 32-bit header and packed fields. A class keeps its base's
|
||||
* field offsets, and places each of its own fields, widest first, at the
|
||||
* first offset after the header that fits it: in a hole another field's
|
||||
* alignment left, in the base's tail padding, or at the end. So 32-bit and
|
||||
* smaller fields share 8-byte words. The struct lists the header and the
|
||||
* fields by offset, which LLVM's layout of the type reproduces (checked). */
|
||||
export function assignCompactLayouts(
|
||||
metaMap: ReadonlyMap<string, LlClassMeta>,
|
||||
compact: (root: LlClassMeta) => boolean,
|
||||
fieldTypes: (cls: IrClassDef) => string[],
|
||||
): void {
|
||||
const byPre = [...metaMap.values()].sort((a, b) => a.pre - b.pre);
|
||||
for (const meta of byPre) {
|
||||
if (meta.def.runtime === true || !compact(meta.root)) continue;
|
||||
const base = meta.base;
|
||||
const inherited = base?.compactElems ?? [
|
||||
{ offset: 0, size: 4, align: 4, type: "i32", field: -1 },
|
||||
];
|
||||
const baseCount = base?.def.fields.length ?? 0;
|
||||
if (base !== null && (!base.compact || !sameFieldPrefix(base.def, meta.def)))
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: compact class ${meta.def.name} does not extend its base's layout`,
|
||||
);
|
||||
const elems = inherited.map((e) => ({ ...e }));
|
||||
const types = fieldTypes(meta.def);
|
||||
const own = types
|
||||
.map((type, field) => ({ type, field, ...fieldStorage(type) }))
|
||||
.slice(baseCount)
|
||||
.sort((a, b) => b.size - a.size || a.field - b.field);
|
||||
for (const f of own) {
|
||||
const sorted = [...elems].sort((a, b) => a.offset - b.offset);
|
||||
let at = -1;
|
||||
let cursor = 0;
|
||||
for (const e of sorted) {
|
||||
const start = alignTo(cursor, f.align);
|
||||
if (start + f.size <= e.offset) {
|
||||
at = start;
|
||||
break;
|
||||
}
|
||||
cursor = Math.max(cursor, e.offset + e.size);
|
||||
}
|
||||
if (at < 0) at = alignTo(cursor, f.align);
|
||||
elems.push({ offset: at, size: f.size, align: f.align, type: f.type, field: f.field });
|
||||
}
|
||||
elems.sort((a, b) => a.offset - b.offset);
|
||||
// LLVM places each element at the next offset its alignment allows.
|
||||
let end = 0;
|
||||
for (const e of elems) {
|
||||
if (alignTo(end, e.align) !== e.offset)
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: compact layout of ${meta.def.name} is not LLVM's`,
|
||||
);
|
||||
end = e.offset + e.size;
|
||||
}
|
||||
const slots = new Array<number>(types.length).fill(-1);
|
||||
elems.forEach((e, index) => {
|
||||
if (e.field >= 0) slots[e.field] = index;
|
||||
});
|
||||
meta.compact = true;
|
||||
meta.compactElems = elems;
|
||||
meta.compactSlots = slots;
|
||||
}
|
||||
}
|
||||
|
||||
function alignTo(n: number, align: number): number {
|
||||
return Math.ceil(n / align) * align;
|
||||
}
|
||||
|
||||
function sameFieldPrefix(base: IrClassDef, cls: IrClassDef): boolean {
|
||||
return (
|
||||
base.fields.length <= cls.fields.length &&
|
||||
base.fields.every((f, i) => cls.fields[i]!.name === f.name)
|
||||
);
|
||||
}
|
||||
|
||||
function undefFieldInits(
|
||||
host: ClassHost,
|
||||
meta: LlClassMeta,
|
||||
@@ -589,12 +760,17 @@ export function emitClassShapes(
|
||||
|
||||
for (const cls of emitted) {
|
||||
const meta = metaMap.get(cls.name)!;
|
||||
// Whole-program int32 fields (int32-slots.ts) store the exact i32.
|
||||
const fieldTys = cls.fields.map((f) =>
|
||||
int32?.isField(cls.name, f.name)
|
||||
? "i32"
|
||||
: llFieldType(f.type, host.inlineUnions.classField(cls.name, f.name)),
|
||||
);
|
||||
if (meta.compact) {
|
||||
typeDefs.push(
|
||||
`%${mangleClassStruct(cls.name)} = type { ${meta.compactElems.map((e) => e.type).join(", ")} } ` +
|
||||
`; class ${cls.name} (32-bit header) { ${meta.compactElems
|
||||
.slice(1)
|
||||
.map((e) => llvmCommentText(cls.fields[e.field]!.name))
|
||||
.join("; ")} }`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const fieldTys = classFieldTypes(host, cls, int32);
|
||||
// Subclasses of a runtime base embed its remaining prefix (the state
|
||||
// and display-name slots) between the vtable word and the field list;
|
||||
// stream subclasses add the stream-state pointer — upcasts to
|
||||
@@ -718,7 +894,7 @@ export function emitClassShapes(
|
||||
const traced = host.tracedShapes.has(`object:${cls.name}`);
|
||||
const runtimePrefix = runtimePrefixOf(meta);
|
||||
const isStreamRooted = streamRooted(meta);
|
||||
const fieldIndex = (i: number): number => classFieldBase(meta) + i;
|
||||
const fieldIndex = (i: number): number => classFieldSlot(meta, i);
|
||||
// Teardown, trace and gcFree see each slot's storage type: a nullable
|
||||
// class field holds the instance pointer (NULL for the unit arm), and
|
||||
// every class retain/release/visit entry point is NULL-tolerant.
|
||||
@@ -851,30 +1027,41 @@ export function emitClassShapes(
|
||||
``,
|
||||
);
|
||||
// Direct release (the vtable entry): the class's own teardown. The
|
||||
// caller already handled NULL/immortal.
|
||||
const reld: string[] = [
|
||||
`define internal void @${mangleClassReleaseDirect(cls.name)}(ptr %o) ${FN_ATTRS} { ; direct release ${cls.name}`,
|
||||
`entry:`,
|
||||
` %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:`,
|
||||
];
|
||||
if (traced) reld.push(...cycleDeadLines(host, "teardown"));
|
||||
if (bounded) {
|
||||
const destroy = `${mangleClassReleaseDirect(cls.name)}_destroy`;
|
||||
const destroyBody: string[] = [];
|
||||
teardown(destroyBody);
|
||||
defs.push(...destroyDefinitions(destroy, destroyBody, chained), ``);
|
||||
host.declare(`declare void @scr_rc_destroy(ptr, ptr)`);
|
||||
reld.push(` call void @scr_rc_destroy(ptr %o, ptr @${destroy})`);
|
||||
} else teardown(reld);
|
||||
reld.push(` br label %done`);
|
||||
if (traced) reld.push(`root:`, ...cycleRootLines(host, "done"));
|
||||
reld.push(`done:`, ` ret void`, `}`, ``);
|
||||
defs.push(...reld);
|
||||
// caller already handled NULL/immortal. A compact instance has no
|
||||
// count: nothing releases it.
|
||||
if (meta.compact) {
|
||||
defs.push(
|
||||
`define internal void @${mangleClassReleaseDirect(cls.name)}(ptr %o) ${FN_ATTRS} { ; direct release ${cls.name} (immortal)`,
|
||||
`entry:`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
} else {
|
||||
const reld: string[] = [
|
||||
`define internal void @${mangleClassReleaseDirect(cls.name)}(ptr %o) ${FN_ATTRS} { ; direct release ${cls.name}`,
|
||||
`entry:`,
|
||||
` %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:`,
|
||||
];
|
||||
if (traced) reld.push(...cycleDeadLines(host, "teardown"));
|
||||
if (bounded) {
|
||||
const destroy = `${mangleClassReleaseDirect(cls.name)}_destroy`;
|
||||
const destroyBody: string[] = [];
|
||||
teardown(destroyBody);
|
||||
defs.push(...destroyDefinitions(destroy, destroyBody, chained), ``);
|
||||
host.declare(`declare void @scr_rc_destroy(ptr, ptr)`);
|
||||
reld.push(` call void @scr_rc_destroy(ptr %o, ptr @${destroy})`);
|
||||
} else teardown(reld);
|
||||
reld.push(` br label %done`);
|
||||
if (traced) reld.push(`root:`, ...cycleRootLines(host, "done"));
|
||||
reld.push(`done:`, ` ret void`, `}`, ``);
|
||||
defs.push(...reld);
|
||||
}
|
||||
} else {
|
||||
const freeBody: string[] = [];
|
||||
teardown(freeBody);
|
||||
@@ -927,10 +1114,22 @@ export function emitClassShapes(
|
||||
host,
|
||||
sizeOf,
|
||||
traced && !lifetime ? { type: cycType } : null,
|
||||
meta.hierarchy && !meta.idInRc ? 16 : 8,
|
||||
storedHeaderBytes(meta),
|
||||
),
|
||||
);
|
||||
if (meta.idInRc) {
|
||||
if (meta.compact && !meta.hierarchy)
|
||||
nw.push(` store i32 ${compactHeaderWord(meta)}, ptr %o ; no count (compactHeaderWord)`);
|
||||
else if (meta.compact) {
|
||||
// The header word: the class id and no count (compactHeaderWord).
|
||||
// Only publication writes it again, setting COMPACT_PUBLISHED in the
|
||||
// top byte, so the class id reads that carry !invariant.group read
|
||||
// the low 16 bits only (classIdLoadLines).
|
||||
host.declare(`declare ptr @llvm.launder.invariant.group.p0(ptr)`);
|
||||
nw.push(
|
||||
` %o.vt = call ptr @llvm.launder.invariant.group.p0(ptr %o)`,
|
||||
` store i32 ${compactHeaderWord(meta)}, ptr %o.vt ; class ${llvmCommentText(cls.name)} is ${meta.pre}`,
|
||||
);
|
||||
} else 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.
|
||||
|
||||
@@ -264,7 +264,9 @@ import {
|
||||
emitLibCall,
|
||||
} from "./lib-dispatch.js";
|
||||
import {
|
||||
assignCompactLayouts,
|
||||
buildClassGraph,
|
||||
classFieldTypes,
|
||||
classMembershipIntervals,
|
||||
classFieldIndex,
|
||||
classPreLines,
|
||||
@@ -1492,6 +1494,15 @@ export class LlEmitter {
|
||||
classDefinitions: (cls, sizeOf) => regions.classDefs(cls, sizeOf),
|
||||
};
|
||||
}
|
||||
// ProgramLifetime hierarchies count nothing: their instances get the
|
||||
// 32-bit header and packed fields (classes.ts LlClassMeta.compact),
|
||||
// except where regions count them or a slot word follows the header.
|
||||
if (this.sizeType === "i64" && mod.lifetimeRegions !== true)
|
||||
assignCompactLayouts(
|
||||
this.classMeta,
|
||||
(root) => programLifetimeClass(this.shapeHost, root) && !root.threadSlot,
|
||||
(cls) => classFieldTypes(this.shapeHost, cls, this.int32Slots),
|
||||
);
|
||||
for (const [name, rec] of RUNTIME_ERROR_CLASSES) {
|
||||
const meta = this.classMeta.get(name);
|
||||
if (!meta) break; // hand-written IR without the builtin defs: no stamps
|
||||
@@ -5114,6 +5125,7 @@ export class LlEmitter {
|
||||
post: meta.post,
|
||||
runtimeRooted: meta.root.def.runtime === true,
|
||||
idInRc: meta.idInRc,
|
||||
compact: meta.compact,
|
||||
// Inline scalar-union fields are plain bits: nothing to push.
|
||||
fields: meta.def.fields.flatMap((f) =>
|
||||
this.inlineUnions.classField(className, f.name)
|
||||
@@ -5154,15 +5166,21 @@ export class LlEmitter {
|
||||
|
||||
/** 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 {
|
||||
* LlClassMeta.idInRc, or the top bit of a compact header word for
|
||||
* "compact": classes.ts COMPACT_PUBLISHED); leaves the builder in the cold
|
||||
* block and answers the continuation label. */
|
||||
private emitPublishedTest(obj: string, count: "i32" | "i64" | "compact" = this.sizeType): string {
|
||||
const B = this.B;
|
||||
const rc = B.tmp(),
|
||||
frozen = B.tmp(),
|
||||
expected = B.tmp();
|
||||
B.line(`${rc} = load ${count}, ptr ${obj}`);
|
||||
B.line(`${frozen} = icmp eq ${count} ${rc}, -1`);
|
||||
if (count === "compact") {
|
||||
B.line(`${rc} = load i32, ptr ${obj}`);
|
||||
B.line(`${frozen} = icmp slt i32 ${rc}, 0`);
|
||||
} else {
|
||||
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"),
|
||||
@@ -5203,7 +5221,10 @@ export class LlEmitter {
|
||||
if (className !== null && !published.classGuarded(className)) return;
|
||||
const B = this.B;
|
||||
const meta = className === null ? undefined : this.classMeta.get(className);
|
||||
const ok = this.emitPublishedTest(obj, meta?.idInRc === true ? "i32" : this.sizeType);
|
||||
const ok = this.emitPublishedTest(
|
||||
obj,
|
||||
meta?.compact === true ? "compact" : 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);
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { IrClassDef, IrRecordShape, IrType, IrUnionDef, SrcLoc } from "../.
|
||||
import { mapOf, STRING, typeKey } from "../../ir/ir.js";
|
||||
import { mangleClassStruct, mangleRecordStruct } from "../mangle.js";
|
||||
import { FN_ATTRS } from "./shapes.js";
|
||||
import { COMPACT_ID_MASK } from "./classes.js";
|
||||
import { shapeTagFn } from "./nullable-unions.js";
|
||||
import type { InlineUnionFields } from "./inline-unions.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
@@ -66,6 +67,8 @@ export interface PublishClassInfo {
|
||||
/** Instances keep their class id in the rc word (classes.ts
|
||||
* LlClassMeta.idInRc). */
|
||||
idInRc: boolean;
|
||||
/** Instances have the 32-bit header (classes.ts LlClassMeta.compact). */
|
||||
compact: boolean;
|
||||
/** The class's own and inherited fields: GEP index and storage type. */
|
||||
fields: { name: string; index: number; type: IrType }[];
|
||||
}
|
||||
@@ -76,6 +79,11 @@ 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_COMPACT, or'ed into a kind: the object has the 32-bit header
|
||||
* (classes.ts compactHeaderWord), whose top bit is the mark. */
|
||||
const PUB_COMPACT = 0x200;
|
||||
/** COMPACT_PUBLISHED as an i32 constant. */
|
||||
const COMPACT_MARK = "-2147483648";
|
||||
/** 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";
|
||||
@@ -223,7 +231,11 @@ export class PublishWalkers {
|
||||
// hold time-value bits that no walker may push.
|
||||
if (def.arms.some((arm) => scalarRefusal(arm)))
|
||||
throw new LlvmUnsupportedError(`publish of ${typeKey(t)}`, loc);
|
||||
const live = arms.flatMap((a) => (a.fn === null ? [] : [{ tag: a.tag, fn: a.fn }]));
|
||||
const live = arms.flatMap((a) =>
|
||||
a.fn === null
|
||||
? []
|
||||
: [{ tag: a.tag, fn: a.fn, compact: this.compactRef(def.arms[a.tag]!) }],
|
||||
);
|
||||
if (this.host.shapeUnion(t)) {
|
||||
// The value is the record itself (or a unit encoding): no box to
|
||||
// mark; the arm's walker marks and walks the record.
|
||||
@@ -256,7 +268,7 @@ export class PublishWalkers {
|
||||
for (const a of live)
|
||||
lines.push(
|
||||
`arm${a.tag}:`,
|
||||
...this.push("%slot", a.fn, `u${a.tag}`),
|
||||
...this.push("%slot", a.fn, `u${a.tag}`, a.compact),
|
||||
` br label %done`,
|
||||
);
|
||||
}
|
||||
@@ -316,18 +328,25 @@ export class PublishWalkers {
|
||||
const lines = [
|
||||
head,
|
||||
`entry:`,
|
||||
...(info.idInRc
|
||||
...(info.compact
|
||||
? [
|
||||
` %idp = getelementptr inbounds i8, ptr %o, i64 4 ; the class id (classes.ts classIdRcWord)`,
|
||||
` %id = load i32, ptr %idp`,
|
||||
` %idw = load i32, ptr %o ; the class id (classes.ts compactHeaderWord)`,
|
||||
` %id = and i32 %idw, ${COMPACT_ID_MASK}`,
|
||||
` %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`,
|
||||
]),
|
||||
: 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)
|
||||
@@ -363,6 +382,7 @@ export class PublishWalkers {
|
||||
path,
|
||||
loc,
|
||||
info.idInRc,
|
||||
info.compact,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -374,23 +394,26 @@ 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, classId = false): string[] {
|
||||
private markLines(cyc: boolean, kind: number, classId = false, compact = false): string[] {
|
||||
const flags = compact ? PUB_COMPACT : classId ? PUB_CLASS_ID : 0;
|
||||
const call = (name: string): string[] => [
|
||||
` %${name} = call zeroext i1 @scr_pub_mark(ptr %ctx, ptr %o, i1 ${cyc ? "true" : "false"}, i32 ${classId ? kind | PUB_CLASS_ID : kind})`,
|
||||
` %${name} = call zeroext i1 @scr_pub_mark(ptr %ctx, ptr %o, i1 ${cyc ? "true" : "false"}, i32 ${kind | flags})`,
|
||||
` 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;
|
||||
// A compact object's header word is logged whole (the restore writes
|
||||
// the low 32 bits back) and marked by its top bit.
|
||||
const C = classId || compact ? "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 ${C}, ptr %o`,
|
||||
` %mimm = icmp eq ${C} %mrc, -1`,
|
||||
compact ? ` %mimm = icmp slt i32 %mrc, 0` : ` %mimm = icmp eq ${C} %mrc, -1`,
|
||||
` br i1 %mimm, label %done, label %mtest`,
|
||||
`mtest:`,
|
||||
` %mrefp = getelementptr inbounds ptr, ptr %ctx, i32 6`,
|
||||
@@ -408,7 +431,7 @@ export class PublishWalkers {
|
||||
ok = "%mok2";
|
||||
}
|
||||
lines.push(` br i1 ${ok}, label %mfast, label %mslow`, `mslow:`, ...call("ms"), `mfast:`);
|
||||
let buf = classId ? String(PUB_LOG_COUNT) : "0";
|
||||
let buf = classId || compact ? String(PUB_LOG_COUNT) : "0";
|
||||
if (cyc) {
|
||||
// ScrCycHdr.buf, the word before the object: its top bit while the
|
||||
// object is a buffered candidate.
|
||||
@@ -443,7 +466,9 @@ export class PublishWalkers {
|
||||
` store i32 ${buf}, ptr %mebuf`,
|
||||
` %mlogged1 = add ${S} %mlogged, 1`,
|
||||
` store ${S} %mlogged1, ptr %mloggedp`,
|
||||
` store ${C} -1, ptr %o`,
|
||||
...(compact
|
||||
? [` %mpub = or i32 %mrc, ${COMPACT_MARK}`, ` store i32 %mpub, ptr %o`]
|
||||
: [` store ${C} -1, ptr %o`]),
|
||||
` br label %walk`,
|
||||
);
|
||||
return lines;
|
||||
@@ -453,15 +478,18 @@ export class PublishWalkers {
|
||||
* immortal references (the parent links of a tree, values published
|
||||
* 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[] {
|
||||
private push(v: string, fn: string, id: string, compact = false): string[] {
|
||||
return [
|
||||
` %null${id} = icmp eq ptr ${v}, null`,
|
||||
` br i1 %null${id}, label %pushed${id}, label %live${id}`,
|
||||
`live${id}:`,
|
||||
// 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).
|
||||
// immortal object of every kind (scr_rc.h SCR_RC_COUNT), and a
|
||||
// compact object's header word has its top bit set once published.
|
||||
` %rc${id} = load i32, ptr ${v}`,
|
||||
` %immortal${id} = icmp eq i32 %rc${id}, -1`,
|
||||
compact
|
||||
? ` %immortal${id} = icmp slt i32 %rc${id}, 0`
|
||||
: ` %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),
|
||||
@@ -497,6 +525,12 @@ export class PublishWalkers {
|
||||
];
|
||||
}
|
||||
|
||||
/** Whether references of type `t` are compact class instances. */
|
||||
private compactRef(t: IrType): boolean {
|
||||
const arm = this.host.nullableArm(t) ?? t;
|
||||
return arm.kind === "object" && this.host.classInfo(arm.className).compact;
|
||||
}
|
||||
|
||||
private objectBody(
|
||||
head: string,
|
||||
struct: string,
|
||||
@@ -505,8 +539,14 @@ export class PublishWalkers {
|
||||
path: string,
|
||||
loc: SrcLoc,
|
||||
classId = false,
|
||||
compact = false,
|
||||
): string[] {
|
||||
const lines = [head, `entry:`, ...this.markLines(traced, PUB_KIND_OBJECT, classId), `walk:`];
|
||||
const lines = [
|
||||
head,
|
||||
`entry:`,
|
||||
...this.markLines(traced, PUB_KIND_OBJECT, classId, compact),
|
||||
`walk:`,
|
||||
];
|
||||
let n = 0;
|
||||
for (const member of members) {
|
||||
const fn = this.walker(member.type, `${path}.${member.name}`, loc);
|
||||
@@ -519,7 +559,7 @@ export class PublishWalkers {
|
||||
lines.push(
|
||||
` %f${n} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${member.index} ; ${llvmComment(member.name)}`,
|
||||
` %v${n} = load ptr, ptr %f${n}`,
|
||||
...this.push(`%v${n}`, fn, `${n}`),
|
||||
...this.push(`%v${n}`, fn, `${n}`, this.compactRef(member.type)),
|
||||
);
|
||||
n++;
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4635-contract-program-lifetime-layouts.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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 &&
|
||||
SCR_RC_COUNT(input->v.typed_ref.ptr) == SCR_RC_IMMORTAL) {
|
||||
SCR_RC_PUBLISHED(input->v.typed_ref.ptr)) {
|
||||
scr_sa_thread_shares();
|
||||
encoder->visits[id].view = (ScrDyn *)input;
|
||||
ScrMessageNode *node = &encoder->message->nodes[id];
|
||||
|
||||
@@ -42,7 +42,8 @@ typedef struct {
|
||||
} 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. */
|
||||
* its rc word (SCR_PUB_CLASS_ID), and `rc` is the count of the low half,
|
||||
* or it is compact (SCR_PUB_COMPACT), and `rc` is its header word. */
|
||||
#define SCR_PUB_LOG_COUNT UINT32_C(0x80000000)
|
||||
|
||||
#define SCR_PUB_DYN ((uintptr_t)1)
|
||||
@@ -108,7 +109,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 || SCR_RC_COUNT(v) == SCR_RC_IMMORTAL || ctx->refusal) return;
|
||||
if (!v || SCR_RC_PUBLISHED(v) || ctx->refusal) return;
|
||||
if (fn == scr_pub_str_fn) { /* a leaf: mark it now */
|
||||
scr_pub_str_fn(v, ctx);
|
||||
return;
|
||||
@@ -125,9 +126,11 @@ 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 (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 (SCR_RC_PUBLISHED(obj) || ctx->refusal) return false;
|
||||
bool compact = (kind & SCR_PUB_COMPACT) != 0;
|
||||
/* Both log the low 32 bits: a count, or a compact header word. */
|
||||
bool class_id = compact || (kind & SCR_PUB_CLASS_ID) != 0;
|
||||
kind &= ~(SCR_PUB_CLASS_ID | SCR_PUB_COMPACT);
|
||||
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
|
||||
@@ -156,7 +159,8 @@ bool scr_pub_mark(ScrPub *ctx, void *obj, bool cyc, int kind) {
|
||||
}
|
||||
#endif
|
||||
*e = (ScrPubLog){(uintptr_t)obj, logged, buf};
|
||||
if (class_id) SCR_RC_COUNT(obj) = SCR_RC_IMMORTAL;
|
||||
if (compact) SCR_RC_COUNT(obj) |= SCR_RC_COMPACT_PUBLISHED;
|
||||
else if (class_id) SCR_RC_COUNT(obj) = SCR_RC_IMMORTAL;
|
||||
else *rc = SIZE_MAX;
|
||||
return true;
|
||||
}
|
||||
@@ -382,7 +386,7 @@ static bool scr_pub_keep_ok(size_t bytes) {
|
||||
}
|
||||
|
||||
void scr_publish(void *root, ScrPubFn fn) {
|
||||
if (!root || SCR_RC_COUNT(root) == SCR_RC_IMMORTAL) return;
|
||||
if (!root || SCR_RC_PUBLISHED(root)) return;
|
||||
ScrPub ctx;
|
||||
if (scr_pub_keep.stack) {
|
||||
ctx.stack = scr_pub_keep.stack;
|
||||
|
||||
@@ -27,6 +27,24 @@ typedef size_t ScrRcCount;
|
||||
#define SCR_RC_IMMORTAL ((ScrRcCount)-1)
|
||||
#define SCR_RC_COUNT(obj) (*(ScrRcCount *)(obj))
|
||||
|
||||
/* Compact class instances (64-bit targets; llvm/classes.ts
|
||||
* LlClassMeta.compact): the instances of a ProgramLifetime hierarchy count
|
||||
* nothing, so their header is one 32-bit word instead of the rc word, and
|
||||
* their fields start at offset 4. The word is SCR_RC_COMPACT_TAG, the class
|
||||
* id (0 outside a hierarchy) in the low 28 bits, and SCR_RC_COMPACT_PUBLISHED
|
||||
* once publication marks it. No count reaches the tag, so a word read through
|
||||
* SCR_RC_COUNT tells the two apart, and a count or word of
|
||||
* SCR_RC_PUBLISHED_MIN and up is published for every object. Only emitted
|
||||
* code holds compact instances in typed slots; the runtime reaches them
|
||||
* through their emitted adapters, publication walkers (SCR_PUB_COMPACT) and
|
||||
* the published test. */
|
||||
#define SCR_RC_COMPACT_TAG UINT32_C(0x70000000)
|
||||
#define SCR_RC_COMPACT_PUBLISHED UINT32_C(0x80000000)
|
||||
#define SCR_RC_COMPACT_ID_MASK UINT32_C(0x0fffffff)
|
||||
#define SCR_RC_PUBLISHED_MIN ((ScrRcCount)UINT32_C(0xf0000000))
|
||||
/* Whether `obj`, an object of any kind, is published (immortal). */
|
||||
#define SCR_RC_PUBLISHED(obj) (SCR_RC_COUNT(obj) >= SCR_RC_PUBLISHED_MIN)
|
||||
|
||||
/* 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
|
||||
@@ -636,6 +654,10 @@ enum {
|
||||
* the high half (SCR_RC_COUNT). The mark, and a refusal's restore, write
|
||||
* only the count. */
|
||||
SCR_PUB_CLASS_ID = 0x100,
|
||||
/* Or'ed into a kind: a compact class instance (SCR_RC_COMPACT_TAG). The
|
||||
* mark sets SCR_RC_COMPACT_PUBLISHED; a refusal's restore writes the
|
||||
* logged word back. */
|
||||
SCR_PUB_COMPACT = 0x200,
|
||||
};
|
||||
void scr_publish(void *root, ScrPubFn fn);
|
||||
void scr_pub_push(ScrPub *ctx, void *v, ScrPubFn fn);
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
import { publish } from "@scriptc/threads";
|
||||
import type { ProgramLifetime } from "@scriptc/types";
|
||||
|
||||
// ProgramLifetime classes whose fields mix widths: booleans, small integers,
|
||||
// numbers, strings, references and optional values, declared in an order
|
||||
// that leaves alignment holes, with subclasses that add fields of each width.
|
||||
// Field reads and writes, virtual dispatch, instanceof, boxing into unknown,
|
||||
// thrown instances and publication behave the same before and after the
|
||||
// objects are published.
|
||||
|
||||
abstract class Node implements ProgramLifetime {
|
||||
readonly kind: number;
|
||||
flagged = false;
|
||||
readonly pos: number;
|
||||
end: number;
|
||||
parent: Node | undefined = undefined;
|
||||
constructor(kind: number, pos: number, end: number) {
|
||||
this.kind = kind;
|
||||
this.pos = pos;
|
||||
this.end = end;
|
||||
}
|
||||
abstract label(): string;
|
||||
width(): number {
|
||||
return this.end - this.pos;
|
||||
}
|
||||
}
|
||||
|
||||
class Ident extends Node {
|
||||
readonly text: string;
|
||||
id = 0;
|
||||
escaped = false;
|
||||
constructor(pos: number, text: string) {
|
||||
super(1, pos, pos + text.length);
|
||||
this.text = text;
|
||||
}
|
||||
label(): string {
|
||||
return `Ident(${this.text}#${this.id}${this.escaped ? "!" : ""})`;
|
||||
}
|
||||
}
|
||||
|
||||
class Literal extends Node {
|
||||
readonly value: number;
|
||||
negative: boolean;
|
||||
radix = 10;
|
||||
constructor(pos: number, value: number) {
|
||||
super(2, pos, pos + String(value).length);
|
||||
this.value = value;
|
||||
this.negative = value < 0;
|
||||
}
|
||||
label(): string {
|
||||
return `Literal(${this.value.toString(this.radix)}${this.negative ? "-" : ""})`;
|
||||
}
|
||||
}
|
||||
|
||||
class Binary extends Node {
|
||||
readonly left: Node;
|
||||
readonly right: Node;
|
||||
operator: string;
|
||||
precedence = 0;
|
||||
parenthesized = false;
|
||||
constructor(left: Node, operator: string, right: Node) {
|
||||
super(3, left.pos, right.end);
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
this.operator = operator;
|
||||
left.parent = this;
|
||||
right.parent = this;
|
||||
}
|
||||
label(): string {
|
||||
const inner = `${this.left.label()} ${this.operator} ${this.right.label()}`;
|
||||
return this.parenthesized ? `(${inner})` : inner;
|
||||
}
|
||||
}
|
||||
|
||||
class Tagged extends Ident {
|
||||
tag: string | undefined = undefined;
|
||||
weight = 0.5;
|
||||
marked = false;
|
||||
label(): string {
|
||||
return `Tagged(${super.label()}, ${this.tag ?? "-"}, ${this.weight}, ${this.marked})`;
|
||||
}
|
||||
}
|
||||
|
||||
class Flow implements ProgramLifetime {
|
||||
flags: number;
|
||||
node: Node | undefined;
|
||||
antecedent: Flow | undefined;
|
||||
reachable = true;
|
||||
constructor(flags: number, node: Node | undefined, antecedent: Flow | undefined) {
|
||||
this.flags = flags;
|
||||
this.node = node;
|
||||
this.antecedent = antecedent;
|
||||
}
|
||||
}
|
||||
|
||||
function describe(n: Node): string {
|
||||
const parent = n.parent === undefined ? "-" : String(n.parent.kind);
|
||||
return `${n.label()} k${n.kind} ${n.pos}-${n.end} w${n.width()} ${n.flagged ? "F" : "f"} up ${parent}`;
|
||||
}
|
||||
|
||||
function classify(value: unknown): string {
|
||||
if (value instanceof Tagged) return `tagged ${value.tag ?? "-"}`;
|
||||
if (value instanceof Ident) return `ident ${value.text}`;
|
||||
if (value instanceof Literal) return `literal ${value.value}`;
|
||||
if (value instanceof Binary) return `binary ${value.operator}`;
|
||||
if (value instanceof Flow) return `flow ${value.flags}`;
|
||||
return typeof value;
|
||||
}
|
||||
|
||||
function isNode(value: unknown): boolean {
|
||||
return value instanceof Node;
|
||||
}
|
||||
|
||||
function isLiteral(value: unknown): boolean {
|
||||
return value instanceof Literal;
|
||||
}
|
||||
|
||||
function walk(n: Node, out: string[]): void {
|
||||
out.push(describe(n));
|
||||
if (n instanceof Binary) {
|
||||
walk(n.left, out);
|
||||
walk(n.right, out);
|
||||
}
|
||||
}
|
||||
|
||||
function build(prefix: string): { root: Binary; flows: Flow[]; idents: Ident[] } {
|
||||
const a = new Ident(0, `${prefix}a`);
|
||||
a.id = 7;
|
||||
const b = new Tagged(4, `${prefix}b`);
|
||||
b.tag = "t";
|
||||
b.weight = 1.25;
|
||||
b.marked = true;
|
||||
b.escaped = true;
|
||||
const lit = new Literal(8, -255);
|
||||
lit.radix = 16;
|
||||
const sum = new Binary(a, "+", b);
|
||||
sum.precedence = 12;
|
||||
sum.parenthesized = true;
|
||||
const root = new Binary(sum, "*", lit);
|
||||
root.precedence = 13;
|
||||
root.flagged = true;
|
||||
const flows: Flow[] = [];
|
||||
let prev: Flow | undefined = undefined;
|
||||
for (const n of [a, b, lit, sum, root]) {
|
||||
prev = new Flow(flows.length * 3 + 1, n, prev);
|
||||
flows.push(prev);
|
||||
}
|
||||
flows[2]!.reachable = false;
|
||||
return { root, flows, idents: [a, b] };
|
||||
}
|
||||
|
||||
function report(side: string, g: { root: Binary; flows: Flow[]; idents: Ident[] }): void {
|
||||
const out: string[] = [];
|
||||
walk(g.root, out);
|
||||
for (const line of out) console.log(side, line);
|
||||
for (const f of g.flows) {
|
||||
const node = f.node === undefined ? "-" : classify(f.node);
|
||||
const before = f.antecedent === undefined ? "-" : String(f.antecedent.flags);
|
||||
console.log(side, "flow", f.flags, f.reachable, node, before);
|
||||
}
|
||||
const boxed: unknown[] = [g.root, g.root.left, g.idents[1], g.flows[0], 3, "s"];
|
||||
console.log(side, boxed.map(classify).join(", "));
|
||||
}
|
||||
|
||||
function attempt(what: string, write: () => void): void {
|
||||
try {
|
||||
write();
|
||||
console.log(what, "wrote");
|
||||
} catch (e) {
|
||||
console.log(what, (e as Error).name, (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
const g = build("x");
|
||||
report("before", g);
|
||||
|
||||
// Writes to fields that share an 8-byte word with their neighbours.
|
||||
g.idents[0]!.id = 9;
|
||||
g.idents[0]!.escaped = true;
|
||||
const tagged = g.idents[1] as Tagged;
|
||||
tagged.marked = false;
|
||||
tagged.weight = -2;
|
||||
g.root.flagged = false;
|
||||
g.flows[1]!.flags = 40;
|
||||
report("written", g);
|
||||
|
||||
// A thrown instance keeps its class.
|
||||
try {
|
||||
throw g.idents[1];
|
||||
} catch (e) {
|
||||
console.log("caught", classify(e), isNode(e), isLiteral(e));
|
||||
}
|
||||
|
||||
// Instances as map keys and set members.
|
||||
const seen = new Map<Node, number>();
|
||||
const visited = new Set<Flow>();
|
||||
for (const f of g.flows) {
|
||||
if (f.node !== undefined) seen.set(f.node, (seen.get(f.node) ?? 0) + f.flags);
|
||||
visited.add(f);
|
||||
}
|
||||
console.log("keys", [...seen.entries()].map(([n, v]) => `${n.kind}:${v}`).join(" "), visited.size);
|
||||
|
||||
const published = publish(g);
|
||||
console.log("same", published === g, publish(g.root) === g.root);
|
||||
report("published", published);
|
||||
attempt("ident id", () => {
|
||||
published.idents[0]!.id = 1;
|
||||
});
|
||||
attempt("tagged flag", () => {
|
||||
(published.idents[1] as Tagged).marked = true;
|
||||
});
|
||||
attempt("root flag", () => {
|
||||
published.root.flagged = true;
|
||||
});
|
||||
attempt("flow reachable", () => {
|
||||
published.flows[2]!.reachable = true;
|
||||
});
|
||||
report("after writes", published);
|
||||
|
||||
// New instances next to published ones stay writable.
|
||||
const fresh = new Binary(new Ident(16, "y"), "-", new Literal(20, 3));
|
||||
fresh.precedence = 11;
|
||||
fresh.right.flagged = true;
|
||||
console.log("fresh", describe(fresh), describe(fresh.right), classify(fresh.left), classify(published.root));
|
||||
|
||||
// Instances in recycled blocks: freed 8- and 16-byte objects leave their
|
||||
// blocks on the allocator's free lists, and the next instances of the same
|
||||
// sizes reuse them. Marker has no fields (a 4-byte instance); Toggle's
|
||||
// field and Pair's first field sit right after the header.
|
||||
class Scrap {
|
||||
readonly n: number;
|
||||
constructor(n: number) {
|
||||
this.n = n;
|
||||
}
|
||||
}
|
||||
|
||||
class Blank {}
|
||||
|
||||
class Marker implements ProgramLifetime {}
|
||||
|
||||
class Toggle implements ProgramLifetime {
|
||||
on: boolean;
|
||||
constructor(on: boolean) {
|
||||
this.on = on;
|
||||
}
|
||||
}
|
||||
|
||||
class Pair implements ProgramLifetime {
|
||||
readonly low: number;
|
||||
readonly high: Node | undefined;
|
||||
constructor(low: number, high: Node | undefined) {
|
||||
this.low = low | 0;
|
||||
this.high = high;
|
||||
}
|
||||
}
|
||||
|
||||
const blanks: Blank[] = [];
|
||||
const scraps: Scrap[] = [];
|
||||
const markers: Marker[] = [];
|
||||
const toggles: Toggle[] = [];
|
||||
const pairs: Pair[] = [];
|
||||
for (let round = 0; round < 3; round++) {
|
||||
for (let i = 0; i < 200; i++) {
|
||||
blanks.push(new Blank());
|
||||
scraps.push(new Scrap(i * 1.5));
|
||||
}
|
||||
let sum = 0;
|
||||
for (const s of scraps) sum += s.n;
|
||||
blanks.length = 0;
|
||||
scraps.length = 0;
|
||||
for (let i = 0; i < 200; i++) {
|
||||
markers.push(new Marker());
|
||||
toggles.push(new Toggle(i % 3 === 0));
|
||||
pairs.push(new Pair(i - 100, i % 2 === 0 ? undefined : published.root));
|
||||
}
|
||||
let on = 0;
|
||||
let low = 0;
|
||||
let high = 0;
|
||||
for (const t of toggles) if (t.on) on++;
|
||||
for (const p of pairs) {
|
||||
low += p.low;
|
||||
if (p.high !== undefined) high++;
|
||||
}
|
||||
console.log("recycled", round, sum, markers.length, on, low, high);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4944, "san": 7757 }
|
||||
Reference in New Issue
Block a user