Give ProgramLifetime class instances a 32-bit header and packed fields (#1651)

This commit is contained in:
Chris Tate
2026-10-21 11:21:22 -05:00
committed by GitHub
11 changed files with 761 additions and 90 deletions
+4 -3
View File
@@ -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 });
+237 -38
View File
@@ -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.
+27 -6
View File
@@ -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);
+61 -21
View File
@@ -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++;
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/4635-contract-program-lifetime-layouts.ts"
],
"diags": []
}
+1 -1
View File
@@ -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];
+11 -7
View File
@@ -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;
+22
View File
@@ -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 }