Represent unions of records as the record pointer itself (#1225)

This commit is contained in:
Chris Tate
2026-10-18 14:04:16 -05:00
committed by GitHub
32 changed files with 979 additions and 84 deletions
+13 -9
View File
@@ -1217,12 +1217,13 @@ export class LlDyn {
`llvm emitter bug: JSON schema of unknown shape ${t.shapeId}`,
);
const struct = mangleRecordStruct(t.shapeId);
const index = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
const base = host.nullableUnions.fieldBase(t.shapeId);
const index = new Map(shape.fields.map((f, i) => [f.name, i + base]));
// The runtime names kept-source keys by this index (record-sources.ts).
const order = recordSourceFieldOrder(shape);
const fieldTy = `{ ptr, ${S}, ${S}, ptr }`;
const entries = order.map((fieldName) => {
const field = shape.fields[index.get(fieldName)! - 1]!;
const field = shape.fields[index.get(fieldName)! - base]!;
const sub = this.jsonSchema(field.type);
const offset = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${index.get(fieldName)}) to ${S})`;
return `${fieldTy} { ptr ${host.cstr(fieldName)}, ${S} ${Buffer.byteLength(fieldName, "utf8")}, ${S} ${offset}, ptr @${sub} }`;
@@ -1240,7 +1241,7 @@ export class LlDyn {
const overflow = shape.indexValue
? {
elem: `@${this.jsonSchema(shape.indexValue)}`,
offset: `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${shape.fields.length + 1}) to ${S})`,
offset: `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${shape.fields.length + base}) to ${S})`,
}
: { elem: "null", offset: "0" };
body = `${S} 3, ${S} ${entries.length}, ptr ${fields}, ptr ${overflow.elem}, ptr @${mangleRecordNew(t.shapeId)}, ptr null, ptr ${vAdapters(host, t).release}, ptr null, ${S} ${overflow.offset}`;
@@ -1547,18 +1548,19 @@ export class LlDyn {
B.line(`call void @scr_arr_outline(ptr ${cached})`);
B.line(`call void @scr_arr_outline(ptr ${checked})`);
}
const base = t.kind === "record" ? host.nullableUnions.fieldBase(t.shapeId) : 1;
const members =
t.kind === "record"
? [
...shape!.fields.map((field, index) => ({
index: index + 1,
index: index + base,
type: llFieldType(field.type, host.inlineUnions.record(field.type)),
name: field.name,
})),
...(shape!.indexValue
? [
{
index: shape!.fields.length + 1,
index: shape!.fields.length + base,
type: "ptr" as const,
name: "[key: string] overflow",
},
@@ -1819,7 +1821,8 @@ export class LlDyn {
`llvm emitter bug: dynCheck of unknown shape ${t.shapeId}`,
);
const struct = mangleRecordStruct(t.shapeId);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
const base = host.nullableUnions.fieldBase(t.shapeId);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + base]));
const releaseR = (): void => {
B.line(`call void ${releaseSym(host, t)}(ptr %r0)`);
};
@@ -1996,7 +1999,7 @@ export class LlDyn {
const ovfp = B.tmp();
const ovf = B.tmp();
B.line(
`${ovfp} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${shape.fields.length + 1}`,
`${ovfp} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${shape.fields.length + base}`,
);
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
if (shape.fields.length === 0) {
@@ -2578,7 +2581,8 @@ export class LlDyn {
if (!shape)
throw new InternalCompilerError(`llvm emitter bug: to-dyn of unknown shape ${t.shapeId}`);
const struct = mangleRecordStruct(t.shapeId);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
const base = host.nullableUnions.fieldBase(t.shapeId);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + base]));
const loadFieldOf = (fname: string, ft: IrType): string => {
const p = B.tmp();
B.line(
@@ -2702,7 +2706,7 @@ export class LlDyn {
const ovfp = B.tmp();
const ovf = B.tmp();
B.line(
`${ovfp} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${shape.fields.length + 1}`,
`${ovfp} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${shape.fields.length + base}`,
);
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
const ks = B.tmp();
+49 -10
View File
@@ -393,6 +393,7 @@ import {
elemAccess,
emitInlineRcHelpers,
emitNullableRetainWrappers,
emitShapeUnionHelpers,
FN_ATTRS,
inlineRcDecls,
type InlineRcMode,
@@ -1183,7 +1184,7 @@ export class LlEmitter {
expr: (expr) => !(expr.kind === "libCall" && expr.fn === "error.stack"),
}),
);
this.nullableUnions = new NullableUnions(mod);
this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined);
this.mockTimers = moduleUsesMockTimers(mod);
this.debug =
options.debugSources === undefined
@@ -1282,8 +1283,10 @@ export class LlEmitter {
// Optional array reads also cover nullable unions: emitLocalArrayRead
// then yields the (borrowed or owned) element pointer, not a stack box.
// Arrays of such a union are read the same way.
this.optionalArrayReads = new OptionalArrayReads(this.fnByName, this.unionsById, (id) =>
this.nullableUnions.has(id),
this.optionalArrayReads = new OptionalArrayReads(
this.fnByName,
this.unionsById,
(id) => this.nullableUnions.single(id) !== null,
);
this.unionSources = {
unions: this.boxedUnionsById,
@@ -1336,7 +1339,8 @@ export class LlEmitter {
(type) =>
this.plainReference(type) &&
type.kind !== "string" &&
(type.kind !== "union" || this.nullableUnions.get(type.unionId)?.arm.kind !== "string"),
(type.kind !== "union" ||
this.nullableUnions.single(type.unionId)?.arm.kind !== "string"),
this.walkBorrowsByFunction.get(fn.name) ?? new Set(),
);
if (rebound.size === 0) continue;
@@ -1389,6 +1393,27 @@ export class LlEmitter {
const traced = computeTraced(mod);
this.tracedShapes = traced.shapes;
this.tracedUnions = traced.unions;
// A shape union's value is whichever record it holds, and the collector
// visits it as that record: when one arm carries a collector header,
// every arm must (nullable-unions.ts). Unions that share an arm pass a
// header on to each other's arms, so repeat until no union adds one.
const shapeUnions = this.nullableUnions.allShapeUnions();
for (let changed = true; changed;) {
changed = false;
for (const union of shapeUnions) {
if (
!this.tracedUnions.has(union.unionId) &&
!union.arms.some((arm) => this.tracedShapes.has(`record:${arm.shapeId}`))
)
continue;
for (const arm of union.arms) {
const key = `record:${arm.shapeId}`;
if (this.tracedShapes.has(key)) continue;
this.tracedShapes.add(key);
changed = true;
}
}
}
this.shapeHost = {
sizeType: this.sizeType,
cycleBufOffset: this.cycleBufOffset,
@@ -3729,6 +3754,7 @@ export class LlEmitter {
return [
...late,
...emitNullableRetainWrappers(this.shapeHost),
...emitShapeUnionHelpers(this.shapeHost),
...emitInlineRcHelpers(this.shapeHost),
];
}
@@ -5896,7 +5922,11 @@ export class LlEmitter {
const nullable = this.nullableUnions.get(unionId);
if (nullable) {
// The owned payload IS the nullable union's value.
if (tag !== nullable.refTag)
if (
nullable.repr === "single"
? tag !== nullable.refTag
: !nullable.arms.some((arm) => arm.tag === tag)
)
throw new InternalCompilerError(
`llvm emitter bug: payload wrap into unit arm ${tag} of ${unionId}`,
);
@@ -6001,7 +6031,7 @@ export class LlEmitter {
const index = shape.fields.findIndex((entry) => entry.name === field);
if (index < 0)
throw new InternalCompilerError(`llvm emitter bug: missing union field ${field}`);
layout = "record:";
layout = this.nullableUnions.fieldBase(arm.shapeId) === 2 ? "record:tagged;" : "record:";
for (let i = 0; i <= index; i++) {
const type = shape.fields[i]!.type;
layout += `${llFieldType(type, this.inlineUnions.record(type))};`;
@@ -6037,7 +6067,7 @@ export class LlEmitter {
);
const p = this.B.tmp();
this.B.line(
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${idx + 1}`,
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${idx + this.nullableUnions.fieldBase(shapeId)}`,
);
const type = shape.fields[idx]!.type;
return { ptr: p, type, inline: this.inlineUnions.record(type) };
@@ -6051,7 +6081,7 @@ export class LlEmitter {
const p = this.B.tmp();
const v = this.B.tmp();
this.B.line(
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${shape.fields.length + 1}`,
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${shape.fields.length + this.nullableUnions.fieldBase(shapeId)}`,
);
this.B.line(`${v} = load ptr, ptr ${p}`);
return v;
@@ -10223,7 +10253,12 @@ export class LlEmitter {
unionsById: this.unionsById,
recordsById: this.recordsById,
inlineUnions: this.inlineUnions,
nullableArm: (t) => this.nullableUnions.of(t)?.arm ?? null,
nullableArm: (t) => {
const nullable = this.nullableUnions.of(t);
return nullable?.repr === "single" ? nullable.arm : null;
},
shapeUnion: (t) => this.nullableUnions.of(t)?.repr === "shapes",
fieldBase: (shapeId) => this.nullableUnions.fieldBase(shapeId),
classInfo: (className) => {
const meta = this.classMetaOf(className);
return {
@@ -10944,7 +10979,11 @@ 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;
return e.tag === nullable.refTag && isRefCounted(e.value.type) ? "ref" : 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;
}
materializeSplitLocal(localId: string): void {
@@ -182,6 +182,7 @@ export function genResultThunkFor(
sym = mangleGenResThunk(host.resolveThunks.size);
host.resolveThunks.set(key, sym);
const shape = host.recordsById.get(recT.shapeId);
const base = host.nullableUnions.fieldBase(recT.shapeId);
const valueT = shape?.fields.find((f) => f.name === "value")?.type;
if (!shape || !valueT)
throw new InternalCompilerError("llvm emitter bug: genResume record lacks its value field");
@@ -242,7 +243,7 @@ export function genResultThunkFor(
` %r = call ptr @${mangleRecordNew(recT.shapeId)}()`,
` %d = call zeroext i1 @scr_gen_done(ptr %g)`,
` %dz = zext i1 %d to i8`,
` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdxD + 1}`,
` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdxD + base}`,
` store i8 %dz, ptr %dp`,
` %has = call zeroext i1 @scr_gen_out_has(ptr %g)`,
` br i1 %has, label %take, label %undefv`,
@@ -259,7 +260,7 @@ export function genResultThunkFor(
` br label %join`,
`join:`,
` %v = load ptr, ptr %vslot`,
` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdxD + 1}`,
` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdxD + base}`,
` store ptr %v, ptr %vp`,
` ret ptr %r`,
`}`,
@@ -293,7 +294,7 @@ export function genResultThunkFor(
` %r = call ptr @${mangleRecordNew(recT.shapeId)}()`,
` %d = call zeroext i1 @scr_gen_done(ptr %g)`,
` %dz = zext i1 %d to i8`,
` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdx + 1}`,
` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdx + base}`,
` store i8 %dz, ptr %dp`,
` br i1 %d, label %doneb, label %susp`,
];
@@ -410,7 +411,7 @@ export function genResultThunkFor(
` br label %join`,
`join:`,
` %v = load ptr, ptr %vslot`,
` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdx + 1}`,
` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdx + base}`,
// An inline value field copies the box's bits and drops the box.
...(host.inlineUnions.record(valueT)
? [
@@ -1092,7 +1093,8 @@ export function settledStoreFor(
return { tag, arm };
};
const fieldIndex = (arm: IrType & { kind: "record" }, field: string): number =>
host.recordsById.get(arm.shapeId)!.fields.findIndex((f) => f.name === field) + 1;
host.recordsById.get(arm.shapeId)!.fields.findIndex((f) => f.name === field) +
host.nullableUnions.fieldBase(arm.shapeId);
const fulfilled = armOf("value");
const rejected = armOf("reason");
const valueT = host.recordsById
@@ -330,7 +330,7 @@ export function emitControlExpr(
);
B.line(`store ${ty} ${t}, ptr ${slot}`);
if (host.mayThrow.has(e.retag)) host.emitPendingCheck();
} else if (host.nullableUnions.has(e.left.type.unionId)) {
} else if (host.nullableUnions.single(e.left.type.unionId)) {
// The owned nullable pointer IS the owned arm value.
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
} else {
@@ -440,7 +440,7 @@ export function emitControlExpr(
host.emitBranchInto(slot, e.right);
B.br(lj);
B.startBlock(lv);
if (typeEquals(e.type, e.left.type) || host.nullableUnions.has(def.id)) {
if (typeEquals(e.type, e.left.type) || host.nullableUnions.single(def.id)) {
// Pass-through, or a nullable pointer that IS its owned arm value.
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
} else {
@@ -62,9 +62,17 @@ export function emitUnionRetype(
const from = host.nullableUnions.get(e.fromId);
const to = host.nullableUnions.get(e.unionId);
if (from && to) {
// Equal encodings make the pointer itself the target value.
if (from.refTag === to.refTag && from.unitTag === to.unitTag && from.nullTag === to.nullTag)
return host.own({ name: value.name, type: e.type });
// 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.
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.unitTag === to.unitTag &&
from.nullTag === to.nullTag;
if (same) return host.own({ name: value.name, type: e.type });
return host.own({ name: rebuildRetype(host, value.name, e), type: e.type });
}
if (from || to) return host.own({ name: rebuildRetype(host, value.name, e), type: e.type });
@@ -500,16 +500,17 @@ export function streamTypedRefCommitAdapter(
` br i1 %missing, label %done, label %swap`,
`swap:`,
];
const base = host.nullableUnions.fieldBase(t.shapeId);
const members = [
...shape.fields.map((field, index) => ({
index: index + 1,
index: index + base,
type: llFieldType(field.type, host.inlineUnions.record(field.type)),
name: field.name,
})),
...(shape.indexValue
? [
{
index: shape.fields.length + 1,
index: shape.fields.length + base,
type: "ptr" as const,
name: "[key: string] overflow",
},
@@ -943,7 +944,7 @@ export function streamTypedRefMaterializeAdapter(
B.line(`${out} = call ptr @scr_dyn_new_arr()`);
const fields = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name));
for (const field of fields) {
const index = shape.fields.indexOf(field) + 1;
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
const fieldPtr = B.tmp();
B.line(
`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`,
@@ -980,7 +981,7 @@ export function streamTypedRefMaterializeAdapter(
liveRecordOps(host, t, snapshot, fields);
}
for (const field of fields) {
const index = shape.fields.indexOf(field) + 1;
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
const fieldPtr = B.tmp();
B.line(
`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`,
@@ -1202,7 +1203,7 @@ function liveRecordOps(
const names = `${snapshot}_names`;
const ops = `${snapshot}_rops`;
const loadField = (B: BlockBuilder, field: IrRecordShape["fields"][number]): string => {
const index = shape.fields.indexOf(field) + 1;
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
const fieldPtr = B.tmp();
B.line(`${fieldPtr} = getelementptr inbounds %${struct}, ptr %p, i64 0, i32 ${index}`);
return loadStructField(
@@ -29,7 +29,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string {
const isString = field === "address" || field === "family";
if (isString) lines.push(` %${field}.owned = call ptr @scr_str_retain_v(ptr %${field})`);
lines.push(
` %${field}.slot = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr %record, i64 0, i32 ${index + 1}`,
` %${field}.slot = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr %record, i64 0, i32 ${index + host.nullableUnions.fieldBase(type.shapeId)}`,
` store ${isString ? "ptr" : "double"} %${field}${isString ? ".owned" : ""}, ptr %${field}.slot`,
);
}
@@ -169,7 +169,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
);
const slot = host.B.tmp();
host.B.line(
`${slot} = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr ${record}, i64 0, i32 ${index + 1}`,
`${slot} = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr ${record}, i64 0, i32 ${index + host.nullableUnions.fieldBase(type.shapeId)}`,
);
host.B.line(`store ${ty} ${value}, ptr ${slot}`);
}
@@ -912,11 +912,8 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
scopeid.inline,
);
}
const rc = vAdapters(host.shapeHost, t);
const rowU = B.tmp();
B.line(
`${rowU} = call ptr @scr_union_new_ref(i32 ${tag}, ptr ${r}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, t)})`,
);
// The row (+1) moves into its union arm (a shape union is the row).
const rowU = host.unionNewOwned(infoT.unionId, tag, { name: r, type: t });
B.line(`store ptr ${rowU}, ptr ${rowSlot}`);
B.br(lRow);
};
@@ -65,7 +65,7 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV
" br i1 %more, label %body, label %finish",
"body:",
" %record = call ptr @scr_arr_get_ref(ptr %addresses, double %index)",
` %addressp = getelementptr inbounds %${struct}, ptr %record, i64 0, i32 ${field + 1}`,
` %addressp = getelementptr inbounds %${struct}, ptr %record, i64 0, i32 ${field + host.nullableUnions.fieldBase(addresses.elem.shapeId)}`,
" %address = load ptr, ptr %addressp",
" %owned.address = call ptr @scr_str_retain_v(ptr %address)",
" call double @scr_arr_push_ref(ptr %ips, ptr %owned.address)",
@@ -168,7 +168,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
const i = shape.fields.findIndex((f) => f.name === name);
if (i < 0)
throw new InternalCompilerError(`llvm emitter bug: net.serverAddress record lacks ${name}`);
return i + 1;
return i + host.nullableUnions.fieldBase(recT.shapeId);
};
const store = (name: string, ty: string, v: string): void => {
const p = B.tmp();
@@ -0,0 +1,83 @@
import { expect, test } from "vitest";
import {
F64,
NULL_T,
UNDEFINED_T,
type IrModule,
type IrType,
type IrUnionDef,
} from "../../ir/ir.js";
import { NullableUnions } from "./nullable-unions.js";
const loc = { file: "nullable-unions.ts", start: 0, end: 0 };
const a: IrType = { kind: "record", shapeId: "a" };
const b: IrType = { kind: "record", shapeId: "b" };
const c: IrType = { kind: "record", shapeId: "c" };
function module(unions: IrUnionDef[], extra: Partial<IrModule> = {}): IrModule {
return {
irVersion: 16,
sourceFile: loc.file,
entry: "main",
records: ["a", "b", "c"].map((id) => ({ id, fields: [{ name: id, type: F64 }] })),
unions,
functions: [],
...extra,
};
}
test("unions of two or more records become shape unions", () => {
const unions = new NullableUnions(
module([
{ id: "ab", arms: [a, b] },
{ id: "abu", arms: [a, UNDEFINED_T, b] },
{ id: "abnu", arms: [a, b, NULL_T, UNDEFINED_T] },
{ id: "af", arms: [a, F64] },
{ id: "au", arms: [a, UNDEFINED_T] },
]),
);
expect(unions.shapes("ab")).toMatchObject({ unitTag: -1, nullTag: -1 });
expect(unions.shapes("abu")).toMatchObject({ unitTag: 1, nullTag: -1 });
expect(unions.shapes("abnu")).toMatchObject({ unitTag: 3, nullTag: 2 });
expect(unions.shapes("abnu")?.arms).toEqual([
{ tag: 0, shapeId: "a" },
{ tag: 1, shapeId: "b" },
]);
// A scalar arm keeps the box; one record arm is a single-arm nullable.
expect(unions.get("af")).toBeNull();
expect(unions.single("au")).not.toBeNull();
expect(unions.shapes("au")).toBeNull();
// Tagged shapes number from 2 and move their fields after the word.
expect([unions.shapeNumber("a"), unions.shapeNumber("b"), unions.shapeNumber("c")]).toEqual([
2, 3, 0,
]);
expect([unions.fieldBase("a"), unions.fieldBase("c")]).toEqual([2, 1]);
expect(unions.boxedUnions(new Map([["af", { id: "af", arms: [a, F64] }]])).size).toBe(1);
});
test("records of runtime-inspected unions keep every union over them boxed", () => {
const set: IrType = { kind: "set", elem: { kind: "union", unionId: "ab" } };
const unions = new NullableUnions(
module(
[
{ id: "ab", arms: [a, b] },
{ id: "abu", arms: [a, b, UNDEFINED_T] },
{ id: "bc", arms: [b, c] },
{ id: "ac", arms: [a, c] },
],
{ globals: [{ id: "%g.s", name: "s", type: set, mutable: false }] },
),
);
expect(unions.get("ab")).toBeNull();
expect(unions.get("abu")).toBeNull();
expect(unions.get("bc")).toBeNull();
expect(unions.get("ac")).toBeNull();
expect(unions.shapeNumber("c")).toBe(0);
});
test("library, lifetime-region and debug builds keep record unions boxed", () => {
const unions = [{ id: "ab", arms: [a, b] }];
expect(new NullableUnions(module(unions), false).get("ab")).toBeNull();
expect(new NullableUnions(module(unions, { lifetimeRegions: true })).get("ab")).toBeNull();
expect(new NullableUnions(module(unions)).shapes("ab")).not.toBeNull();
});
@@ -17,6 +17,7 @@ import { everyModuleNode } from "../../ir/traverse.js";
* every container, field, capture, parameter, and return slot holds the
* plain pointer. Every other union keeps the tagged `%ScrUnion` box. */
export interface NullableUnion {
repr: "single";
unionId: string;
refTag: number;
/** The unit arm NULL encodes. */
@@ -24,7 +25,57 @@ export interface NullableUnion {
/** The `null` arm of a two-unit-arm union, encoded by NULLABLE_NULL;
* -1 when the union has one unit arm. */
nullTag: number;
arm: NullableRefArm;
/** One of NullableRefArm. Typed as the plain IrType so the self-hosted
* frontend keeps IrType's structurally identical arms one record shape
* (a PointerUnion arm holding the narrowed intersection would split them). */
arm: IrType;
}
/** A union of two or more record arms and at most two unit arms (`A | B`,
* `A | B | undefined`, discriminated unions of object literals) whose
* VALUES are the record pointer itself, like a NullableUnion: NULL is the
* unit arm (`undefined` when both exist, NULLABLE_NULL is then `null`).
* Every record shape that is an arm of such a union is a TAGGED shape: its
* struct carries the shape's module-wide number in a hidden `i32` word
* right after the reference count (struct index 1), stored by every
* allocation, so the arm of a pointer is that word mapped through the
* union's arm table (the per-union `@sc_stag.<id>` helper). Records share
* one retain; release and trace dispatch on the arm. */
export interface ShapeUnion {
repr: "shapes";
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 record arms in tag order. */
arms: readonly { tag: number; shapeId: string }[];
}
/** A union whose values are plain pointers (no `%ScrUnion` box). */
export type PointerUnion = NullableUnion | ShapeUnion;
/** The struct index of a tagged record shape's hidden shape-number word. */
export const SHAPE_WORD_INDEX = 1;
/** The LLVM type through which any tagged record's shape word is read
* (the reference count, then the word). */
export function shapeWordHeader(sizeType: "i32" | "i64"): string {
return `{ ${sizeType}, i32 }`;
}
/** The per-union helper mapping a shape-union value to its tag. */
export function shapeTagFn(unionId: string): string {
return `@sc_stag.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`;
}
/** The per-union release and trace dispatchers of a shape union. */
export function shapeReleaseFn(unionId: string): string {
return `@sc_srelease.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`;
}
export function shapeTraceFn(unionId: string): string {
return `@sc_strace.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`;
}
/** The module-scope immortal sentinel standing for `null` in a
@@ -48,44 +99,98 @@ export type NullableRefArm =
| (IrType & { kind: "array" | "map" | "set" });
/** The arm is a JS object: always truthy, compared by identity. */
export function isObjectArm(nullable: NullableUnion): boolean {
return nullable.arm.kind !== "string";
export function isObjectArm(nullable: PointerUnion): boolean {
return nullable.repr === "shapes" || nullable.arm.kind !== "string";
}
/** 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. */
export class NullableUnions {
private readonly byId = new Map<string, NullableUnion>();
private readonly byId = new Map<string, PointerUnion>();
/** Tagged record shapes (arms of a shape union) by their module-wide
* shape number (2 and up: 0 and 1 are the unit encodings' pseudo
* numbers, never stored). */
private readonly shapeNumbers = new Map<string, number>();
/** NULL-skipping retain wrappers requested so far: symbol → inner retain
* call target. The emitter defines each once. */
readonly retainWrappers = new Map<string, string>();
/** Some nullable union encodes `null` with NULLABLE_NULL. */
readonly usesNullSentinel: boolean = false;
constructor(mod: IrModule) {
/** `shapes`: allow shape unions (debug builds keep boxes, whose
* debug-info layouts describe every arm). */
constructor(mod: IrModule, shapes = true) {
const emitted = new Set<string>();
for (const cls of mod.classes ?? []) if (!cls.runtime) emitted.add(cls.name);
const records = new Set<string>();
for (const shape of mod.records ?? []) records.add(shape.id);
const boxed = boxedUnionIds(mod);
// Shape unions stay out of library builds (their host ABI marshals
// 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;
// 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.
const boxedRecords = new Set<string>();
for (const def of mod.unions ?? [])
if (boxed.has(def.id))
for (const arm of def.arms) if (arm.kind === "record") boxedRecords.add(arm.shapeId);
for (const def of mod.unions ?? []) {
if (boxed.has(def.id)) continue;
const nullable = nullableShape(def, emitted, records);
const nullable =
nullableShape(def, emitted, records) ??
(allowShapes ? shapeUnion(def, records, boxedRecords) : null);
if (!nullable) continue;
this.byId.set(def.id, nullable);
if (nullable.nullTag >= 0) this.usesNullSentinel = true;
if (nullable.repr === "shapes")
for (const arm of nullable.arms)
if (!this.shapeNumbers.has(arm.shapeId))
this.shapeNumbers.set(arm.shapeId, this.shapeNumbers.size + 2);
}
}
get(unionId: string): NullableUnion | null {
get(unionId: string): PointerUnion | null {
return this.byId.get(unionId) ?? null;
}
of(type: IrType): NullableUnion | null {
of(type: IrType): PointerUnion | null {
return type.kind === "union" ? this.get(type.unionId) : null;
}
/** The single-reference-arm form only (shape unions answer null). */
single(unionId: string): NullableUnion | null {
const nullable = this.byId.get(unionId);
return nullable?.repr === "single" ? nullable : null;
}
/** The shape-union form only. */
shapes(unionId: string): ShapeUnion | null {
const nullable = this.byId.get(unionId);
return nullable?.repr === "shapes" ? nullable : null;
}
/** Every shape union, in module order. */
allShapeUnions(): ShapeUnion[] {
const out: ShapeUnion[] = [];
for (const nullable of this.byId.values()) if (nullable.repr === "shapes") 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 {
return this.shapeNumbers.get(shapeId) ?? 0;
}
/** The struct index of a record shape's first declared field: 2 for
* tagged shapes (after the shape word), 1 otherwise. */
fieldBase(shapeId: string): number {
return this.shapeNumbers.has(shapeId) ? 2 : 1;
}
has(unionId: string): boolean {
return this.byId.has(unionId);
}
@@ -94,10 +199,11 @@ export class NullableUnions {
return this.byId.size;
}
/** The reference arm of every nullable union, by union id. */
/** The reference arm of every single-arm nullable union, by union id. */
armTypes(): Map<string, IrType> {
const result = new Map<string, IrType>();
for (const [id, nullable] of this.byId) result.set(id, nullable.arm);
for (const [id, nullable] of this.byId)
if (nullable.repr === "single") result.set(id, nullable.arm);
return result;
}
@@ -129,7 +235,7 @@ function nullableShape(
if (unitTag < 0 || nullTag < 0) return null;
}
const arm = def.arms[refTag]!;
const base = { unionId: def.id, refTag, unitTag, nullTag };
const base = { repr: "single" as const, unionId: def.id, refTag, unitTag, nullTag };
if (arm.kind === "object" && emittedClasses.has(arm.className))
return { ...base, arm: { kind: "object", className: arm.className } };
if (arm.kind === "record" && records.has(arm.shapeId))
@@ -140,6 +246,34 @@ function nullableShape(
return null;
}
/** Two or more emitted record arms and at most two unit arms (both:
* undefined and null). */
function shapeUnion(
def: IrUnionDef,
records: ReadonlySet<string>,
boxedRecords: ReadonlySet<string>,
): ShapeUnion | null {
const units = def.arms.filter(isUnitType).length;
if (units > 2 || def.arms.length - units < 2) return null;
const arms: { tag: number; shapeId: string }[] = [];
for (let tag = 0; tag < def.arms.length; tag++) {
const arm = def.arms[tag]!;
if (isUnitType(arm)) continue;
if (arm.kind !== "record" || !records.has(arm.shapeId) || boxedRecords.has(arm.shapeId))
return null;
arms.push({ tag, shapeId: arm.shapeId });
}
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: "shapes", 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. */
+21 -2
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 { shapeTagFn } from "./nullable-unions.js";
import type { InlineUnionFields } from "./inline-unions.js";
import { LlvmUnsupportedError } from "./unsupported.js";
@@ -44,6 +45,10 @@ 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 struct index of a record shape's first field. */
fieldBase(shapeId: string): number;
/** Class layout: hierarchy membership, preorder interval, field slots
* (with their storage types), runtime roots. */
classInfo(className: string): PublishClassInfo;
@@ -210,6 +215,19 @@ export class PublishWalkers {
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 }]));
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 lines = [head, `entry:`];
lines.push(
` %tag = call i32 ${shapeTagFn(t.unionId)}(ptr %o)`,
` switch i32 %tag, label %done [ ${live.map((a) => `i32 ${a.tag}, label %arm${a.tag}`).join(" ")} ]`,
);
for (const a of live)
lines.push(`arm${a.tag}:`, ` call void ${a.fn}(ptr %o, ptr %ctx)`, ` br label %done`);
lines.push(`done:`, ` ret void`, `}`);
return lines;
}
const lines = [
head,
`entry:`,
@@ -240,17 +258,18 @@ export class PublishWalkers {
const shape = this.host.recordsById.get(t.shapeId);
if (!shape)
throw new LlvmUnsupportedError(`publish of an unknown record ${t.shapeId}`, loc);
const base = this.host.fieldBase(shape.id);
const members = [
...shape.fields.flatMap((f, i) =>
this.host.inlineUnions.record(f.type)
? []
: [{ name: f.name, index: i + 1, type: f.type }],
: [{ name: f.name, index: i + base, type: f.type }],
),
...(shape.indexValue
? [
{
name: "[key: string]",
index: shape.fields.length + 1,
index: shape.fields.length + base,
type: mapOf(STRING, shape.indexValue),
},
]
@@ -101,8 +101,8 @@ export class LifetimeRegions {
/** Can a value of type `t` hold (or own something that holds) a
* reference to a ProgramLifetime instance the walk can explain? */
private reaches(t: IrType): boolean {
const arm = this.host.nullableUnions.of(t)?.arm;
if (arm !== undefined) return this.reaches(arm);
const nullable = this.host.nullableUnions.of(t);
if (nullable?.repr === "single") return this.reaches(nullable.arm);
switch (t.kind) {
case "object":
return this.isLifetime(t.className) || this.reach.get(`object:${t.className}`) === true;
@@ -287,7 +287,8 @@ export class LifetimeRegions {
* ProgramLifetime instance). */
private reportLines(struct: string, f: Field, i: number): string[] {
if (!isRefCounted(f.type) || !this.reaches(f.type)) return [];
const arm = this.host.nullableUnions.of(f.type)?.arm ?? f.type;
const nullable = this.host.nullableUnions.of(f.type);
const arm = nullable?.repr === "single" ? nullable.arm : f.type;
const load = [
` %f${i} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`,
` %v${i} = load ptr, ptr %f${i}`,
@@ -411,7 +412,7 @@ export class LifetimeRegions {
...this.reportBody(
`define internal void ${walk}(ptr %o, ptr %w) ${FN_ATTRS} { ; region walk record ${shape.id}`,
mangleRecordStruct(shape.id),
rcMembers(shape, this.host.inlineUnions),
rcMembers(shape, this.host.inlineUnions, this.host.nullableUnions.fieldBase(shape.id)),
),
);
rows.push(`{ ptr, ptr } { ptr @${mangleRecordRelease(shape.id)}, ptr ${walk} }`);
+134 -9
View File
@@ -31,7 +31,16 @@ import {
import { cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js";
import type { BlockBuilder } from "./blocks.js";
import { SCR_ARR_FIELD, llvmCommentText } from "./common.js";
import type { NullableUnions } from "./nullable-unions.js";
import {
NULLABLE_NULL,
SHAPE_WORD_INDEX,
shapeReleaseFn,
shapeTagFn,
shapeTraceFn,
shapeWordHeader,
type NullableUnions,
type ShapeUnion,
} from "./nullable-unions.js";
import {
INLINE_UNION_FIELD,
loadInlineUnion,
@@ -129,6 +138,7 @@ export { computeTraced } from "../cycle-analysis.js";
* `_v`-shaped. */
export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release: string } {
const nullable = host.nullableUnions.of(t);
if (nullable?.repr === "shapes") return shapeUnionRc(nullable);
if (nullable) {
const arm = vAdapters(host, nullable.arm);
return { retain: nullableRetainSym(host, nullable.arm), release: arm.release };
@@ -184,6 +194,7 @@ export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release
* Families with a mirrored fast path call the inline helper instead. */
export function retainSym(host: ShapeHost, t: IrType): string {
const nullable = host.nullableUnions.of(t);
if (nullable?.repr === "shapes") return shapeUnionRc(nullable).retain;
if (nullable) return nullableRetainSym(host, nullable.arm);
const stem = runtimeRcStem(t);
if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "retain") !== null) {
@@ -195,6 +206,99 @@ export function retainSym(host: ShapeHost, t: IrType): string {
return vAdapters(host, t).retain;
}
/** A shape union's RC entry points: every record retain is the same
* NULL- and immortal-skipping increment (the null sentinel is immortal),
* so the first arm's serves; release dispatches on the arm. */
function shapeUnionRc(union: ShapeUnion): { retain: string; release: string } {
return {
retain: `@${mangleRecordRetain(union.arms[0]!.shapeId)}`,
release: shapeReleaseFn(union.unionId),
};
}
/** The tag, release and trace helpers of every shape union
* (nullable-unions.ts). The tag helper maps a value to its arm: NULL and
* the null sentinel are the unit arms, anything else a tagged record whose
* shape word names its arm (the switch folds to a lookup table). */
export function emitShapeUnionHelpers(host: ShapeHost): string[] {
const out: string[] = [];
const S = host.sizeType;
for (const union of host.nullableUnions.allShapeUnions()) {
const header = shapeWordHeader(S);
const tagFn = shapeTagFn(union.unionId);
const cases = union.arms
.map((arm) => `i32 ${host.nullableUnions.shapeNumber(arm.shapeId)}, label %a${arm.tag}`)
.join(" ");
out.push(
`define internal i32 ${tagFn}(ptr %u) alwaysinline ${FN_ATTRS} { ; shape union tag`,
`entry:`,
` %isnull = icmp eq ptr %u, null`,
` br i1 %isnull, label %unit, label %ref`,
`unit:`,
` ret i32 ${union.unitTag}`,
`ref:`,
);
if (union.nullTag >= 0)
out.push(
` %isnullarm = icmp eq ptr %u, ${NULLABLE_NULL}`,
` br i1 %isnullarm, label %nullarm, label %rec`,
`nullarm:`,
` ret i32 ${union.nullTag}`,
`rec:`,
);
out.push(
` %wp = getelementptr inbounds ${header}, ptr %u, i32 0, i32 ${SHAPE_WORD_INDEX}`,
` %w = load i32, ptr %wp`,
` switch i32 %w, label %bad [ ${cases} ]`,
...union.arms.flatMap((arm) => [`a${arm.tag}:`, ` ret i32 ${arm.tag}`]),
`bad:`,
` ret i32 -1`,
`}`,
``,
);
// release: unit encodings are NULL or immortal; each arm releases
// through its own (NULL- and immortal-tolerant) helper.
out.push(
`define internal void ${shapeReleaseFn(union.unionId)}(ptr %u) ${FN_ATTRS} { ; shape union release`,
`entry:`,
` %t = call i32 ${tagFn}(ptr %u)`,
` switch i32 %t, label %done [ ${union.arms.map((arm) => `i32 ${arm.tag}, label %a${arm.tag}`).join(" ")} ]`,
...union.arms.flatMap((arm) => [
`a${arm.tag}:`,
` call void @${mangleRecordRelease(arm.shapeId)}(ptr %u)`,
` br label %done`,
]),
`done:`,
` ret void`,
`}`,
``,
);
if (shapeUnionTraced(host, union))
out.push(
`define internal void ${shapeTraceFn(union.unionId)}(ptr %u, ptr %visit, ptr %ctx) ${FN_ATTRS} { ; shape union trace`,
`entry:`,
` %t = call i32 ${tagFn}(ptr %u)`,
` switch i32 %t, label %done [ ${union.arms.map((arm) => `i32 ${arm.tag}, label %a${arm.tag}`).join(" ")} ]`,
...union.arms.flatMap((arm) => [
`a${arm.tag}:`,
` call void @${mangleRecordTrace(arm.shapeId)}(ptr %u, ptr %visit, ptr %ctx)`,
` br label %done`,
]),
`done:`,
` ret void`,
`}`,
``,
);
}
return out;
}
/** A shape union's arms carry collector headers (all or none: the emitter
* traces every arm of a union with one traced arm). */
function shapeUnionTraced(host: ShapeHost, union: ShapeUnion): boolean {
return union.arms.some((arm) => host.tracedShapes.has(`record:${arm.shapeId}`));
}
/** The retain entry point of a nullable union's value: the arm's own when
* it tolerates NULL (emitted class and record helpers), otherwise a
* NULL-skipping wrapper around it (the null sentinel and literals are
@@ -233,6 +337,7 @@ export function emitNullableRetainWrappers(host: ShapeHost): string[] {
* Families with a mirrored fast path call the inline helper instead. */
export function releaseSym(host: ShapeHost, t: IrType): string {
const nullable = host.nullableUnions.of(t);
if (nullable?.repr === "shapes") return shapeUnionRc(nullable).release;
if (nullable) return releaseSym(host, nullable.arm);
const stem = runtimeRcStem(t);
if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "release") !== null) {
@@ -643,6 +748,8 @@ export function traceAdapter(host: ShapeHost, t: IrType): string | null {
return "@scr_promise_trace_v";
case "union": {
const nullable = host.nullableUnions.get(t.unionId);
if (nullable?.repr === "shapes")
return shapeUnionTraced(host, nullable) ? shapeTraceFn(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)`);
@@ -945,15 +1052,17 @@ export function mapValKindNum(value: IrType): number {
export function rcMembers(
shape: IrRecordShape,
inline: InlineUnionFields,
/** The struct index of the first field (2 on tagged shapes). */
base = 1,
): { index: number; type: IrType; name: string }[] {
return [
...shape.fields.flatMap((f, i) =>
inline.record(f.type) ? [] : [{ index: i + 1, type: f.type, name: f.name }],
inline.record(f.type) ? [] : [{ index: i + base, type: f.type, name: f.name }],
),
...(shape.indexValue
? [
{
index: shape.fields.length + 1,
index: shape.fields.length + base,
type: mapOf(STRING, shape.indexValue),
name: "[key: string] overflow",
},
@@ -1107,7 +1216,12 @@ export function directDestroyTarget(
}
function directDestroyKey(host: ShapeHost, type: IrType): string | null {
const t = host.nullableUnions.of(type)?.arm ?? type;
const nullable = host.nullableUnions.of(type);
let t = type;
if (nullable !== null) {
if (nullable.repr === "shapes") return null;
t = nullable.arm;
}
if (t.kind === "record") return `record:${t.shapeId}`;
return t.kind === "object" ? `object:${t.className}` : null;
}
@@ -1338,8 +1452,12 @@ export function emitRecordShapes(
for (const shape of records) {
const struct = mangleRecordStruct(shape.id);
const fieldTys = shape.fields.map((f) => llFieldType(f.type, host.inlineUnions.record(f.type)));
const fieldTys: string[] = shape.fields.map((f) =>
llFieldType(f.type, host.inlineUnions.record(f.type)),
);
if (shape.indexValue) fieldTys.push("ptr"); // the overflow ScrMap *
// A tagged shape (an arm of a shape union) carries its shape number.
if (host.nullableUnions.shapeNumber(shape.id) !== 0) fieldTys.unshift("i32");
typeDefs.push(
`%${struct} = type { ${host.sizeType}${fieldTys.length ? ", " + fieldTys.join(", ") : ""} } ` +
`; record ${shape.id} { ${shape.fields.map((f) => llvmCommentText(f.name)).join("; ")}${shape.indexValue ? "; [key: string]" : ""} }`,
@@ -1352,7 +1470,7 @@ export function emitRecordShapes(
const candidates: [string, DirectDestroy][] = [];
const memberTypes: IrType[] = [];
for (const shape of records) {
const members = rcMembers(shape, host.inlineUnions);
const members = rcMembers(shape, host.inlineUnions, host.nullableUnions.fieldBase(shape.id));
for (const m of members) memberTypes.push(m.type);
if (!members.some((m) => isRefCounted(m.type) && needsBoundedRelease(m.type))) continue;
const release = mangleRecordRelease(shape.id);
@@ -1370,7 +1488,9 @@ export function emitRecordShapes(
for (const shape of records) {
const struct = mangleRecordStruct(shape.id);
const traced = host.tracedShapes.has(`record:${shape.id}`);
const members = rcMembers(shape, host.inlineUnions);
const base = host.nullableUnions.fieldBase(shape.id);
const shapeNumber = host.nullableUnions.shapeNumber(shape.id);
const members = rcMembers(shape, host.inlineUnions, base);
const refMembers = members.filter((m) => isRefCounted(m.type));
const sizeOf = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 1) to ${host.sizeType})`;
@@ -1439,6 +1559,11 @@ export function emitRecordShapes(
);
nw.push(...emitObjectAlloc(host, sizeOf, traced ? { type: cycType } : null));
nw.push(` store ${host.sizeType} 1, ptr %o`);
if (shapeNumber !== 0)
nw.push(
` %shp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${SHAPE_WORD_INDEX}`,
` store i32 ${shapeNumber}, ptr %shp ; shape number`,
);
if (shape.indexValue) {
// The overflow map (string-keyed): value handling is type-directed
// exactly like shapes.ts's overflowNewC.
@@ -1449,7 +1574,7 @@ export function emitRecordShapes(
const trace = valKind === 2 ? traceArg(host, v) : "null";
nw.push(
` %ovf = call ptr @scr_map_new(i32 1, i32 ${valKind}, ptr ${host.mapOps(["null", "null", "null", rc.retain, rc.release, trace, "null"])})`,
` %ovfp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${shape.fields.length + 1}`,
` %ovfp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${shape.fields.length + base}`,
` store ptr %ovf, ptr %ovfp`,
);
}
@@ -1465,7 +1590,7 @@ export function emitRecordShapes(
];
let i = 0;
for (const field of shape.fields) {
const index = i + 1;
const index = i + base;
const inline = host.inlineUnions.record(field.type);
const fieldTy = llFieldType(field.type, inline);
clone.push(
@@ -21,6 +21,7 @@ import type { CallLifetimes } from "./call-lifetimes.js";
import { emitCallExpr, emitConstruction } from "./expr-calls.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { emitRecordLitFields } from "./expr-primitives.js";
import { SHAPE_WORD_INDEX } from "./nullable-unions.js";
import { llFieldType } from "./shapes.js";
export type StackObjectSource = IrExpr & { kind: "recordLit" | "recordClone" | "new" | "call" };
@@ -196,12 +197,13 @@ export function stackObjectFields(
const cached = host.stackObjectFieldPtrs.get(slot);
if (cached) return cached;
const struct = stackObjectStruct(type);
const base = host.nullableUnions.fieldBase(type.shapeId);
const fields: StackObjectField[] = [];
host.recordShape(type.shapeId).fields.forEach((field, index) => {
if (!isRefCounted(field.type) || host.inlineUnions.record(field.type)) return;
const ptr = host.B.slot();
host.B.entryAllocas.push(
`${ptr} = getelementptr inbounds ${struct}, ptr ${slot}, i64 0, i32 ${index + 1}`,
`${ptr} = getelementptr inbounds ${struct}, ptr ${slot}, i64 0, i32 ${index + base}`,
);
fields.push({ name: field.name, ptr, type: field.type });
});
@@ -296,6 +298,17 @@ export function emitStackObject(
}
}
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);
if (shapeNumber !== 0) {
const p = B.tmp();
B.line(
`${p} = getelementptr inbounds ${struct}, ptr ${slot}, i64 0, i32 ${SHAPE_WORD_INDEX}`,
);
B.line(`store i32 ${shapeNumber}, ptr ${p}`);
}
}
if (e.kind === "call") {
// The entry writes the result into the slot it receives last.
emitCallExpr(
@@ -2,14 +2,19 @@
* helper generators (walkers, dyn). A tagged union is a `%ScrUnion` box; a
* 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`. Every structural read of a union value
* 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. */
import type { BlockBuilder } from "./blocks.js";
import { NULLABLE_NULL, type NullableUnion } from "./nullable-unions.js";
import { NULLABLE_NULL, shapeTagFn, type PointerUnion } from "./nullable-unions.js";
/** The i32 tag of a union value. */
export function emitUnionTag(B: BlockBuilder, nullable: NullableUnion | null, u: string): string {
export function emitUnionTag(B: BlockBuilder, nullable: PointerUnion | null, u: string): string {
const t = B.tmp();
if (nullable?.repr === "shapes") {
B.line(`${t} = call i32 ${shapeTagFn(nullable.unionId)}(ptr ${u})`);
return t;
}
if (nullable) {
const isNull = B.tmp();
B.line(`${isNull} = icmp eq ptr ${u}, null`);
@@ -32,8 +37,13 @@ export function emitUnionTag(B: BlockBuilder, nullable: NullableUnion | null, u:
/** i1: a nullable union value holds its reference arm (neither NULL nor
* the `null` sentinel). */
export function emitNullablePresent(B: BlockBuilder, nullable: NullableUnion, u: string): string {
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).
B.line(`${present} = icmp ne ptr ${u}, null`);
return present;
}
B.line(`${present} = icmp ne ptr ${u}, null`);
if (nullable.nullTag < 0) return present;
const notNullArm = B.tmp(),
@@ -46,12 +56,18 @@ export function emitNullablePresent(B: BlockBuilder, nullable: NullableUnion, u:
/** i1: a nullable union value holds the arm `tag` (`negated`: does not). */
export function emitNullableIsTag(
B: BlockBuilder,
nullable: NullableUnion,
nullable: PointerUnion,
u: string,
tag: number,
negated: boolean,
): string {
if (tag === nullable.refTag) {
if (nullable.repr === "shapes" && 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}`);
return t;
}
if (nullable.repr === "single" && tag === nullable.refTag) {
const present = emitNullablePresent(B, nullable, u);
if (!negated) return present;
const t = B.tmp();
@@ -65,7 +81,7 @@ export function emitNullableIsTag(
}
/** The BORROWED payload pointer of a reference arm. */
export function emitUnionPeek(B: BlockBuilder, nullable: NullableUnion | null, u: string): string {
export function emitUnionPeek(B: BlockBuilder, nullable: PointerUnion | null, u: string): string {
if (nullable) return u;
const p = B.tmp();
const t = B.tmp();
@@ -76,7 +92,9 @@ export function emitUnionPeek(B: BlockBuilder, nullable: NullableUnion | null, u
/** Text-template forms for helpers written as raw LLVM lines: `%out`
* receives the tag / payload of `u`. */
export function unionTagLines(nullable: NullableUnion | null, u: string, out: string): string[] {
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 && nullable.nullTag < 0)
return [
` ${out}.isnull = icmp eq ptr ${u}, null`,
@@ -95,7 +113,7 @@ export function unionTagLines(nullable: NullableUnion | null, u: string, out: st
];
}
export function unionPeekLines(nullable: NullableUnion | null, u: string, out: string): string[] {
export function unionPeekLines(nullable: PointerUnion | null, u: string, out: string): string[] {
// A plain copy of the pointer (NULL included) under the template's name.
if (nullable) return [` ${out} = getelementptr i8, ptr ${u}, i64 0`];
return [
@@ -183,7 +183,9 @@ test("a nullable-pointer source rebuilds the target box", () => {
expect(retag).not.toContain("retype.share");
});
async function llvmFor(source: string): Promise<{ module: IrModule; ir: string }> {
/** `boxed`: keep every union a tagged box (record unions otherwise become
* shape unions, which retype without any box). */
async function llvmFor(source: string, boxed = true): Promise<{ module: IrModule; ir: string }> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-union-retype-"));
try {
const entry = join(dir, "main.ts"),
@@ -194,7 +196,7 @@ async function llvmFor(source: string): Promise<{ module: IrModule; ir: string }
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
const module = deserializeModule(await readFile(output, "utf8"));
expect(validateModule(module)).toEqual([]);
return { module, ir: emitLlvmModule(module) };
return { module, ir: emitLlvmModule(boxed ? keepUnionsBoxed(module) : module) };
} finally {
await rm(dir, { recursive: true, force: true });
}
@@ -253,3 +255,16 @@ console.log(leafValue(parsed));
expect(stored).not.toBeNull();
expect(ir).toContain(`load i8, ptr ${stored![1]}`);
});
test("record unions retype as the record pointer itself", async () => {
const { module, ir } = await llvmFor(VISITOR, false);
const helpers = module.functions.filter((fn) => fn.name.startsWith("%union.retag."));
expect(helpers.length).toBeGreaterThan(0);
for (const helper of helpers) {
const symbol = `sc_bf__x25_union_retag_${helper.name.slice("%union.retag.".length)}`;
const retag = body(ir, symbol);
expect(retag).not.toMatch(/@scr_union_new|retype\.share|sc_retype_guard/);
}
// No record of the visitor is ever boxed.
expect(ir).not.toMatch(/call ptr @scr_union_new_ref/);
});
@@ -284,7 +284,8 @@ export class LlWalkers {
throw new InternalCompilerError(
`llvm emitter bug: jsonStringify of unknown shape ${shapeId}`,
);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
const fieldBase = this.host.nullableUnions.fieldBase(shapeId);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + fieldBase]));
if (recordMayKeepSource(shape)) this.emitRecordSourceWriter(B, shapeId);
// CYCLE-CAPABLE shapes bracket the walk with the circular-detection
// stack; edge labels stamp before members whose walk can re-enter —
@@ -437,7 +438,8 @@ export class LlWalkers {
throw new InternalCompilerError(`llvm emitter bug: field writer of unknown shape ${shapeId}`);
const name = `sc_jwf_${this.recordFieldWriters.size}`;
this.recordFieldWriters.set(shapeId, name);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
const fieldBase = this.host.nullableUnions.fieldBase(shapeId);
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + fieldBase]));
const byName = new Map(shape.fields.map((f) => [f.name, f]));
const cyclic = traceAdapter(this.host, { kind: "record", shapeId }) !== null;
const order = recordSourceFieldOrder(shape);
@@ -534,7 +536,7 @@ export class LlWalkers {
const ovfp = B.tmp();
const ovf = B.tmp();
B.line(
`${ovfp} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr %v, i64 0, i32 ${shape.fields.length + 1}`,
`${ovfp} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr %v, i64 0, i32 ${shape.fields.length + this.host.nullableUnions.fieldBase(shape.id)}`,
);
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
if (iv.kind === "dyn") {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5919-record-unions-shared-arms.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5930-record-union-pointers.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5931-record-union-unit-arms.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5932-record-union-trees.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5933-record-union-conversions.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5934-record-union-set-members.ts"
],
"diags": []
}
@@ -0,0 +1,60 @@
// Record unions that share arms: Third = D | A, First = A | B and
// Second = B | C. Only C is cyclic. Every arm of a union whose values the
// cycle collector can visit carries a collector header, and the shared
// arms pass that requirement from Second back through First to Third.
type A = { kind: "a"; v: number };
type B = { kind: "b"; w: number };
type C = { kind: "c"; kids: C[] };
type D = { kind: "d"; label: string };
type Third = D | A;
type First = A | B;
type Second = B | C;
function third(u: Third): string {
return u.kind === "d" ? u.label : `a${u.v}`;
}
function first(u: First): number {
return u.kind === "a" ? u.v : u.w;
}
function second(u: Second): number {
return u.kind === "b" ? u.w : u.kids.length + u.kids.reduce((n, k) => n + second(k), 0);
}
const zs: Third[] = [
{ kind: "d", label: "dee" },
{ kind: "a", v: 7 },
];
console.log(zs.map(third).join(","));
const xs: First[] = [
{ kind: "a", v: 1 },
{ kind: "b", w: 2 },
];
console.log(xs.map(first).join(","));
const leaf: C = { kind: "c", kids: [] };
const ys: Second[] = [
{ kind: "b", w: 3 },
{ kind: "c", kids: [leaf, { kind: "c", kids: [leaf] }] },
];
console.log(ys.map(second).join(","));
// One record held through two unions.
const sharedA: A = { kind: "a", v: 5 };
const sharedB: B = { kind: "b", w: 6 };
const viaThird: Third = sharedA;
const viaFirst: First[] = [sharedA, sharedB];
const viaSecond: Second = sharedB;
console.log(third(viaThird), viaFirst.map(first).join(","), second(viaSecond));
// A cyclic C reachable only through Second, dropped and rebuilt in a loop.
let total = 0;
for (let i = 0; i < 200; i++) {
const node: C = { kind: "c", kids: [] };
node.kids.push(node);
const held: Second[] = [node, { kind: "b", w: i }];
total += held.length + node.kids.length;
}
console.log("total", total);
@@ -0,0 +1,79 @@
// Unions whose arms are all object literals (discriminated unions) hold the
// record itself: construction, narrowing, storage in arrays, maps, fields
// and closures, identity, and printing behave exactly as in Node.
type Shape =
| { kind: "circle"; r: number }
| { kind: "rect"; w: number; h: number }
| { kind: "tri"; b: number; h: number; label: string };
function area(s: Shape): number {
switch (s.kind) {
case "circle":
return Math.round(3.14159 * s.r * s.r);
case "rect":
return s.w * s.h;
case "tri":
return 0.5 * s.b * s.h;
}
}
function make(i: number): Shape {
if (i % 3 === 0) return { kind: "circle", r: i };
if (i % 3 === 1) return { kind: "rect", w: i, h: i + 1 };
return { kind: "tri", b: i, h: 2, label: "t" + i };
}
const shapes: Shape[] = [];
for (let i = 0; i < 9; i++) shapes.push(make(i));
let total = 0;
for (const s of shapes) total += area(s);
console.log("total", total);
console.log(shapes.map((s) => s.kind).join(","));
console.log(shapes.filter((s) => s.kind === "rect").length);
// Identity: the same record compares equal, a structurally equal one does not.
const first = shapes[0]!;
const again: Shape = shapes[0]!;
const copy: Shape = { kind: "circle", r: 0 };
console.log(first === again, first === copy, shapes.indexOf(again), shapes.includes(copy));
// Mutation through one alias is visible through the union value.
const rect = { kind: "rect" as const, w: 2, h: 3 };
const asShape: Shape = rect;
rect.w = 10;
console.log(area(asShape), asShape === rect);
// Fields, maps and closures holding union values.
interface Holder {
current: Shape;
history: Shape[];
}
const holder: Holder = { current: make(1), history: [] };
for (let i = 0; i < 4; i++) {
holder.history.push(holder.current);
holder.current = make(i + 5);
}
console.log(holder.current.kind, holder.history.map(area).join(" "));
const byName = new Map<string, Shape>();
for (const s of shapes) byName.set(s.kind + area(s), s);
console.log(byName.size, byName.get("rect2")?.kind, byName.get("missing") === undefined);
const pick = (k: Shape["kind"]): (() => Shape | undefined) => {
const found = shapes.find((s) => s.kind === k);
return () => found;
};
const getTri = pick("tri");
const tri = getTri();
console.log(tri !== undefined && tri.kind === "tri" ? tri.label : "none");
// Printing and serialization see the plain objects.
console.log(shapes.slice(0, 3));
console.log(JSON.stringify(shapes.slice(0, 3)));
const second = shapes[2]!;
console.log(`${String(shapes[1]!.kind)} ${"label" in second} ${"label" in shapes[1]!}`);
// Spread copies and sorting.
const grown = shapes.map((s): Shape => (s.kind === "circle" ? { ...s, r: s.r + 1 } : s));
grown.sort((a, b) => area(a) - area(b));
console.log(grown.map((s) => `${s.kind}:${area(s)}`).join(" "));
@@ -0,0 +1,67 @@
// Record unions with undefined and null arms: NULL and the null sentinel
// encode the unit arms, optional properties keep their absent state, and
// narrowing, nullish operators and equality agree with Node.
type Ok = { ok: true; value: number };
type Err = { ok: false; error: string };
type Res = Ok | Err;
function parse(text: string): Res {
const n = Number(text);
return Number.isNaN(n) ? { ok: false, error: `bad ${text}` } : { ok: true, value: n };
}
function lookup(table: Res[], i: number): Res | undefined {
return i < table.length ? table[i] : undefined;
}
function maybe(i: number): Res | null | undefined {
if (i % 3 === 0) return null;
if (i % 3 === 1) return undefined;
return parse(String(i));
}
const table = ["1", "x", "3"].map(parse);
for (let i = 0; i < 4; i++) {
const r = lookup(table, i);
if (r === undefined) console.log(i, "missing");
else if (r.ok) console.log(i, "value", r.value);
else console.log(i, "error", r.error);
}
for (let i = 0; i < 6; i++) {
const m = maybe(i);
const tag = m === null ? "null" : m === undefined ? "undefined" : m.ok ? "ok" : "err";
console.log(i, tag, m == null, m ?? "default", typeof m);
}
// Optional properties of a record-union type.
interface Slot {
name: string;
result?: Res;
last?: Res | null;
}
const slots: Slot[] = [
{ name: "a" },
{ name: "b", result: parse("7") },
{ name: "c", result: parse("q"), last: null },
];
for (const s of slots) {
console.log(s.name, "result" in s, s.result?.ok, s.last === null, s.last === undefined);
}
console.log(JSON.stringify(slots));
slots[0]!.result = table[1];
slots[1]!.result = undefined;
console.log(slots.map((s) => (s.result === undefined ? "-" : s.result.ok ? "ok" : "err")).join(""));
// Equality across arms and unit encodings.
const a: Res | undefined = table[0];
const b: Res | undefined = undefined;
const c: Res | null = null;
console.log(a === table[0], a === b, b === undefined, c === null, (c as Res | null) === table[2]);
// Unions of records reaching unknown and back.
const values: unknown[] = [table[0], table[1], undefined, null];
for (const v of values) {
if (v === null || v === undefined) console.log("unit", v);
else console.log("object", JSON.stringify(v));
}
+77
View File
@@ -0,0 +1,77 @@
// A recursive discriminated union (an expression tree) whose leaf arm holds
// no references: every arm shares the cycle collector's header, so trees
// can be built, rewritten, shared and dropped in cycles.
type Expr =
| { kind: "num"; value: number }
| { kind: "neg"; arg: Expr }
| { kind: "add"; left: Expr; right: Expr }
| { kind: "ref"; target: { expr: Expr | null } };
function evaluate(e: Expr): number {
switch (e.kind) {
case "num":
return e.value;
case "neg":
return -evaluate(e.arg);
case "add":
return evaluate(e.left) + evaluate(e.right);
case "ref":
return e.target.expr === null ? 0 : evaluate(e.target.expr);
}
}
function build(depth: number, seed: number): Expr {
if (depth === 0) return { kind: "num", value: seed % 10 };
if (seed % 4 === 0) return { kind: "neg", arg: build(depth - 1, seed + 1) };
return { kind: "add", left: build(depth - 1, seed * 3 + 1), right: build(depth - 1, seed + 7) };
}
function simplify(e: Expr): Expr {
switch (e.kind) {
case "neg": {
const inner = simplify(e.arg);
return inner.kind === "neg" ? inner.arg : inner.kind === "num" ? { kind: "num", value: -inner.value } : { kind: "neg", arg: inner };
}
case "add": {
const l = simplify(e.left);
const r = simplify(e.right);
if (l.kind === "num" && r.kind === "num") return { kind: "num", value: l.value + r.value };
return l === e.left && r === e.right ? e : { kind: "add", left: l, right: r };
}
default:
return e;
}
}
function show(e: Expr): string {
switch (e.kind) {
case "num":
return String(e.value);
case "neg":
return `-(${show(e.arg)})`;
case "add":
return `(${show(e.left)} + ${show(e.right)})`;
case "ref":
return "ref";
}
}
let checksum = 0;
for (let round = 0; round < 50; round++) {
const tree = build(6, round);
const simple = simplify(tree);
checksum += evaluate(tree) * 3 + evaluate(simple);
// A cycle through a record arm: the tree refers back to itself.
const target: { expr: Expr | null } = { expr: null };
const loop: Expr = { kind: "add", left: tree, right: { kind: "ref", target } };
target.expr = { kind: "num", value: round };
checksum += evaluate(loop);
target.expr = loop.left;
}
console.log(checksum);
console.log(show(simplify(build(3, 5))));
console.log(show(build(2, 4)), evaluate(build(4, 2)));
const shared: Expr = { kind: "num", value: 4 };
const twice: Expr = { kind: "add", left: shared, right: shared };
console.log(evaluate(twice), twice.kind === "add" && twice.left === twice.right);
@@ -0,0 +1,78 @@
// Record unions meet other representations: widening into unions with
// string and number arms, narrowing back, `in` tests on undiscriminated
// arms, generic helpers, class fields and promises.
type Point = { x: number; y: number };
type Named = { name: string };
type Item = Point | Named;
type Wide = Item | string | number;
function describe(i: Item): string {
return "x" in i ? `point(${i.x},${i.y})` : `named(${i.name})`;
}
function widen(i: Item): Wide {
return i;
}
function narrow(w: Wide): Item | undefined {
if (typeof w === "string" || typeof w === "number") return undefined;
return w;
}
const items: Item[] = [{ x: 1, y: 2 }, { name: "a" }, { x: 3, y: 4 }];
const wide: Wide[] = items.map(widen);
wide.push("text", 42);
for (const w of wide) {
const back = narrow(w);
console.log(typeof w, back === undefined ? "-" : describe(back), back !== undefined && items.includes(back));
}
// Generic helpers instantiate over the union.
function firstWhere<T>(xs: T[], pred: (x: T) => boolean): T | undefined {
for (const x of xs) if (pred(x)) return x;
return undefined;
}
const named = firstWhere(items, (i) => "name" in i);
console.log(named === items[1], named !== undefined ? describe(named) : "none");
const pair: [Item, Item] = [items[2]!, items[0]!];
const [p0, p1] = pair;
console.log(describe(p0), describe(p1));
// Class fields and methods.
class Cursor {
private at: Item;
readonly seen: Item[] = [];
constructor(start: Item) {
this.at = start;
}
move(next: Item): Item {
const prev = this.at;
this.seen.push(prev);
this.at = next;
return prev;
}
get current(): Item {
return this.at;
}
}
const cursor = new Cursor(items[0]!);
for (const i of items) cursor.move(i);
console.log(describe(cursor.current), cursor.seen.map(describe).join(" "));
// Promises carry union payloads.
async function load(i: number): Promise<Item> {
await null;
return items[i % items.length]!;
}
async function main(): Promise<void> {
const loaded: Item[] = [];
for (const i of [0, 1, 5]) loaded.push(await load(i));
console.log(loaded.map(describe).join(" "), loaded[0] === items[0]);
const slow = new Promise<Item>((resolve) => setTimeout(() => resolve({ x: 9, y: 9 }), 5));
const raced = await Promise.race([load(1), slow]);
console.log(describe(raced));
const pending: Promise<Item>[] = [load(2), load(0)];
const all = await Promise.all(pending);
console.log(all.map(describe).join(" "), all[1] === items[0]);
}
main().then(() => console.log("done"));
@@ -0,0 +1,25 @@
// A record union used as Set elements and Map keys keeps the runtime's
// tagged representation; other unions over the same records convert to and
// from it without changing identity semantics.
type Point = { x: number; y: number };
type Named = { name: string };
type Item = Point | Named;
const items: Item[] = [{ x: 1, y: 2 }, { name: "a" }, { x: 3, y: 4 }];
const marked = new Set<Item>();
marked.add(items[0]!);
marked.add(items[0]!);
marked.add({ x: 1, y: 2 });
console.log(marked.size, marked.has(items[0]!), marked.has(items[1]!));
const counts = new Map<Item, number>();
for (const i of [...items, items[0]!, items[2]!]) counts.set(i, (counts.get(i) ?? 0) + 1);
console.log([...counts.values()].join(","));
function maybeItem(i: number): Item | undefined {
return i < items.length ? items[i] : undefined;
}
for (let i = 0; i < 4; i++) {
const m = maybeItem(i);
console.log(i, m === undefined ? "none" : "x" in m ? m.x : m.name, m !== undefined && marked.has(m));
}
+12
View File
@@ -2137,6 +2137,7 @@
"5913-prototype-type-error-messages.ts": 2484,
"5914-dyn-in-class-prototype-members.ts": 3169,
"5915-dyn-error-string-conversions.ts": 3200,
"5919-record-unions-shared-arms.ts": 3182,
"5920-promise-try-args.ts": 3028,
"5921-promise-try-args-js.js": 3179,
"5922-matchall-groups-callbacks.ts": 3650,
@@ -2149,10 +2150,15 @@
"5929-groupby-js.js": 2997,
"5930-contract-int32-arrays.ts": 3715,
"5930-method-census-asserted-number-keys.ts": 3330,
"5930-record-union-pointers.ts": 3182,
"5931-asserted-reads-keep-number-parameters.ts": 2990,
"5931-record-union-unit-arms.ts": 2745,
"5932-generic-class-observed-methods.ts": 4272,
"5932-record-union-trees.ts": 2863,
"5933-class-fields-unknown-object.ts": 4189,
"5933-record-union-conversions.ts": 4140,
"5934-js-class-fields-property-order.js": 4613,
"5934-record-union-set-members.ts": 2265,
"5935-js-subclass-fields-and-element-calls.js": 2484,
"5936-checked-js-class-types.js": 3412,
"5937-override-narrowed-union-returns.ts": 3036,
@@ -5167,6 +5173,7 @@
"5913-prototype-type-error-messages.ts": 4068,
"5914-dyn-in-class-prototype-members.ts": 4327,
"5915-dyn-error-string-conversions.ts": 4247,
"5919-record-unions-shared-arms.ts": 4941,
"5920-promise-try-args.ts": 3195,
"5921-promise-try-args-js.js": 3082,
"5922-matchall-groups-callbacks.ts": 5960,
@@ -5179,10 +5186,15 @@
"5929-groupby-js.js": 3482,
"5930-contract-int32-arrays.ts": 4745,
"5930-method-census-asserted-number-keys.ts": 4815,
"5930-record-union-pointers.ts": 4941,
"5931-asserted-reads-keep-number-parameters.ts": 3015,
"5931-record-union-unit-arms.ts": 4558,
"5932-generic-class-observed-methods.ts": 5678,
"5932-record-union-trees.ts": 4329,
"5933-class-fields-unknown-object.ts": 4834,
"5933-record-union-conversions.ts": 4357,
"5934-js-class-fields-property-order.js": 4883,
"5934-record-union-set-members.ts": 4003,
"5935-js-subclass-fields-and-element-calls.js": 4470,
"5936-checked-js-class-types.js": 4183,
"5937-override-narrowed-union-returns.ts": 3827,