mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Represent unions of records as the record pointer itself (#1225)
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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} }`);
|
||||
|
||||
@@ -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") {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5919-record-unions-shared-arms.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5930-record-union-pointers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5931-record-union-unit-arms.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5932-record-union-trees.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5933-record-union-conversions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user