diff --git a/packages/compiler/src/backend/llvm/class-unions.test.ts b/packages/compiler/src/backend/llvm/class-unions.test.ts new file mode 100644 index 0000000000..1a987057b7 --- /dev/null +++ b/packages/compiler/src/backend/llvm/class-unions.test.ts @@ -0,0 +1,142 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { F64, VOID, type IrModule, type IrType } from "../../ir/ir.js"; +import { mangleClassNew, mangleClassRelease, mangleClassRetain } from "../mangle.js"; +import { emitLlvmModule } from "./emitter.js"; +import { classReleaseFn, classRetainFn, classTagFn } from "./nullable-unions.js"; + +const loc = { file: "class-unions.ts", start: 0, end: 1 }; +const tree: IrType = { kind: "union", unionId: "tree" }; + +/** `Leaf | Pair`, two standalone classes, and a hierarchy beside them. */ +function treeModule(): IrModule { + return { + irVersion: 17, + sourceFile: loc.file, + entry: "main", + functions: [{ name: "main", params: [], returnType: VOID, locals: [], body: [], loc }], + classes: [ + { name: "Leaf", fields: [{ name: "value", type: F64 }], methods: [], loc }, + { + name: "Pair", + fields: [ + { name: "left", type: tree }, + { name: "right", type: tree }, + ], + methods: [], + loc, + }, + { name: "Other", fields: [{ name: "value", type: F64 }], methods: [], loc }, + ], + unions: [ + { + id: "tree", + arms: [ + { kind: "object", className: "Leaf" }, + { kind: "object", className: "Pair" }, + ], + }, + ], + }; +} + +function definition(llvm: string, symbol: string): string { + const escaped = symbol.replace(/[.$]/g, "\\$&"); + const body = new RegExp(`^define internal [^@]*@${escaped}\\([^]*?^}`, "m").exec(llvm)?.[0]; + expect(body, symbol).toBeDefined(); + return body!; +} + +test("standalone arms keep their class id in the rc word's high half", () => { + const llvm = emitLlvmModule(treeModule(), { pointerBits: 64 }); + // Arms store the id beside a count of 1; other standalone classes keep a + // plain count. + expect(definition(llvm, mangleClassNew("Leaf"))).toMatch(/store i64 \d{10,}, ptr %o\.vt/); + expect(definition(llvm, mangleClassNew("Other"))).toMatch(/store i64 1, ptr %o/); + expect(definition(llvm, mangleClassRetain("Leaf"))).toContain("load i32, ptr %o"); + expect(definition(llvm, mangleClassRetain("Other"))).toContain("load i64, ptr %o"); + // The tag reads the id at byte 4 and switches on it. + const tag = definition(llvm, classTagFn("tree").slice(1)); + expect(tag).toContain("getelementptr inbounds i8, ptr %o, i64 4"); + expect(tag).toMatch(/switch i32 %id/); + // 32-bit targets keep the box and the plain count. + const llvm32 = emitLlvmModule(treeModule(), { pointerBits: 32 }); + expect(llvm32).not.toContain(classTagFn("tree")); + expect(definition(llvm32, mangleClassRetain("Leaf"))).toContain("load i32, ptr %o"); + expect(definition(llvm32, mangleClassNew("Leaf"))).toMatch(/store i32 1, ptr %o/); +}); + +test("a tagged class's 32-bit count saturates at the immortal count instead of wrapping", () => { + const llvm = emitLlvmModule(treeModule(), { pointerBits: 64 }); + // The class's and the union's retain and release, run on an rc word whose + // count is two below the immortal all-ones count. + const symbols = [ + [mangleClassRetain("Leaf"), "retain_leaf"], + [mangleClassRelease("Leaf"), "release_leaf"], + [classRetainFn("tree").slice(1), "retain_tree"], + [classReleaseFn("tree").slice(1), "release_tree"], + ] as const; + let ir = ""; + for (const [symbol] of symbols) ir += `${definition(llvm, symbol)}\n\n`; + // A last reference never reaches these: the harness aborts if one does. + const last = [ + [`${mangleClassRelease("Leaf")}_slow`, "last_leaf"], + [`${classReleaseFn("tree").slice(1)}.last`, "last_tree"], + ] as const; + for (const [symbol, name] of [...symbols, ...last]) + ir = ir.replaceAll(`@${symbol}(`, `@${name}(`); + ir = ir.replaceAll("define internal", "define").replaceAll(" #0 ", " "); + ir += `declare void @last_leaf(ptr)\ndeclare void @last_tree(ptr)\n`; + const dir = mkdtempSync(join(tmpdir(), "scriptc-class-unions-")); + try { + writeFileSync(join(dir, "rc.ll"), ir); + writeFileSync( + join(dir, "main.c"), + `#include +#include +void *retain_leaf(void *); +void release_leaf(void *); +void *retain_tree(void *); +void release_tree(void *); +void last_leaf(void *o) { (void)o; abort(); } +void last_tree(void *o) { (void)o; abort(); } +static int check(void *(*retain)(void *), void (*release)(void *)) { + const uint64_t id = (uint64_t)42 << 32; + uint64_t word = id | 0xfffffffdu; + retain(&word); + if (word != (id | 0xfffffffeu)) return 1; + retain(&word); + if (word != (id | 0xffffffffu)) return 2; + retain(&word); + if (word != (id | 0xffffffffu)) return 3; + release(&word); + if (word != (id | 0xffffffffu)) return 4; + word = id | 3; + release(&word); + if (word != (id | 2)) return 5; + return 0; +} +int main(void) { + int leaf = check(retain_leaf, release_leaf); + if (leaf != 0) return leaf; + int tree = check(retain_tree, release_tree); + return tree == 0 ? 0 : 10 + tree; +} +`, + ); + execFileSync("clang", [ + "-Wno-override-module", + "-O1", + join(dir, "rc.ll"), + join(dir, "main.c"), + "-o", + join(dir, "rc"), + ]); + execFileSync(join(dir, "rc")); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/src/backend/llvm/class-unions.ts b/packages/compiler/src/backend/llvm/class-unions.ts new file mode 100644 index 0000000000..5353503acd --- /dev/null +++ b/packages/compiler/src/backend/llvm/class-unions.ts @@ -0,0 +1,255 @@ +/* Class-union helpers (nullable-unions.ts ClassUnion). A class union's value + * is the instance pointer; its arm is the arm whose preorder interval holds + * the instance's class id, which every arm's hierarchy keeps in its header + * (classes.ts LlClassMeta.idInRc). Each union gets a tag helper, and the + * unions over the same classes share one retain, release and trace. */ +import { InternalCompilerError } from "../../errors.js"; +import type { IrModule } from "../../ir/ir.js"; +import { mangleClassRelease, mangleClassTrace } from "../mangle.js"; +import { + classIdLoadLines, + programLifetimeClass, + type ClassHost, + type LlClassMeta, +} from "./classes.js"; +import { + NULLABLE_NULL, + classReleaseFn, + classRetainFn, + classTagFn, + classTraceFn, + type ClassUnion, +} from "./nullable-unions.js"; +import { FN_ATTRS, cycleRootLines, retainBody } from "./shapes.js"; + +/** The widest span of class ids a union's arms may cover for its helpers + * to switch on the id; wider unions test each arm's interval. */ +const CLASS_TAG_SWITCH_SPAN = 32; + +/** One arm's class ids: its class's preorder interval plus 1. */ +interface ArmIds { + tag: number; + className: string; + lo: number; + hi: number; +} + +function armIds( + union: ClassUnion, + classMeta: ReadonlyMap, +): { arms: ArmIds[]; first: LlClassMeta } { + const arms = union.arms.map((arm) => { + const meta = classMeta.get(arm.className); + if (meta === undefined || !meta.idInRc) + throw new InternalCompilerError( + `llvm emitter bug: class-union arm ${arm.className} keeps no class id`, + ); + return { tag: arm.tag, className: arm.className, lo: meta.pre + 1, hi: meta.post + 1 }; + }); + return { arms, first: classMeta.get(union.arms[0]!.className)! }; +} + +/** Lines that branch from the open block on the class id of the instance + * `%o`: to `%a` for the arm holding it and to `missing` for none. */ +function armBranchLines(arms: readonly ArmIds[], first: LlClassMeta, missing: string): string[] { + const lines = classIdLoadLines(first, "%o", "%id", ""); + const span = Math.max(...arms.map((arm) => arm.hi)) - Math.min(...arms.map((arm) => arm.lo)) + 1; + if (span <= CLASS_TAG_SWITCH_SPAN) { + const cases: string[] = []; + for (const arm of arms) + for (let id = arm.lo; id <= arm.hi; id++) cases.push(`i32 ${id}, label %a${arm.tag}`); + lines.push(` switch i32 %id, label %${missing} [ ${cases.join(" ")} ]`); + return lines; + } + arms.forEach((arm, i) => { + const next = i + 1 < arms.length ? `n${arm.tag}` : missing; + lines.push( + ` %r${arm.tag} = sub i32 %id, ${arm.lo}`, + ` %in${arm.tag} = icmp ult i32 %r${arm.tag}, ${arm.hi - arm.lo + 1}`, + ` br i1 %in${arm.tag}, label %a${arm.tag}, label %${next}`, + ); + if (next !== missing) lines.push(`${next}:`); + }); + return lines; +} + +/** Lines that leave for `unit` on NULL and for `nullArm` on the null + * sentinel, and continue in a new block for an instance. */ +function unitLines(union: ClassUnion, unit: string, nullArm: string): string[] { + const lines = [ + ` %isnull = icmp eq ptr %o, null`, + ` br i1 %isnull, label %${unit}, label %ref`, + `ref:`, + ]; + if (union.nullTag >= 0) + lines.push( + ` %isnullarm = icmp eq ptr %o, ${NULLABLE_NULL}`, + ` br i1 %isnullarm, label %${nullArm}, label %obj`, + `obj:`, + ); + return lines; +} + +/** A dispatch on the arm of `%o` that runs `call(className)` and returns. */ +function armDispatchLines( + arms: readonly ArmIds[], + first: LlClassMeta, + call: (className: string) => string, +): string[] { + return [ + ...armBranchLines(arms, first, "done"), + ...arms.flatMap((arm) => [`a${arm.tag}:`, call(arm.className), ` br label %done`]), + `done:`, + ]; +} + +/** A class union's value is whichever instance it holds, and the collector + * visits it as that instance: when one arm carries a collector header, + * every arm must. Every class of a hierarchy shares its header decision, so + * a header reaches each arm's whole hierarchy, and unions that share a + * hierarchy pass headers on to each other: this repeats until no union + * adds one. ProgramLifetime classes carry none. */ +export function classUnionHeaders( + mod: IrModule, + unions: readonly ClassUnion[], + tracedShapes: Set, + tracedUnions: ReadonlySet, +): void { + if (unions.length === 0) return; + const bases = new Map(); + for (const cls of mod.classes ?? []) bases.set(cls.name, cls.base); + const rootOf = (name: string): string => { + let root = name; + for (let base = bases.get(root); base !== undefined; base = bases.get(root)) root = base; + return root; + }; + const members = new Map(); + for (const cls of mod.classes ?? []) { + const root = rootOf(cls.name); + const list = members.get(root); + if (list) list.push(cls.name); + else members.set(root, [cls.name]); + } + const lifetime = new Set(mod.contracts?.programLifetime ?? []); + for (let changed = true; changed;) { + changed = false; + for (const union of unions) { + if (lifetime.has(rootOf(union.arms[0]!.className))) continue; + const names = union.arms.flatMap((arm) => members.get(rootOf(arm.className)) ?? []); + if ( + !tracedUnions.has(union.unionId) && + !names.some((name) => tracedShapes.has(`object:${name}`)) + ) + continue; + for (const name of names) { + const key = `object:${name}`; + if (tracedShapes.has(key)) continue; + tracedShapes.add(key); + changed = true; + } + } + } +} + +/** The tag helper of every class union, and the retain, release and trace + * of each one that owns them (NullableUnions.helperOwner). + * - Retain and release of unions over ProgramLifetime classes do nothing: + * those instances count nothing. + * - Otherwise every arm counts in the low half of its rc word (idInRc), so + * retain is the shared increment and release the shared decrement; only + * a release of the last reference dispatches on the arm, to the arm's + * own release (which tears the dynamic class down). + * - Trace dispatches on the arm. Every arm carries a collector header when + * one does (the emitter's traced fixpoint). */ +export function emitClassUnionHelpers( + host: ClassHost, + classMeta: ReadonlyMap, +): string[] { + const out: string[] = []; + for (const union of host.nullableUnions.allClassUnions()) { + const { arms, first } = armIds(union, classMeta); + out.push( + `define internal i32 ${classTagFn(union.unionId)}(ptr %o) alwaysinline ${FN_ATTRS} { ; class union tag`, + `entry:`, + ...unitLines(union, "unit", "nullarm"), + ...armBranchLines(arms, first, "bad"), + ...arms.flatMap((arm) => [`a${arm.tag}:`, ` ret i32 ${arm.tag}`]), + `unit:`, + ` ret i32 ${union.unitTag}`, + ...(union.nullTag >= 0 ? [`nullarm:`, ` ret i32 ${union.nullTag}`] : []), + `bad:`, + ` ret i32 -1`, + `}`, + ``, + ); + if (host.nullableUnions.helperOwner(union.unionId) !== union.unionId) continue; + const retain = classRetainFn(union.unionId).slice(1); + const release = classReleaseFn(union.unionId); + if (programLifetimeClass(host, first)) { + out.push( + `define internal ptr @${retain}(ptr %o) ${FN_ATTRS} { ; class union retain (immortal)`, + `entry:`, + ` ret ptr %o`, + `}`, + ``, + `define internal void ${release}(ptr %o) ${FN_ATTRS} { ; class union release (immortal)`, + `entry:`, + ` ret void`, + `}`, + ``, + ); + continue; + } + const traced = host.tracedShapes.has(`object:${first.def.name}`); + out.push( + ...retainBody(host, retain, "class union retain", false, "i32"), + ``, + `define internal void ${release}(ptr %o) ${FN_ATTRS} { ; class union release (fast path)`, + `entry:`, + ...unitLines(union, "done", "done"), + ` %rc = load i32, ptr %o`, + ` %rcm2 = sub i32 %rc, 2`, + ` %shared = icmp ult i32 %rcm2, -3`, + ` br i1 %shared, label %dec, label %slow`, + `dec:`, + ` %n = sub i32 %rc, 1`, + ` store i32 %n, ptr %o`, + ...(traced ? cycleRootLines(host, "done") : [` br label %done`]), + `slow:`, + ` %imm = icmp eq i32 %rc, -1`, + ` br i1 %imm, label %done, label %last`, + `last:`, + ` call void ${release}.last(ptr %o)`, + ` br label %done`, + `done:`, + ` ret void`, + `}`, + ``, + `define internal void ${release}.last(ptr %o) noinline ${FN_ATTRS} { ; class union release (last reference)`, + `entry:`, + ...armDispatchLines( + arms, + first, + (name) => ` call void @${mangleClassRelease(name)}(ptr %o)`, + ), + ` ret void`, + `}`, + ``, + ); + if (traced) + out.push( + `define internal void ${classTraceFn(union.unionId)}(ptr %o, ptr %visit, ptr %ctx) ${FN_ATTRS} { ; class union trace`, + `entry:`, + ...unitLines(union, "done", "done"), + ...armDispatchLines( + arms, + first, + (name) => ` call void @${mangleClassTrace(name)}(ptr %o, ptr %visit, ptr %ctx)`, + ), + ` ret void`, + `}`, + ``, + ); + } + return out; +} diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts index e4e8bd459e..7ab7315c16 100644 --- a/packages/compiler/src/backend/llvm/classes.ts +++ b/packages/compiler/src/backend/llvm/classes.ts @@ -100,7 +100,9 @@ export class LlClassMeta { * a vtable word, the rc word holds the class id (classIdRcWord) in its * high 32 bits (CLASS_ID_OFFSET) and the reference count in its low 32 * bits, and the hierarchy's vtables are one array indexed by the id - * (mangleVtArray). */ + * (mangleVtArray). A standalone class that is an arm of a class union + * (nullable-unions.ts taggedClasses) keeps its id the same way, without + * vtables. */ 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 @@ -131,6 +133,7 @@ export function buildClassGraph( mod: IrModule, fnByName: Map, sizeType: "i32" | "i64" = "i64", + tagged: ReadonlySet = new Set(), ): Map { const metaMap = new Map(); for (const cls of mod.classes ?? []) { @@ -161,7 +164,10 @@ export function buildClassGraph( // EventEmitter, EventTarget, Event and the streams) and 32-bit targets // have no spare half in the rc word: those keep the vtable word. for (const meta of metaMap.values()) - meta.idInRc = sizeType === "i64" && meta.hierarchy && meta.root.def.runtime !== true; + meta.idInRc = + sizeType === "i64" && + (meta.hierarchy || tagged.has(meta.def.name)) && + meta.root.def.runtime !== true; const slotRoots = new Set(); for (const key of mod.threadSlotKeys ?? []) { const meta = metaMap.get(key); @@ -628,10 +634,20 @@ export function classIdRcWord(meta: LlClassMeta, lifetime: boolean): string { return (BigInt(meta.pre + 1) * 2n ** 32n + (lifetime ? 2n ** 31n : 1n)).toString(); } +/** The rc word of an immortal LlClassMeta.idInRc instance (a frame + * object): the class id in the high half and the immortal count, all ones, + * in the low half, which scr_rc_immortal reads as immortal. */ +export function classIdImmortalRcWord(meta: LlClassMeta): string { + if (meta.pre + 1 >= LAYOUT_ID_LIMIT) + throw new InternalCompilerError(`class id ${meta.pre + 1} is not a layout id`); + return (BigInt(meta.pre + 1) * 2n ** 32n + 0xffffffffn).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 + * for a class without LlClassMeta.idInRc) 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; @@ -650,7 +666,7 @@ export function storedHeaderBytes(meta: LlClassMeta): number { /** 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); + return COMPACT_HEADER_TAG | (meta.idInRc ? meta.pre + 1 : 0); } /** Lines that decode the 32-bit reference `idx` (an i32) of a compressed @@ -1024,7 +1040,7 @@ export function emitClassShapes( // instance is the element at its number minus the root's. for (const name of roots) { const root = metaMap.get(name)!; - if (!root.idInRc) continue; + if (!root.hierarchy || !root.idInRc) continue; const members = [...metaMap.values()] .filter((m) => m.root === root) .sort((a, b) => a.pre - b.pre); @@ -1395,6 +1411,7 @@ export function emitClassShapes( count, ) : null, + count, ), ``, ); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index edad06ff3b..529e93bd99 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -364,7 +364,15 @@ import { VirtualBorrows } from "./virtual-borrows.js"; import { planRecordViews, recordViewFieldPtr } from "./record-views.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import { LlWalkers } from "./walkers.js"; -import { isObjectArm, NULLABLE_ABSENT, NULLABLE_NULL, NullableUnions } from "./nullable-unions.js"; +import { + isObjectArm, + isPayloadTag, + NULLABLE_ABSENT, + NULLABLE_NULL, + NullableUnions, + pointerTagFn, +} from "./nullable-unions.js"; +import { classUnionHeaders, emitClassUnionHelpers } from "./class-unions.js"; import { emitNullablePresent, emitUnionPeek, emitUnionTag } from "./union-repr.js"; import { emitConcatInputs, @@ -2039,6 +2047,7 @@ export class LlEmitter { mod, options.debugSources === undefined, this.nodeIndex, + options.pointerBits !== 32, ); this.heapCensus = heapCensusOf(mod, this.nullableUnions); for (const decl of this.heapCensus?.declarations() ?? []) this.declare(decl); @@ -2323,6 +2332,13 @@ export class LlEmitter { } } } + // The same holds for a class union's arms (class-unions.ts). + classUnionHeaders( + mod, + this.nullableUnions.allClassUnions(), + this.tracedShapes, + this.tracedUnions, + ); this.shapeHost = { // Journal hooks only on emitters that can emit ranges (chunk // emitters, and emitters whose workers take a journal after the @@ -2488,7 +2504,12 @@ export class LlEmitter { // The class graph: base/children links, hierarchy membership, the // whole-program preorder numbering for instanceof over runtime and // compiled error objects, and the per-hierarchy virtual slot lists. - this.classMeta = buildClassGraph(mod, this.fnByName, this.sizeType); + this.classMeta = buildClassGraph( + mod, + this.fnByName, + this.sizeType, + this.nullableUnions.taggedClasses(), + ); if (mod.lifetimeRegions === true) { this.regions = new LifetimeRegions( this.shapeHost, @@ -2756,6 +2777,7 @@ export class LlEmitter { ...late, ...emitNullableRetainWrappers(this.shapeHost), ...emitShapeUnionHelpers(this.shapeHost), + ...emitClassUnionHelpers(this.shapeHost, this.classMeta), ...emitInlineRcHelpers(this.shapeHost), ]; } @@ -4702,11 +4724,7 @@ export class LlEmitter { const nullable = this.nullableUnions.get(unionId); if (nullable) { // The owned payload IS the nullable union's value. - if ( - nullable.repr === "single" - ? tag !== nullable.refTag - : !nullable.arms.some((arm) => arm.tag === tag) - ) + if (!isPayloadTag(nullable, tag)) throw new InternalCompilerError( `llvm emitter bug: payload wrap into unit arm ${tag} of ${unionId}`, ); @@ -7148,7 +7166,10 @@ export class LlEmitter { const nullable = this.nullableUnions.of(t); return nullable?.repr === "single" ? nullable.arm : null; }, - shapeUnion: (t) => this.nullableUnions.of(t)?.repr === "shapes", + pointerTagFn: (t) => { + const nullable = this.nullableUnions.of(t); + return nullable === null || nullable.repr === "single" ? null : pointerTagFn(nullable); + }, fieldBase: (shapeId) => this.nullableUnions.fieldBase(shapeId), classInfo: (className) => { const meta = this.classMetaOf(className); @@ -7656,11 +7677,7 @@ export class LlEmitter { const nullable = this.nullableUnions.get(e.unionId); if (!nullable) return null; if (isUnitType(e.value.type)) return e.value.kind === "unitLit" ? "unit" : null; - const refArm = - nullable.repr === "single" - ? e.tag === nullable.refTag - : nullable.arms.some((arm) => arm.tag === e.tag); - return refArm && isRefCounted(e.value.type) ? "ref" : null; + return isPayloadTag(nullable, e.tag) && isRefCounted(e.value.type) ? "ref" : null; } materializeSplitLocal(localId: string): void { diff --git a/packages/compiler/src/backend/llvm/expr-records.ts b/packages/compiler/src/backend/llvm/expr-records.ts index e68b0ed4c9..e65a356eb9 100644 --- a/packages/compiler/src/backend/llvm/expr-records.ts +++ b/packages/compiler/src/backend/llvm/expr-records.ts @@ -74,13 +74,13 @@ export function emitUnionRetype( const to = host.nullableUnions.get(e.unionId); if (from && to) { // Equal encodings make the pointer itself the target value. A shape - // union derives its record arms from the record, so only the unit - // encodings must agree. + // union derives its record arms from the record and a class union its + // class arms from the class id, so only the unit encodings must agree. const same = from.repr === "single" && to.repr === "single" ? from.refTag === to.refTag && from.unitTag === to.unitTag && from.nullTag === to.nullTag - : from.repr === "shapes" && - to.repr === "shapes" && + : from.repr !== "single" && + from.repr === to.repr && from.unitTag === to.unitTag && from.nullTag === to.nullTag; if (same) return host.own({ name: value.name, type: e.type }); diff --git a/packages/compiler/src/backend/llvm/nullable-unions.test.ts b/packages/compiler/src/backend/llvm/nullable-unions.test.ts index ea0517b56a..c155a86f60 100644 --- a/packages/compiler/src/backend/llvm/nullable-unions.test.ts +++ b/packages/compiler/src/backend/llvm/nullable-unions.test.ts @@ -3,6 +3,7 @@ import { F64, NULL_T, UNDEFINED_T, + type IrClassDef, type IrModule, type IrType, type IrUnionDef, @@ -146,3 +147,73 @@ test("typed array arms are nullable pointers compared by identity", () => { expect(isObjectArm(unions.single("iu")!)).toBe(true); expect(isPointerIdentityArm(unions.single("iu")!)).toBe(true); }); + +test("unions of two or more classes become class unions", () => { + const cls = (className: string): IrType => ({ kind: "object", className }); + const def = (name: string, base?: string, runtime?: true): IrClassDef => ({ + name, + fields: [], + loc, + ...(base === undefined ? {} : { base }), + ...(runtime === undefined ? {} : { runtime }), + }); + const classes = [ + def("%Error", undefined, true), + def("%TypeError", "%Error", true), + def("Leaf"), + def("Pair"), + def("Node"), + def("Name", "Node"), + def("Call", "Node"), + def("Sym"), + def("Type"), + ]; + const unions = [ + { id: "lp", arms: [cls("Leaf"), cls("Pair")] }, + { id: "lpu", arms: [cls("Leaf"), UNDEFINED_T, cls("Pair")] }, + { id: "lpnu", arms: [NULL_T, cls("Leaf"), cls("Pair"), UNDEFINED_T] }, + { id: "siblings", arms: [cls("Name"), cls("Call")] }, + { id: "target", arms: [cls("Sym"), cls("Node"), cls("Type")] }, + { id: "ancestor", arms: [cls("Node"), cls("Name")] }, + { id: "number", arms: [cls("Leaf"), F64] }, + { id: "error", arms: [cls("Leaf"), cls("%TypeError")] }, + { id: "mixed", arms: [cls("Leaf"), cls("Sym")] }, + { id: "rp", arms: [cls("Pair"), cls("Leaf")] }, + ]; + const lifetime = { contracts: { programLifetime: ["Sym", "Type", "Node"] } }; + const nullable = new NullableUnions(module(unions, { classes, ...lifetime })); + expect(nullable.classes("lp")).toMatchObject({ unitTag: -1, nullTag: -1 }); + expect(nullable.classes("lpu")).toMatchObject({ unitTag: 1, nullTag: -1 }); + expect(nullable.classes("lpnu")).toMatchObject({ unitTag: 3, nullTag: 0 }); + expect(nullable.classes("lpnu")?.arms).toEqual([ + { tag: 1, className: "Leaf" }, + { tag: 2, className: "Pair" }, + ]); + expect(nullable.classes("siblings")).not.toBeNull(); + expect(nullable.classes("target")).not.toBeNull(); + // An arm that extends another, a scalar arm, a runtime-rooted arm, and + // arms of which only some are ProgramLifetime keep the box. + expect(nullable.get("ancestor")).toBeNull(); + expect(nullable.get("number")).toBeNull(); + expect(nullable.get("error")).toBeNull(); + expect(nullable.get("mixed")).toBeNull(); + // Standalone arms keep their class id only because they are arms. + expect([...nullable.taggedClasses()].sort()).toEqual(["Leaf", "Pair", "Sym", "Type"]); + expect(nullable.helperOwner("rp")).toBe("lp"); + expect(nullable.helperOwner("lpu")).toBe("lp"); + expect(nullable.allClassUnions().map((u) => u.unionId)).toEqual([ + "lp", + "lpu", + "lpnu", + "siblings", + "target", + "rp", + ]); + const plain = { classes, ...lifetime }; + // 32-bit targets, debug builds and library builds keep boxes. + expect(new NullableUnions(module(unions, plain), true, undefined, false).get("lp")).toBeNull(); + expect(new NullableUnions(module(unions, plain), false).get("lp")).toBeNull(); + expect( + new NullableUnions(module(unions, plain), true, undefined, false).taggedClasses().size, + ).toBe(0); +}); diff --git a/packages/compiler/src/backend/llvm/nullable-unions.ts b/packages/compiler/src/backend/llvm/nullable-unions.ts index 587f3c18ca..3f7ca8ca16 100644 --- a/packages/compiler/src/backend/llvm/nullable-unions.ts +++ b/packages/compiler/src/backend/llvm/nullable-unions.ts @@ -54,8 +54,31 @@ export interface ShapeUnion { arms: readonly { tag: number; shapeId: string }[]; } +/** A union of two or more class arms and at most two unit arms (`A | B`, + * `A | B | undefined`, sum types over classes) whose VALUES are the + * instance pointer itself, like a ShapeUnion: NULL is the unit arm + * (`undefined` when both exist, NULLABLE_NULL is then `null`). No arm is + * an ancestor of another, so the arm of an instance is the arm whose + * preorder interval holds its class id: every instance of an arm's + * hierarchy keeps its class id in its header (classes.ts LlClassMeta.idInRc + * and compactHeaderWord), which standalone arm classes do only because they + * are arms (taggedClasses). The arms are all compact classes or all keep + * the rc word, so one load reads the id of any value (the per-union + * `@sc_ctag.` helper). */ +export interface ClassUnion { + repr: "classes"; + unionId: string; + /** The unit arm NULL encodes; -1 when the union has no unit arm. */ + unitTag: number; + /** The `null` arm of a two-unit-arm union, encoded by NULLABLE_NULL; + * -1 otherwise. */ + nullTag: number; + /** The class arms in tag order. */ + arms: readonly { tag: number; className: string }[]; +} + /** A union whose values are plain pointers (no `%ScrUnion` box). */ -export type PointerUnion = NullableUnion | ShapeUnion; +export type PointerUnion = NullableUnion | ShapeUnion | ClassUnion; /** The struct index of a tagged record shape's hidden shape-number word. */ export const SHAPE_WORD_INDEX = 1; @@ -79,6 +102,27 @@ export function shapeTraceFn(unionId: string): string { return `@sc_strace.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`; } +/** The per-union helper mapping a class-union value to its tag. */ +export function classTagFn(unionId: string): string { + return `@sc_ctag.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`; +} + +/** The per-union retain, release and trace entry points of a class union. */ +export function classRetainFn(unionId: string): string { + return `@sc_cretain.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`; +} +export function classReleaseFn(unionId: string): string { + return `@sc_crelease.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`; +} +export function classTraceFn(unionId: string): string { + return `@sc_ctrace.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`; +} + +/** The helper mapping a shape- or class-union value to its tag. */ +export function pointerTagFn(union: ShapeUnion | ClassUnion): string { + return union.repr === "shapes" ? shapeTagFn(union.unionId) : classTagFn(union.unionId); +} + /** The module-scope immortal sentinel standing for `null` in a * `C | null | undefined` union (NULL is `undefined` there). */ export const NULLABLE_NULL = "@sc_nullable_null"; @@ -102,17 +146,25 @@ export type NullableRefArm = /** The arm is a JS object: always truthy. */ export function isObjectArm(nullable: PointerUnion): boolean { - return nullable.repr === "shapes" || nullable.arm.kind !== "string"; + return nullable.repr !== "single" || nullable.arm.kind !== "string"; } /** The arm compares by pointer identity. Function arms compare identity * roots instead: a signature adapter is the function it adapts. */ export function isPointerIdentityArm(nullable: PointerUnion): boolean { return ( - nullable.repr === "shapes" || (nullable.arm.kind !== "string" && nullable.arm.kind !== "func") + nullable.repr !== "single" || (nullable.arm.kind !== "string" && nullable.arm.kind !== "func") ); } +/** The arm `tag` of a pointer union holds a reference: the value is that + * payload itself (not a unit encoding). */ +export function isPayloadTag(nullable: PointerUnion, tag: number): boolean { + if (nullable.repr === "single") return tag === nullable.refTag; + if (nullable.repr === "shapes") return nullable.arms.some((arm) => arm.tag === tag); + return nullable.arms.some((arm) => arm.tag === tag); +} + /** The representation decision for every union of one module. It is a * pure function of the module, so serialized-IR and in-memory emission * agree. */ @@ -128,12 +180,18 @@ export class NullableUnions { /** Some nullable union encodes `null` with NULLABLE_NULL. */ readonly usesNullSentinel: boolean = false; /** The shape union whose release and trace helpers each shape union - * uses: the first one over the same record arms. */ + * uses: the first one over the same record arms; likewise for class + * unions over the same classes. */ private readonly helperOwners = new Map(); + /** The standalone classes that are arms of a class union, which keep + * their class id in their header only for that (taggedClasses). */ + private readonly tagged = new Set(); - /** `shapes`: allow shape unions (debug builds keep boxes, whose - * debug-info layouts describe every arm). */ - constructor(mod: IrModule, shapes = true, nodes?: IrNodeIndex) { + /** `shapes`: allow shape and class unions (debug builds keep boxes, + * whose debug-info layouts describe every arm). `classIds`: the target + * keeps class ids in instance headers (64-bit targets), which class + * unions read their arm from. */ + constructor(mod: IrModule, shapes = true, nodes?: IrNodeIndex, classIds = true) { const emitted = new Set(); for (const cls of mod.classes ?? []) if (!cls.runtime) emitted.add(cls.name); const records = new Set(); @@ -143,6 +201,7 @@ export class NullableUnions { // boxes) and of programs with lifetime regions (whose walks report a // reference slot by its type's release entry point). const allowShapes = shapes && mod.lib === undefined && mod.lifetimeRegions !== true; + const classes = allowShapes && classIds ? new ClassForest(mod) : null; // A record that is an arm of a boxed union never becomes a shape // union's arm: conversions between unions over the same records then // keep one representation. @@ -154,7 +213,8 @@ export class NullableUnions { if (boxed.has(def.id)) continue; const nullable = nullableShape(def, emitted, records) ?? - (allowShapes ? shapeUnion(def, records, boxedRecords) : null); + (allowShapes ? shapeUnion(def, records, boxedRecords) : null) ?? + (classes !== null ? classUnion(def, classes) : null); if (!nullable) continue; this.byId.set(def.id, nullable); if (nullable.nullTag >= 0) this.usesNullSentinel = true; @@ -162,22 +222,34 @@ export class NullableUnions { for (const arm of nullable.arms) if (!this.shapeNumbers.has(arm.shapeId)) this.shapeNumbers.set(arm.shapeId, this.shapeNumbers.size + 2); + if (nullable.repr === "classes") + for (const arm of nullable.arms) + if (classes!.standalone(arm.className)) this.tagged.add(arm.className); } // Release and trace dispatch on the shape word, which names a record - // shape module-wide, so unions over the same records share them. + // shape module-wide, so unions over the same records share them; class + // unions dispatch on the class id, so the same holds for their arms. const owners = new Map(); for (const nullable of this.byId.values()) { - if (nullable.repr !== "shapes") continue; - const key = nullable.arms - .map((arm) => arm.shapeId) - .sort() - .join(" "); + if (nullable.repr === "single") continue; + const names = + nullable.repr === "shapes" + ? nullable.arms.map((arm) => arm.shapeId) + : nullable.arms.map((arm) => arm.className); + const key = `${nullable.repr}:${names.sort().join(" ")}`; const owner = owners.get(key) ?? nullable.unionId; owners.set(key, owner); this.helperOwners.set(nullable.unionId, owner); } } + /** Standalone classes (no base, no subclass) that are arms of a class + * union: their instances keep their class id in their header like + * hierarchy members do (classes.ts buildClassGraph). */ + taggedClasses(): ReadonlySet { + return this.tagged; + } + /** The shape union whose release and trace helpers this one uses. */ helperOwner(unionId: string): string { return this.helperOwners.get(unionId) ?? unionId; @@ -203,6 +275,12 @@ export class NullableUnions { return nullable?.repr === "shapes" ? nullable : null; } + /** The class-union form only. */ + classes(unionId: string): ClassUnion | null { + const nullable = this.byId.get(unionId); + return nullable?.repr === "classes" ? nullable : null; + } + /** Every shape union, in module order. */ allShapeUnions(): ShapeUnion[] { const out: ShapeUnion[] = []; @@ -210,6 +288,13 @@ export class NullableUnions { return out; } + /** Every class union, in module order. */ + allClassUnions(): ClassUnion[] { + const out: ClassUnion[] = []; + for (const nullable of this.byId.values()) if (nullable.repr === "classes") out.push(nullable); + return out; + } + /** The module-wide number a tagged record shape stores in its shape * word, or 0 for shapes that carry none. */ shapeNumber(shapeId: string): number { @@ -312,6 +397,107 @@ function shapeUnion( return { repr: "shapes", unionId: def.id, unitTag, nullTag, arms }; } +/** The class forest of a module, by base links: what classUnion needs + * before the emitter builds its class graph. */ +class ClassForest { + private readonly byName = new Map(); + private readonly hasChildren = new Set(); + private readonly lifetimeRoots: ReadonlySet; + private readonly slotRoots = new Set(); + + constructor(mod: IrModule) { + for (const cls of mod.classes ?? []) { + this.byName.set(cls.name, cls); + if (cls.base !== undefined) this.hasChildren.add(cls.base); + } + this.lifetimeRoots = new Set(mod.contracts?.programLifetime ?? []); + for (const key of mod.threadSlotKeys ?? []) { + const root = this.root(key); + if (root !== undefined) this.slotRoots.add(root.name); + } + } + + root(className: string): IrClassDef | undefined { + let cls = this.byName.get(className); + while (cls?.base !== undefined) cls = this.byName.get(cls.base); + return cls; + } + + /** `ancestor` is `className` or one of its bases. */ + extends(className: string, ancestor: string): boolean { + let cls = this.byName.get(className); + while (cls !== undefined) { + if (cls.name === ancestor) return true; + cls = cls.base === undefined ? undefined : this.byName.get(cls.base); + } + return false; + } + + standalone(className: string): boolean { + return this.byName.get(className)?.base === undefined && !this.hasChildren.has(className); + } + + /** The class is in a ProgramLifetime hierarchy, whose instances count + * nothing. */ + lifetime(className: string): boolean { + const root = this.root(className); + return root !== undefined && this.lifetimeRoots.has(root.name); + } + + /** Instances of the class keep their class id in a compact header + * (classes.ts assignCompactLayouts): ProgramLifetime hierarchies without + * a slot word. */ + compact(className: string): boolean { + return this.lifetime(className) && !this.slotRoots.has(this.root(className)!.name); + } + + /** An emitted class in a program-rooted hierarchy that is no Array + * subclass: one whose instances can carry a class id. */ + identifiable(className: string): boolean { + const cls = this.byName.get(className); + const root = this.root(className); + return ( + cls !== undefined && + cls.runtime !== true && + root !== undefined && + root.runtime !== true && + root.arrayElem === undefined + ); + } +} + +/** Two or more class arms none of which extends another, all compact or + * all keeping the rc word, all ProgramLifetime classes or none, and at most + * two unit arms (both: undefined and null). */ +function classUnion(def: IrUnionDef, classes: ClassForest): ClassUnion | null { + const units = def.arms.filter(isUnitType).length; + if (units > 2 || def.arms.length - units < 2) return null; + const arms: { tag: number; className: string }[] = []; + for (let tag = 0; tag < def.arms.length; tag++) { + const arm = def.arms[tag]!; + if (isUnitType(arm)) continue; + if (arm.kind !== "object" || !classes.identifiable(arm.className)) return null; + arms.push({ tag, className: arm.className }); + } + const compact = classes.compact(arms[0]!.className); + const lifetime = classes.lifetime(arms[0]!.className); + for (const arm of arms) { + if (classes.compact(arm.className) !== compact) return null; + if (classes.lifetime(arm.className) !== lifetime) return null; + for (const other of arms) + if (other !== arm && classes.extends(arm.className, other.className)) return null; + } + let unitTag = -1, + nullTag = -1; + if (units === 1) unitTag = def.arms.findIndex(isUnitType); + else if (units === 2) { + unitTag = def.arms.findIndex((arm) => arm.kind === "undefinedT"); + nullTag = def.arms.findIndex((arm) => arm.kind === "nullT"); + if (unitTag < 0 || nullTag < 0) return null; + } + return { repr: "classes", unionId: def.id, unitTag, nullTag, arms }; +} + /** Unions the runtime itself inspects as tagged boxes keep that * representation: Map keys and Set elements hash and compare union keys by * tag and payload inside the C map. A union of records alone is the diff --git a/packages/compiler/src/backend/llvm/publish.ts b/packages/compiler/src/backend/llvm/publish.ts index 62fdd75ec9..cdc2a68e44 100644 --- a/packages/compiler/src/backend/llvm/publish.ts +++ b/packages/compiler/src/backend/llvm/publish.ts @@ -26,7 +26,6 @@ import { mapOf, STRING, typeKey } from "../../ir/ir.js"; import { mangleClassStruct, mangleRecordStruct } from "../mangle.js"; import { FN_ATTRS } from "./shapes.js"; import { COMPACT_ID_MASK, compactRefLoadLines } from "./classes.js"; -import { shapeTagFn } from "./nullable-unions.js"; import type { InlineUnionFields } from "./inline-unions.js"; import { LlvmUnsupportedError } from "./unsupported.js"; @@ -53,8 +52,9 @@ export interface PublishHost { readonly inlineUnions: InlineUnionFields; /** The nullable-pointer representation of a union, when it has one. */ nullableArm(t: IrType): IrType | null; - /** The union's values are tagged record pointers (nullable-unions.ts). */ - shapeUnion(t: IrType): boolean; + /** The tag helper of a union whose values are tagged record pointers or + * class instance pointers (nullable-unions.ts), or null. */ + pointerTagFn(t: IrType): string | null; /** The struct index of a record shape's first field. */ fieldBase(shapeId: string): number; /** Class layout: hierarchy membership, preorder interval, field slots @@ -258,12 +258,13 @@ export class PublishWalkers { ? [] : [{ 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. + const tagFn = this.host.pointerTagFn(t); + if (tagFn !== null) { + // The value is the record or instance itself (or a unit + // encoding): no box to mark; the arm's walker marks and walks it. const lines = [head, `entry:`]; lines.push( - ` %tag = call i32 ${shapeTagFn(t.unionId)}(ptr %o)`, + ` %tag = call i32 ${tagFn}(ptr %o)`, ` switch i32 %tag, label %done [ ${live.map((a) => `i32 ${a.tag}, label %arm${a.tag}`).join(" ")} ]`, ); for (const a of live) diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 69ca911d38..489b5d0282 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -47,10 +47,14 @@ import { SCR_ARR_FIELD, llvmCommentText } from "./common.js"; import { NULLABLE_NULL, SHAPE_WORD_INDEX, + classReleaseFn, + classRetainFn, + classTraceFn, shapeReleaseFn, shapeTagFn, shapeTraceFn, shapeWordHeader, + type ClassUnion, type NullableUnions, type ShapeUnion, } from "./nullable-unions.js"; @@ -257,6 +261,7 @@ export { computeTraced } from "../cycle-analysis.js"; export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release: string } { const nullable = host.nullableUnions.of(t); if (nullable?.repr === "shapes") return shapeUnionRc(host, nullable); + if (nullable?.repr === "classes") return classUnionRc(host, nullable); if (nullable) { const arm = vAdapters(host, nullable.arm); return { retain: nullableRetainSym(host, nullable.arm), release: arm.release }; @@ -314,6 +319,7 @@ export function retainSym(host: ShapeHost, t: IrType): string { if (t.kind === "jsval") return jsvalRcSym(host, "retain"); const nullable = host.nullableUnions.of(t); if (nullable?.repr === "shapes") return shapeUnionRc(host, nullable).retain; + if (nullable?.repr === "classes") return classUnionRc(host, nullable).retain; if (nullable) return nullableRetainSym(host, nullable.arm); const stem = runtimeRcStem(t); if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "retain") !== null) { @@ -335,6 +341,13 @@ function shapeUnionRc(host: ShapeHost, union: ShapeUnion): { retain: string; rel }; } +/** A class union's RC entry points (class-unions.ts), shared by the unions + * over the same classes. */ +function classUnionRc(host: ShapeHost, union: ClassUnion): { retain: string; release: string } { + const owner = host.nullableUnions.helperOwner(union.unionId); + return { retain: classRetainFn(owner), release: classReleaseFn(owner) }; +} + /** The widest span of shape numbers a union's arms may cover for its tag * helper to switch on the shape word (emitShapeUnionHelpers). */ const SHAPE_TAG_SWITCH_SPAN = 32; @@ -552,6 +565,7 @@ export function releaseSym(host: ShapeHost, t: IrType): string { if (t.kind === "jsval") return jsvalRcSym(host, "release"); const nullable = host.nullableUnions.of(t); if (nullable?.repr === "shapes") return shapeUnionRc(host, nullable).release; + if (nullable?.repr === "classes") return classUnionRc(host, nullable).release; if (nullable) return releaseSym(host, nullable.arm); const stem = runtimeRcStem(t); if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "release") !== null) { @@ -1111,6 +1125,11 @@ export function traceAdapter(host: ShapeHost, t: IrType): string | null { return shapeUnionTraced(host, nullable) ? shapeTraceFn(host.nullableUnions.helperOwner(nullable.unionId)) : null; + // Every arm of a class union carries a collector header when one does. + if (nullable?.repr === "classes") + return host.tracedShapes.has(`object:${nullable.arms[0]!.className}`) + ? classTraceFn(host.nullableUnions.helperOwner(nullable.unionId)) + : null; if (nullable) return traceAdapter(host, nullable.arm); if (!host.tracedUnions.has(t.unionId)) return null; host.declare(`declare void @scr_union_trace_v(ptr, ptr, ptr)`); @@ -1754,7 +1773,7 @@ function directDestroyKey(host: ShapeHost, type: IrType): string | null { const nullable = host.nullableUnions.of(type); let t = type; if (nullable !== null) { - if (nullable.repr === "shapes") return null; + if (nullable.repr !== "single") return null; t = nullable.arm; } if (t.kind === "record") return `record:${t.shapeId}`; @@ -1977,8 +1996,9 @@ export function releaseBody( segmented = false, frozen = false, destroyDefs: readonly string[] | null = null, + count: RcCount = host.sizeType, ): string[] { - const S = host.sizeType; + const S = count; if (bounded) host.declare(`declare void @scr_rc_destroy(ptr, ptr)`); const destroy = `${fnName}_destroy`; // A freezable record's count is the word without its frozen bit. diff --git a/packages/compiler/src/backend/llvm/stack-objects.ts b/packages/compiler/src/backend/llvm/stack-objects.ts index d4c46ac118..260347d990 100644 --- a/packages/compiler/src/backend/llvm/stack-objects.ts +++ b/packages/compiler/src/backend/llvm/stack-objects.ts @@ -16,7 +16,7 @@ import { } from "./record-results.js"; import { everyStmtList } from "../../ir/traverse.js"; import { mangleClassStruct, mangleRecordStruct } from "../mangle.js"; -import { classFieldIndex, classUndefinedFieldInits } from "./classes.js"; +import { classFieldIndex, classIdImmortalRcWord, classUndefinedFieldInits } from "./classes.js"; import { CYC_BUF, CYC_HDR } from "./alloc.js"; import type { CallLifetimes } from "./call-lifetimes.js"; import { emitCallExpr, emitConstruction } from "./expr-calls.js"; @@ -396,7 +396,10 @@ export function emitStackObject( B.line(line.trim()); } } - B.line(`store ${host.sizeType} -1, ptr ${slot}`); + const tagged = type.kind === "object" ? host.classMeta.get(type.className) : undefined; + // A class-union arm keeps its class id beside the immortal count. + if (tagged?.idInRc === true) B.line(`store i64 ${classIdImmortalRcWord(tagged)}, ptr ${slot}`); + else B.line(`store ${host.sizeType} -1, ptr ${slot}`); if (type.kind === "record") { // A tagged shape's word names it to every shape union's tag read. const shapeNumber = host.nullableUnions.shapeNumber(type.shapeId); diff --git a/packages/compiler/src/backend/llvm/union-repr.ts b/packages/compiler/src/backend/llvm/union-repr.ts index 8c6fb4c858..4ec9f181c3 100644 --- a/packages/compiler/src/backend/llvm/union-repr.ts +++ b/packages/compiler/src/backend/llvm/union-repr.ts @@ -3,10 +3,11 @@ * nullable union (nullable-unions.ts) is the arm pointer itself: NULL for * its NULL-encoded unit arm and, in `C | null | undefined`, the immortal * NULLABLE_NULL sentinel for `null`. A shape union's value is likewise the - * record pointer; its tag comes from the record's shape word. Every structural read of a union value - * goes through here. */ + * record pointer; its tag comes from the record's shape word. A class + * union's value is the instance pointer; its tag comes from the class id. + * Every structural read of a union value goes through here. */ import type { BlockBuilder } from "./blocks.js"; -import { NULLABLE_NULL, shapeTagFn, type PointerUnion } from "./nullable-unions.js"; +import { NULLABLE_NULL, pointerTagFn, type PointerUnion } from "./nullable-unions.js"; /** The i32 tag of a union value. `alias` is the tag word's alias * attachment (", !tbaa !N"), when the caller has one. */ @@ -17,8 +18,8 @@ export function emitUnionTag( alias = "", ): string { const t = B.tmp(); - if (nullable?.repr === "shapes") { - B.line(`${t} = call i32 ${shapeTagFn(nullable.unionId)}(ptr ${u})`); + if (nullable !== null && nullable.repr !== "single") { + B.line(`${t} = call i32 ${pointerTagFn(nullable)}(ptr ${u})`); return t; } if (nullable) { @@ -45,8 +46,9 @@ export function emitUnionTag( * the `null` sentinel). */ export function emitNullablePresent(B: BlockBuilder, nullable: PointerUnion, u: string): string { const present = B.tmp(); - if (nullable.repr === "shapes" && nullable.unitTag < 0) { - // No unit arm: every value is a record (NULL only as a pending dummy). + if (nullable.repr !== "single" && nullable.unitTag < 0) { + // No unit arm: every value is a record or an instance (NULL only as a + // pending dummy). B.line(`${present} = icmp ne ptr ${u}, null`); return present; } @@ -67,7 +69,7 @@ export function emitNullableIsTag( tag: number, negated: boolean, ): string { - if (nullable.repr === "shapes" && tag !== nullable.unitTag && tag !== nullable.nullTag) { + if (nullable.repr !== "single" && tag !== nullable.unitTag && tag !== nullable.nullTag) { const actual = emitUnionTag(B, nullable, u), t = B.tmp(); B.line(`${t} = icmp ${negated ? "ne" : "eq"} i32 ${actual}, ${tag}`); @@ -104,8 +106,8 @@ export function emitUnionPeek( /** Text-template forms for helpers written as raw LLVM lines: `%out` * receives the tag / payload of `u`. */ export function unionTagLines(nullable: PointerUnion | null, u: string, out: string): string[] { - if (nullable?.repr === "shapes") - return [` ${out} = call i32 ${shapeTagFn(nullable.unionId)}(ptr ${u})`]; + if (nullable !== null && nullable.repr !== "single") + return [` ${out} = call i32 ${pointerTagFn(nullable)}(ptr ${u})`]; if (nullable && nullable.nullTag < 0) return [ ` ${out}.isnull = icmp eq ptr ${u}, null`, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-union-cycles.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-union-cycles.ts.json new file mode 100644 index 0000000000..a2ee6b5206 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-union-cycles.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-union-cycles.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-union-hierarchies.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-union-hierarchies.ts.json new file mode 100644 index 0000000000..80a0684c4f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-union-hierarchies.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-union-hierarchies.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-union-pointers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-union-pointers.ts.json new file mode 100644 index 0000000000..0b78bc621c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-union-pointers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-union-pointers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-union-program-lifetime.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-union-program-lifetime.ts.json new file mode 100644 index 0000000000..23173ad35f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/class-union-program-lifetime.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/class-union-program-lifetime.ts" + ], + "diags": [] +} diff --git a/tests/corpus/class-union-cycles.ts b/tests/corpus/class-union-cycles.ts new file mode 100644 index 0000000000..187b56031e --- /dev/null +++ b/tests/corpus/class-union-cycles.ts @@ -0,0 +1,93 @@ +// Cycles through class-union fields: a union whose arms include a class +// that can reach itself is cycle-capable, so every arm carries the cycle +// collector's header, including arms that hold only numbers. Graphs are +// built, linked into cycles, rewired and dropped across many rounds. +class Leaf { + readonly value: number; + constructor(value: number) { + this.value = value; + } +} + +class Branch { + left: Tree; + right: Tree; + parent: Tree | undefined = undefined; + constructor(left: Tree, right: Tree) { + this.left = left; + this.right = right; + } +} + +class Ref { + target: Tree | null = null; + readonly label: string; + constructor(label: string) { + this.label = label; + } +} + +type Tree = Leaf | Branch | Ref; + +function sum(t: Tree | null, depth: number): number { + if (t === null || depth > 40) return 0; + if (t instanceof Leaf) return t.value; + if (t instanceof Ref) return 1 + sum(t.target, depth + 1); + return sum(t.left, depth + 1) + sum(t.right, depth + 1); +} + +function build(depth: number, seed: number): Tree { + if (depth === 0) return new Leaf(seed % 5); + const b = new Branch(build(depth - 1, seed * 7 + 1), build(depth - 1, seed + 3)); + if (b.left instanceof Branch) b.left.parent = b; + if (b.right instanceof Branch) b.right.parent = b; + return b; +} + +let checksum = 0; +const kept: Tree[] = []; +for (let round = 0; round < 300; round++) { + const tree = build(5, round); + const ref = new Ref(`r${round}`); + // A cycle: the tree reaches the ref, and the ref reaches the tree. + if (tree instanceof Branch) { + tree.right = ref; + ref.target = tree; + } + checksum += sum(tree, 0); + if (round % 50 === 0) kept.push(tree); + // Rewire: drop the cycle on some rounds and keep it on others. + if (round % 3 === 0) ref.target = new Leaf(round); +} +console.log(checksum, kept.length, kept.map((t) => sum(t, 0)).join(" ")); + +// A self-referential ref and a two-ref ring, dropped. +for (let i = 0; i < 100; i++) { + const self = new Ref("self"); + self.target = self; + const a = new Ref("a"); + const b = new Ref("b"); + a.target = b; + b.target = a; + checksum += sum(a, 30) + sum(self, 35); +} +console.log(checksum); + +// Union values in arrays and maps that form cycles with their holders. +class Holder { + readonly items: Tree[] = []; + readonly named = new Map(); +} +let held = 0; +for (let i = 0; i < 50; i++) { + const h = new Holder(); + const ref = new Ref(`h${i}`); + h.items.push(new Leaf(i), ref, build(2, i)); + h.named.set("ref", ref); + const branch = new Branch(ref, new Leaf(1)); + ref.target = branch; + branch.parent = h.items[2]!; + h.items.push(branch); + held += h.items.length + h.named.size + sum(h.items[3]!, 0); +} +console.log(held); diff --git a/tests/corpus/class-union-hierarchies.ts b/tests/corpus/class-union-hierarchies.ts new file mode 100644 index 0000000000..f61809de66 --- /dev/null +++ b/tests/corpus/class-union-hierarchies.ts @@ -0,0 +1,161 @@ +// Class-union arms that are hierarchies: an instance of a subclass is in +// its base's arm, sibling subclasses can be arms of one union, virtual +// methods dispatch through a narrowed arm, and thrown instances keep their +// class through a catch. +abstract class Node { + readonly pos: number; + constructor(pos: number) { + this.pos = pos; + } + abstract text(): string; +} + +class Identifier extends Node { + readonly name: string; + constructor(pos: number, name: string) { + super(pos); + this.name = name; + } + text(): string { + return this.name; + } +} + +class PrivateIdentifier extends Node { + readonly name: string; + constructor(pos: number, name: string) { + super(pos); + this.name = name; + } + text(): string { + return `#${this.name}`; + } +} + +class Literal extends Node { + readonly value: number; + constructor(pos: number, value: number) { + super(pos); + this.value = value; + } + text(): string { + return String(this.value); + } +} + +class BigLiteral extends Literal { + text(): string { + return `${this.value}n`; + } +} + +class Symbol_ { + readonly name: string; + uses = 0; + constructor(name: string) { + this.name = name; + } +} + +class Signature { + readonly params: number; + constructor(params: number) { + this.params = params; + } +} + +class Type { + readonly id: number; + constructor(id: number) { + this.id = id; + } +} + +class ObjectType extends Type { + readonly members: string[] = []; +} + +class UnionType extends Type { + readonly types: Type[]; + constructor(id: number, types: Type[]) { + super(id); + this.types = types; + } +} + +// Sibling subclasses as arms. +type MemberName = Identifier | PrivateIdentifier; +function memberText(name: MemberName): string { + return name instanceof PrivateIdentifier ? `private ${name.name}` : `public ${name.name}`; +} +const names: MemberName[] = [new Identifier(0, "a"), new PrivateIdentifier(1, "b"), new Identifier(2, "c")]; +console.log(names.map(memberText).join(", "), names.map((n) => n.text()).join(" "), names.map((n) => n.pos).join("")); + +// Whole hierarchies as arms, and a subclass instance in its base's arm. +type Target = Symbol_ | Type | Signature | Node; +function describe(target: Target): string { + if (target instanceof Symbol_) return `symbol ${target.name}`; + if (target instanceof Signature) return `signature/${target.params}`; + if (target instanceof UnionType) return `union ${target.id} of ${target.types.length}`; + if (target instanceof Type) return `${target instanceof ObjectType ? "object" : "type"} ${target.id}`; + return `node ${target.text()} at ${target.pos}`; +} +const targets: Target[] = []; +const sym = new Symbol_("x"); +const obj = new ObjectType(2); +targets.push(sym, new Type(1), obj, new UnionType(3, [obj, new Type(4)]), new Signature(2)); +targets.push(new Identifier(5, "y"), new Literal(6, 42), new BigLiteral(7, 9), names[1]!); +console.log(targets.map(describe).join("; ")); + +// A resolution stack keyed by target identity, as a checker keeps one. +const stack: Target[] = []; +const props: number[] = []; +function push(target: Target, prop: number): boolean { + for (let i = stack.length - 1; i >= 0; i--) if (stack[i] === target && props[i] === prop) return false; + stack.push(target); + props.push(prop); + return true; +} +function pop(): void { + stack.pop(); + props.pop(); +} +let cycles = 0; +for (let round = 0; round < 200; round++) { + const t = targets[round % targets.length]!; + if (!push(t, round % 3)) cycles++; + if (!push(sym, 0)) cycles++; + if (round % 4 === 3) { + pop(); + pop(); + } +} +console.log(stack.length, cycles, describe(stack[stack.length - 1]!)); + +// Checked casts out of the union. +function symbolName(t: Target): string { + return (t as Symbol_).name; +} +function typeId(t: Target): number { + return (t as Type).id; +} +console.log(symbolName(sym), typeId(obj), typeId(targets[3]!)); + +// Unit arms beside hierarchy arms. +type OptionalTarget = Type | Node | undefined; +const optional: OptionalTarget[] = [undefined, new Literal(1, 1), new UnionType(5, []), undefined]; +console.log(optional.map((t) => (t === undefined ? "-" : t instanceof Node ? t.text() : `t${t.id}`)).join(" ")); + +// Throwing and catching union arms. +function fail(target: Target): never { + throw target; +} +for (const t of [sym, new Signature(0), new Literal(3, 3)] as Target[]) { + try { + fail(t); + } catch (e) { + if (e instanceof Symbol_) console.log("caught symbol", e.name); + else if (e instanceof Signature) console.log("caught signature", e.params); + else if (e instanceof Node) console.log("caught node", e.text()); + } +} diff --git a/tests/corpus/class-union-pointers.ts b/tests/corpus/class-union-pointers.ts new file mode 100644 index 0000000000..f3841aa630 --- /dev/null +++ b/tests/corpus/class-union-pointers.ts @@ -0,0 +1,182 @@ +// Unions of two or more classes hold the instance pointer itself: wraps, +// narrowing by instanceof and by a discriminant field, unit arms, identity, +// containers, fields, retypes between such unions, captures, and the +// conversions that print or serialize a union value. +class Circle { + readonly kind = "circle"; + readonly r: number; + constructor(r: number) { + this.r = r; + } + area(): number { + return 3 * this.r * this.r; + } +} + +class Rect { + readonly kind = "rect"; + readonly w: number; + readonly h: number; + constructor(w: number, h: number) { + this.w = w; + this.h = h; + } + area(): number { + return this.w * this.h; + } +} + +class Tri { + readonly kind = "tri"; + readonly b: number; + readonly h: number; + constructor(b: number, h: number) { + this.b = b; + this.h = h; + } +} + +type Shape = Circle | Rect | Tri; +type MaybeShape = Shape | undefined; +type NullableShape = Circle | Rect | null | undefined; + +function area(s: Shape): number { + if (s instanceof Circle) return s.area(); + if (s instanceof Rect) return s.area(); + return (s.b * s.h) / 2; +} + +function byKind(s: Shape): string { + switch (s.kind) { + case "circle": + return `circle ${s.r}`; + case "rect": + return `rect ${s.w}x${s.h}`; + case "tri": + return `tri ${s.b}/${s.h}`; + } +} + +function make(i: number): Shape { + if (i % 3 === 0) return new Circle(i); + if (i % 3 === 1) return new Rect(i, i + 1); + return new Tri(i, 2); +} + +const shapes: Shape[] = []; +for (let i = 0; i < 9; i++) shapes.push(make(i)); +console.log(shapes.map(area).join(" ")); +console.log(shapes.map(byKind).join(", ")); + +// Unit arms: undefined is NULL, null is a sentinel when both are present. +function describe(s: MaybeShape): string { + if (s === undefined) return "none"; + return byKind(s); +} +function describeNullable(s: NullableShape): string { + if (s === null) return "null"; + if (s === undefined) return "undefined"; + return s instanceof Circle ? `c${s.r}` : `r${s.w}`; +} +const maybe: MaybeShape[] = [undefined, new Circle(2), undefined, new Tri(1, 1)]; +console.log(maybe.map(describe).join(" ")); +const nullable: NullableShape[] = [null, new Rect(3, 4), undefined, new Circle(5)]; +console.log(nullable.map(describeNullable).join(" ")); +console.log(nullable.map((s) => s ?? "fallback").map((s) => (typeof s === "string" ? s : s.kind)).join(" ")); +console.log(maybe.map((s) => s?.kind ?? "-").join(" ")); + +// Identity: equality compares instances. +const c = new Circle(1); +const a: Shape = c; +const b: Shape = c; +const other: Shape = new Circle(1); +console.log(a === b, a === other, a !== other, shapes.indexOf(shapes[4]!), shapes.includes(other)); + +// Fields of class-union type, reassigned and cleared. +class Slot { + current: MaybeShape = undefined; + history: Shape[] = []; + put(s: Shape): void { + if (this.current !== undefined) this.history.push(this.current); + this.current = s; + } +} +const slot = new Slot(); +for (const s of shapes.slice(0, 5)) slot.put(s); +console.log(describe(slot.current), slot.history.length, slot.history.map((s) => s.kind).join(",")); +slot.current = undefined; +console.log(describe(slot.current)); + +// Maps and sets of instances, keyed by an arm-independent value. +const byName = new Map(); +for (const s of shapes) byName.set(byKind(s), s); +let total = 0; +for (const [, s] of byName) total += area(s); +console.log(byName.size, total); +const seen = new Set(); +for (const s of shapes) if (s instanceof Circle) seen.add(s); +console.log(seen.size); + +// Retypes: a narrower union widens to a wider one and back. +type Round = Circle | Rect; +function widen(r: Round): MaybeShape { + return r; +} +function narrow(s: MaybeShape): Round | undefined { + if (s === undefined || s instanceof Tri) return undefined; + return s; +} +const round: Round[] = [new Circle(7), new Rect(1, 2)]; +console.log(round.map(widen).map(describe).join(" "), describe(narrow(new Tri(3, 3))), describe(narrow(round[1]))); + +// Captures and callbacks. +function counter(start: Shape): () => string { + let s: Shape = start; + let n = 0; + return () => { + n++; + s = n % 2 === 0 ? new Rect(n, n) : new Circle(n); + return byKind(s); + }; +} +const next = counter(new Tri(0, 0)); +console.log(next(), next(), next()); +console.log(shapes.filter((s) => s instanceof Rect).length, shapes.find((s) => s instanceof Tri)?.kind); +console.log([...shapes].sort((x, y) => area(x) - area(y)).map((s) => area(s)).join(" ")); + +// Printing and serializing union values. +console.log(shapes[0], shapes[1], shapes[2]); +console.log(JSON.stringify(shapes.slice(0, 3))); +console.log(JSON.stringify({ current: slot.history[0], none: maybe[0] })); +console.log(String(shapes[3] instanceof Circle), `${shapes[4] instanceof Rect}`); +const mixed: (Shape | number)[] = [new Circle(1), 4]; +console.log(mixed.map((m) => (typeof m === "number" ? m : m.kind)).join(" ")); + +// Fresh instances passed straight to functions that only read them. +function width(s: Shape): number { + return s instanceof Rect ? s.w : s instanceof Circle ? s.r * 2 : s.b; +} +let widths = 0; +for (let i = 0; i < 100; i++) widths += width(new Rect(i, 1)) + width(new Circle(i)) + width(new Tri(1, i)); +console.log(widths, byKind(new Rect(2, 3)), width(new Circle(4))); + +// Conversions between class unions, boxed unions and unknown values. +function boxed(s: Shape): Shape | number | string { + return s; +} +function unboxed(v: Shape | number | string): Shape | undefined { + return typeof v === "number" || typeof v === "string" ? undefined : v; +} +const wide = shapes.slice(0, 3).map(boxed); +wide.push(5, "text"); +console.log(wide.map((v) => (typeof v === "object" ? v.kind : typeof v)).join(" ")); +console.log(wide.map(unboxed).map(describe).join(" ")); +const anyValues: unknown[] = shapes.slice(3, 6); +console.log(anyValues.map((v) => (v instanceof Circle ? "C" : v instanceof Rect ? "R" : v instanceof Tri ? "T" : "?")).join("")); +const back = anyValues[1] as Shape; +console.log(byKind(back), (anyValues[0] as Circle).r, JSON.stringify(anyValues)); +try { + console.log((anyValues[2] as Circle).area()); +} catch (e) { + console.log((e as Error).name); +} diff --git a/tests/corpus/class-union-program-lifetime.ts b/tests/corpus/class-union-program-lifetime.ts new file mode 100644 index 0000000000..e856bb63a3 --- /dev/null +++ b/tests/corpus/class-union-program-lifetime.ts @@ -0,0 +1,173 @@ +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; +import { publish } from "@scriptc/threads"; +import type { ProgramLifetime } from "@scriptc/types"; + +// Class unions over ProgramLifetime classes, whose instances keep their +// class id in a 32-bit header and count no references: a checker's +// resolution stack of `Symbol | Type | Signature | Node` targets, union +// fields, and a published graph read by a worker. Receivers read only +// fields, since Node.js clones what it posts. + +abstract class Node implements ProgramLifetime { + readonly kind: string; + readonly pos: number; + parent: Node | undefined = undefined; + constructor(kind: string, pos: number) { + this.kind = kind; + this.pos = pos; + } +} + +class Identifier extends Node { + readonly text: string; + constructor(pos: number, text: string) { + super("identifier", pos); + this.text = text; + } +} + +class PrivateIdentifier extends Node { + readonly text: string; + constructor(pos: number, text: string) { + super("private", pos); + this.text = text; + } +} + +class Call extends Node { + readonly callee: Identifier | PrivateIdentifier; + constructor(pos: number, callee: Identifier | PrivateIdentifier) { + super("call", pos); + this.callee = callee; + callee.parent = this; + } +} + +class Sym implements ProgramLifetime { + readonly kind = "symbol"; + readonly name: string; + declaration: Node | Sym | undefined = undefined; + constructor(name: string) { + this.name = name; + } +} + +class Type implements ProgramLifetime { + readonly kind: string = "type"; + readonly id: number; + origin: Sym | Type | undefined = undefined; + constructor(id: number) { + this.id = id; + } +} + +class ObjectType extends Type { + readonly kind: string = "object"; +} + +class Signature implements ProgramLifetime { + readonly kind = "signature"; + readonly arity: number; + constructor(arity: number) { + this.arity = arity; + } +} + +type Target = Sym | Type | Signature | Node; + +function label(t: Target | undefined): string { + if (t === undefined) return "-"; + switch (t.kind) { + case "symbol": + return `S:${(t as Sym).name}`; + case "type": + case "object": + return `T${(t as Type).id}`; + case "signature": + return `sig${(t as Signature).arity}`; + default: + return `N@${(t as Node).pos}`; + } +} + +class Checker { + readonly targets: Target[] = []; + readonly props: number[] = []; + depth = 0; + cycles = 0; + push(target: Target, prop: number): boolean { + for (let i = this.depth - 1; i >= 0; i--) + if (this.targets[i] === target && this.props[i] === prop) { + this.cycles++; + return false; + } + if (this.depth < this.targets.length) { + this.targets[this.depth] = target; + this.props[this.depth] = prop; + } else { + this.targets.push(target); + this.props.push(prop); + } + this.depth++; + return true; + } + pop(): void { + this.depth--; + } +} + +function program(prefix: string): Target[] { + const out: Target[] = []; + for (let i = 0; i < 4; i++) { + const sym = new Sym(`${prefix}${i}`); + const id = new Identifier(i * 10, `${prefix}${i}`); + const call = new Call(i * 10 + 1, i % 2 === 0 ? id : new PrivateIdentifier(i * 10 + 2, `${prefix}p${i}`)); + sym.declaration = i % 3 === 0 ? call : sym; + const type = i % 2 === 0 ? new Type(i) : new ObjectType(i); + type.origin = i === 0 ? sym : type; + out.push(sym, type, new Signature(i), call, call.callee); + } + return out; +} + +function local(): void { + const targets = program("a"); + const checker = new Checker(); + for (let round = 0; round < 400; round++) { + const t = targets[(round * 7) % targets.length]!; + if (checker.push(t, round % 2)) { + if (round % 5 === 4) checker.pop(); + } + if (checker.depth > 12) checker.depth = 2; + } + console.log(checker.depth, checker.cycles, checker.targets.slice(0, 6).map(label).join(" ")); + console.log(targets.map(label).join(" ")); + console.log( + targets + .filter((t) => t instanceof Sym) + .map((t) => label((t as Sym).declaration)) + .join(" "), + targets.filter((t) => t instanceof Type).map((t) => label((t as Type).origin)).join(" "), + ); + const member: (Identifier | PrivateIdentifier)[] = []; + for (const t of targets) { + if (t instanceof Identifier) member.push(t); + else if (t instanceof PrivateIdentifier) member.push(t); + } + console.log(member.map((m) => (m instanceof PrivateIdentifier ? `#${m.text}` : m.text)).join(" ")); +} + +if (isMainThread) { + local(); + const shared = publish(program("w")); + console.log("published", shared.map(label).join(" ")); + const worker = new Worker(new URL(import.meta.url), { workerData: { shared } }); + worker.on("message", (message: unknown) => { + console.log("worker", message as string); + }); +} else { + const { shared } = workerData as { shared: Target[] }; + const kinds = shared.map((t) => t.kind).join(","); + const names = shared.map((t) => (t.kind === "symbol" ? (t as Sym).name : "")).join(""); + parentPort!.postMessage(`${shared.length} ${kinds} ${names}`); +} diff --git a/tests/harness/corpus-costs/class-union-cycles.ts.json b/tests/harness/corpus-costs/class-union-cycles.ts.json new file mode 100644 index 0000000000..0f58b9a89d --- /dev/null +++ b/tests/harness/corpus-costs/class-union-cycles.ts.json @@ -0,0 +1 @@ +{ "plain": 4300, "san": 6100 } diff --git a/tests/harness/corpus-costs/class-union-hierarchies.ts.json b/tests/harness/corpus-costs/class-union-hierarchies.ts.json new file mode 100644 index 0000000000..d08b6810d4 --- /dev/null +++ b/tests/harness/corpus-costs/class-union-hierarchies.ts.json @@ -0,0 +1 @@ +{ "plain": 5600, "san": 7900 } diff --git a/tests/harness/corpus-costs/class-union-pointers.ts.json b/tests/harness/corpus-costs/class-union-pointers.ts.json new file mode 100644 index 0000000000..9ed5a41d65 --- /dev/null +++ b/tests/harness/corpus-costs/class-union-pointers.ts.json @@ -0,0 +1 @@ +{ "plain": 8000, "san": 8200 } diff --git a/tests/harness/corpus-costs/class-union-program-lifetime.ts.json b/tests/harness/corpus-costs/class-union-program-lifetime.ts.json new file mode 100644 index 0000000000..e3d3608e12 --- /dev/null +++ b/tests/harness/corpus-costs/class-union-program-lifetime.ts.json @@ -0,0 +1 @@ +{ "plain": 5300, "san": 9700 }