Answer cross-layout record casts with views of the original record (#2419)

This commit is contained in:
Chris Tate
2026-10-27 13:47:06 -05:00
committed by GitHub
15 changed files with 768 additions and 161 deletions
+53 -70
View File
@@ -1,5 +1,5 @@
import { emitPerKeyOrder } from "./key-orders.js";
import { rcMembers, typedRefConstructor } from "./shapes.js";
import { typedRefConstructor } from "./shapes.js";
import { bindLiveArrayOps } from "./expr-stream-bridges.js";
import {
BYTES_ELEM_NUM,
@@ -45,7 +45,6 @@ import {
DYN_HANDLE_KINDS,
isDirentShape,
KEY_ORDER_FIELD,
shapeHasAccessorSlots,
shapeKeyOrders,
isDynTypedRefType,
isRefCounted,
@@ -261,7 +260,7 @@ export class LlDyn {
/** Emitted function definitions, in interning order. */
readonly defs: string[] = [];
private readonly prefixSources = new Map<string, string[]>();
private readonly recordViewTables = new Map<string, string>();
constructor(private readonly host: DynHost) {}
@@ -1161,51 +1160,47 @@ export class LlDyn {
);
}
/** The record shapes whose values a pointer of shape `targetId` may
* reference unchanged: their struct begins with exactly the target's
* fields (same names, types and representations, so the same offsets),
* and everything the target's release, teardown and collector support
* would get wrong on them is absent. The extra fields hold no
* references (the target's release frees the block without visiting
* them; frees are size-class agnostic), neither shape carries a shape
* word, a cycle header, an overflow map, key order variants or accessor
* slots, and both freeze alike. A record that keeps undeclared source
* properties is refused at run time (scr_dyn_typed_ref_prefix_view). */
private prefixViewSources(targetId: string): string[] {
const memo = this.prefixSources.get(targetId);
if (memo) return memo;
/** `scr_dyn_record_view(d, sources, n, key, key_len, new)` for
* view target `targetId`: the view (+1) of the record a capsule `%d`
* holds when its type is one of the target's sources, else NULL. The
* sources table lists each source's type key and the byte offsets of
* the target's fields in it. */
private recordViewCall(B: BlockBuilder, targetId: string): string {
const host = this.host;
const plain = (shape: IrRecordShape): boolean =>
!shape.tuple &&
!shape.indexValue &&
!isDirentShape(shape) &&
!shapeHasAccessorSlots(shape) &&
shapeKeyOrders(shape).length === 1 &&
!shape.fields.some((field) => field.name === KEY_ORDER_FIELD) &&
host.nullableUnions.shapeNumber(shape.id) === 0 &&
!host.tracedShapes.has(`record:${shape.id}`);
const target = host.recordsById.get(targetId);
const out: string[] = [];
if (target && plain(target) && target.fields.length > 0) {
const base = host.nullableUnions.fieldBase(targetId);
for (const shape of host.recordsById.values()) {
if (shape.id === targetId || shape.fields.length <= target.fields.length) continue;
if (!plain(shape) || shape.freezable !== target.freezable) continue;
if (host.nullableUnions.fieldBase(shape.id) !== base) continue;
const prefix = target.fields.every(
(field, i) =>
shape.fields[i]!.name === field.name &&
typeKey(shape.fields[i]!.type) === typeKey(field.type),
let table = this.recordViewTables.get(targetId);
const sources = host.recordViews!.sources(targetId);
const target = host.recordsById.get(targetId)!;
if (table === undefined) {
table = `@sc_rv_${this.recordViewTables.size}`;
this.recordViewTables.set(targetId, table);
const S = this.S;
const entries = sources.map((sourceId, k) => {
const source = host.recordsById.get(sourceId)!;
const struct = mangleRecordStruct(sourceId);
const base = host.nullableUnions.fieldBase(sourceId);
const offsets = target.fields.map((field) => {
const index = source.fields.findIndex((f) => f.name === field.name) + base;
return `${S} ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 0, i32 ${index}) to ${S})`;
});
const offsetTable = `${table}_${k}`;
this.defs.push(
`${offsetTable} = internal constant [${offsets.length} x ${S}] [${offsets.join(", ")}]`,
);
if (!prefix) continue;
const extra = rcMembers(shape, host.inlineUnions, base).some(
(member) => member.index >= target.fields.length + base && isRefCounted(member.type),
);
if (!extra) out.push(shape.id);
}
const key = typeKey({ kind: "record", shapeId: sourceId });
return `{ ptr, ${S}, ptr } { ptr ${host.cstr(key)}, ${S} ${Buffer.byteLength(key, "utf8")}, ptr ${offsetTable} }`;
});
this.defs.push(
`${table} = internal constant [${entries.length} x { ptr, ${S}, ptr }] [${entries.join(", ")}]`,
``,
);
}
this.prefixSources.set(targetId, out);
return out;
const key = typeKey({ kind: "record", shapeId: targetId });
host.declare(`declare ptr @scr_dyn_record_view(ptr, ptr, ${this.S}, ptr, ${this.S}, ptr)`);
const view = B.tmp();
B.line(
`${view} = call ptr @scr_dyn_record_view(ptr %d, ptr ${table}, ${this.S} ${sources.length}, ptr ${host.cstr(key)}, ${this.S} ${Buffer.byteLength(key, "utf8")}, ptr @${mangleRecordNew(targetId)})`,
);
return view;
}
private typedRefMatchesAny(B: BlockBuilder, types: readonly IrType[]): string {
@@ -2222,31 +2217,19 @@ export class LlDyn {
B.line(`${ref} = call ptr @scr_dyn_typed_ref_unbox(ptr %d)`);
B.terminate(`ret ptr ${ref}`);
B.startBlock(lNext);
if (t.kind === "record") {
// A record whose storage begins with exactly this type's storage
// is this type's value too: the conversion passes the pointer, so
// both names keep one object (decision D3).
const sources = this.prefixViewSources(t.shapeId);
if (sources.length > 0) {
const viewed = this.typedRefMatchesAny(
B,
sources.map((shapeId) => ({ kind: "record", shapeId })),
);
const lView = B.newLabel("dc.prefix");
const lOther = B.newLabel("dc.noprefix");
B.condBr(viewed, lView, lOther);
B.startBlock(lView);
host.declare(`declare ptr @scr_dyn_typed_ref_prefix_view(ptr)`);
const view = B.tmp();
B.line(`${view} = call ptr @scr_dyn_typed_ref_prefix_view(ptr %d)`);
const passed = B.tmp();
B.line(`${passed} = icmp ne ptr ${view}, null`);
const lPass = B.newLabel("dc.prefix.pass");
B.condBr(passed, lPass, lOther);
B.startBlock(lPass);
B.terminate(`ret ptr ${view}`);
B.startBlock(lOther);
}
if (t.kind === "record" && host.recordViews?.targets.has(t.shapeId) === true) {
// A capsule of a record with all of this type's fields converts
// to a view of that record: both names keep one object
// (record-views.ts, decision D3).
const view = this.recordViewCall(B, t.shapeId);
const viewed = B.tmp();
B.line(`${viewed} = icmp ne ptr ${view}, null`);
const lView = B.newLabel("dc.view");
const lOther = B.newLabel("dc.noview");
B.condBr(viewed, lView, lOther);
B.startBlock(lView);
B.terminate(`ret ptr ${view}`);
B.startBlock(lOther);
}
// A constructor-function stream (scr_dyn_builtin_stream.c) carries
// its runtime stream.
+43 -4
View File
@@ -358,6 +358,7 @@ import {
import { children, type Node } from "./region-ir.js";
import { LlDyn, recordDynKeepsIdentity, type DynHost } from "./dyn.js";
import { VirtualBorrows } from "./virtual-borrows.js";
import { planRecordViews, recordViewFieldPtr } from "./record-views.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import { LlWalkers } from "./walkers.js";
import { isObjectArm, NULLABLE_ABSENT, NULLABLE_NULL, NullableUnions } from "./nullable-unions.js";
@@ -1397,6 +1398,8 @@ export class LlEmitter {
readonly dyn: LlDyn;
/** Explicit callback boundary for standalone layout and walker stages. */
readonly shapeHost: DynHost;
/** The record view plan's target set (record-views.ts), shared by reference. */
private recordViewTargets: Set<string> | null = null;
/** External declarations, in first-use order. */
readonly decls = new Set<string>();
/** Declared functions referenced as values: each needs an env-signature
@@ -2409,6 +2412,20 @@ export class LlEmitter {
}
}
}
// Planned before any host copies the shape host (record-views.ts).
{
const views = planRecordViews(mod, {
traced: (id) => this.tracedShapes.has(`record:${id}`),
// A tagged shape's values share field reads with the other arms of
// its shape union.
excluded: (id) =>
this.nullableUnions.shapeNumber(id) !== 0 ||
this.publishedTypes?.recordGuarded(id) === true ||
mod.workers === true,
});
this.shapeHost.recordViews = views;
this.recordViewTargets = views.targets as Set<string>;
}
this.walkers = new LlWalkers(this.shapeHost);
this.dyn = new LlDyn(this.shapeHost);
for (const g of mod.globals ?? []) {
@@ -2531,6 +2548,13 @@ export class LlEmitter {
this.shapeHost.arenasClosed = this.arenas.allClosed;
}
}
// Arena shapes are known only now; the plan's target set is shared by
// every host that copied the shape host, so dropping them here reaches
// all of them.
const arenaShapes = this.shapeHost.arenaShapes;
const viewTargets = this.recordViewTargets;
if (arenaShapes !== undefined && viewTargets !== null)
for (const id of arenaShapes) viewTargets.delete(id);
for (const [name, rec] of RUNTIME_ERROR_CLASSES) {
const meta = this.classMeta.get(name);
if (!meta) break; // hand-written IR without the builtin defs: no stamps
@@ -4852,10 +4876,25 @@ export class LlEmitter {
throw new InternalCompilerError(
`llvm emitter bug: unknown field ${field} on shape ${shapeId}`,
);
const p = this.B.tmp();
this.B.line(
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${idx + this.nullableUnions.fieldBase(shapeId)}`,
);
const base = this.nullableUnions.fieldBase(shapeId);
let p: string;
if (this.shapeHost.recordViews?.targets.has(shapeId) === true) {
p = recordViewFieldPtr(
this.B,
this.sizeType,
mangleRecordStruct(shapeId),
objName,
idx + base,
idx,
(decl) => this.declare(decl),
this.rcMode === undefined,
);
} else {
p = this.B.tmp();
this.B.line(
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${idx + base}`,
);
}
const type = shape.fields[idx]!.type;
return { ptr: p, type, inline: this.inlineUnions.record(type) };
}
@@ -0,0 +1,130 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile, deserializeModule } from "../../index.js";
import type { IrModule } from "../../ir/ir.js";
import { computeTraced } from "../cycle-analysis.js";
import { planRecordViews } from "./record-views.js";
/** The views planned for `source`'s module, as target field names → source
* field names (each list sorted). */
async function plannedViews(source: string): Promise<Map<string, string[]>> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-record-views-"));
try {
const entry = join(dir, "main.ts"),
outPath = join(dir, "main.ir.json");
await writeFile(entry, source);
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
const mod: IrModule = deserializeModule(await readFile(outPath, "utf8"));
const traced = computeTraced(mod).shapes;
const views = planRecordViews(mod, {
traced: (id) => traced.has(`record:${id}`),
excluded: () => false,
});
const names = new Map(
(mod.records ?? []).map((shape) => [shape.id, shape.fields.map((f) => f.name).join(",")]),
);
return new Map(
[...views.targets].map((target) => [
names.get(target)!,
views.sources(target).map((id) => names.get(id)!),
]),
);
} finally {
await rm(dir, { recursive: true, force: true });
}
}
const conversions = `
const o: unknown = wide;
const v = o as AC;
v.a = 2;
console.log(wide.a, v.a, o === v);
`;
test("a checked conversion of a live record capsule into a record type it covers plans a view", async () => {
const views = await plannedViews(`
type Wide = { a: number; b: string; c: boolean };
type AC = { a: number; c: boolean };
const wide: Wide = { a: 1, b: "x", c: true };
${conversions}`);
expect(views).toEqual(new Map([["a,c", ["a,b,c"]]]));
});
test("a source whose fields can hold the target type keeps the copy", async () => {
// A view stored in its own source would be a cycle through the view's
// capsule reference, which the cycle analysis does not see.
const views = await plannedViews(`
type AC = { a: number; c: boolean };
type Holder = { items: AC[] };
type Wide = { a: number; b: string; c: boolean; self: AC | undefined; holder: Holder };
const wide: Wide = { a: 1, b: "x", c: true, self: undefined, holder: { items: [] } };
${conversions}
wide.self = v;
wide.holder.items.push(v);`);
expect(views).toEqual(new Map());
});
test("a source that holds the target type through a class instance keeps the copy", async () => {
const views = await plannedViews(`
type AC = { a: number; c: boolean };
class Base { note = 0; }
class Keeper extends Base { kept: AC | undefined = undefined; }
type Wide = { a: number; b: string; c: boolean; keeper: Base };
const wide: Wide = { a: 1, b: "x", c: true, keeper: new Keeper() };
${conversions}`);
expect(views).toEqual(new Map());
});
test("a source that holds itself still views a type it cannot hold", async () => {
const views = await plannedViews(`
type Tree = { a: number; b: string; c: boolean; children: Tree[] };
type AC = { a: number; c: boolean };
const wide: Tree = { a: 1, b: "x", c: true, children: [] };
wide.children.push({ a: 2, b: "y", c: false, children: [] });
${conversions}`);
expect(views).toEqual(new Map([["a,c", ["a,b,c,children"]]]));
});
test("of two sources that can hold each other's views, one keeps the copy", async () => {
// Each source only holds the other's target type, but a view of that type
// names the other source, so the two views could form a cycle. Without
// Parent's view, Kid holds only a copy and its own view is safe.
const views = await plannedViews(`
type PView = { id: number; name: string };
type KView = { id: number; tag: string };
type Parent = { id: number; name: string; kids: KView[] };
type Kid = { id: number; tag: string; parent: PView | undefined };
const parent: Parent = { id: 1, name: "p", kids: [] };
const kid: Kid = { id: 2, tag: "k", parent: undefined };
const p: unknown = parent;
const k: unknown = kid;
kid.parent = p as PView;
parent.kids.push(k as KView);
console.log(parent.kids.length, kid.parent === p);`);
expect(views).toEqual(new Map([["id,tag", ["id,parent,tag"]]]));
});
test("a source with a function field keeps the copy", async () => {
// A function can capture anything, the view included.
const views = await plannedViews(`
type AC = { a: number; c: boolean };
type Wide = { a: number; b: string; c: boolean; f: () => number };
const wide: Wide = { a: 1, b: "x", c: true, f: () => 1 };
${conversions}`);
expect(views).toEqual(new Map());
});
test("a source that may be frozen keeps the copy", async () => {
// Writes through a view take the target type's stores, which have no
// frozen-write guard.
const views = await plannedViews(`
type Wide = { a: number; b: string; c: boolean };
type AC = { a: number; c: boolean };
const wide: Wide = { a: 1, b: "x", c: true };
${conversions}
Object.freeze(wide);`);
expect(views).toEqual(new Map());
});
@@ -0,0 +1,268 @@
/* Record views: one object across record layouts (decision D3).
*
* A checked conversion of a capsule of a native record A into another
* record type B whose fields A also has (same names and types, any
* offsets) answers a view: a B object, its rc word marked with the frozen
* bit, whose record-source entry names A and A's byte offsets for B's
* fields (scr_dyn_record_view). Every field address of a B value tests
* the mark (recordViewFieldPtr) and takes A's slot for a view, so reads
* and writes through either name reach the same storage, and boxing a
* view answers A's capsule, so identity holds too. B's storage itself is
* unchanged.
*
* Only the target types of such conversions carry the test. They are
* found from the IR, before any code is emitted: B is a record type a
* checked conversion can produce (the closure of dynCheck targets), and
* some other record type whose values reach untyped code as live capsules
* (the closure of live dynFrom types) has all of B's fields. A program
* without checked conversions has none, so its code is unchanged. */
import {
isDirentShape,
KEY_ORDER_FIELD,
shapeHasAccessorSlots,
shapeKeyOrders,
typeKey,
type IrModule,
type IrRecordShape,
type IrType,
} from "../../ir/ir.js";
import { everyStmtList } from "../../ir/traverse.js";
import type { BlockBuilder } from "./blocks.js";
export interface RecordViews {
/** Shape ids whose values may be views. */
readonly targets: ReadonlySet<string>;
/** The record shapes whose capsules convert to a view of `target`. */
sources(target: string): readonly string[];
}
export const NO_RECORD_VIEWS: RecordViews = { targets: new Set(), sources: () => [] };
interface ViewOptions {
readonly traced: (shapeId: string) => boolean;
readonly excluded: (shapeId: string) => boolean;
}
/** Value kinds that hold no reference to a record. */
const LEAF_KINDS: ReadonlySet<IrType["kind"]> = new Set([
"f64",
"bool",
"string",
"bigint",
"date",
"regex",
"bytes",
"symbol",
"url",
"searchParams",
"stats",
"undefinedT",
"nullT",
"void",
]);
function plainShape(shape: IrRecordShape): boolean {
return (
!shape.tuple &&
!shape.indexValue &&
shape.fields.length > 0 &&
!isDirentShape(shape) &&
!shapeHasAccessorSlots(shape) &&
shapeKeyOrders(shape).length === 1 &&
!shape.fields.some((field) => field.name === KEY_ORDER_FIELD || field.name.startsWith("%"))
);
}
/** Whether every field of `target` is a field of `source` with the same
* type (the view reads A's field with B's representation). */
function covers(source: IrRecordShape, target: IrRecordShape): boolean {
const fields = new Map(source.fields.map((field) => [field.name, typeKey(field.type)]));
return target.fields.every((field) => fields.get(field.name) === typeKey(field.type));
}
export function planRecordViews(mod: IrModule, options: ViewOptions): RecordViews {
const shapes = new Map((mod.records ?? []).map((shape) => [shape.id, shape]));
const unions = new Map((mod.unions ?? []).map((union) => [union.id, union]));
// Targets: record types a checked conversion produces. Sources: record
// types whose values reach untyped code as live capsules. A view arises
// only where the two meet; elsewhere a conversion keeps its copy.
const closure = (roots: IrType[]): Set<string> => {
const out = new Set<string>();
const seen = new Set<string>();
const visit = (t: IrType): void => {
const key = typeKey(t);
if (seen.has(key)) return;
seen.add(key);
switch (t.kind) {
case "record": {
out.add(t.shapeId);
for (const field of shapes.get(t.shapeId)?.fields ?? []) visit(field.type);
return;
}
case "array":
visit(t.elem);
return;
case "union":
for (const arm of unions.get(t.unionId)?.arms ?? []) visit(arm);
return;
default:
return;
}
};
for (const root of roots) visit(root);
return out;
};
// The views planned so far: target shape → the sources it may view.
const sources = new Map<string, string[]>();
// Whether a value of a record can hold a value of another record type:
// through its fields, elements, map entries, union arms and overflow
// properties, and through the record a view of it names. Class instances,
// functions, untyped values, promises and the other kinds that can
// reference arbitrary objects count as holding every type.
const held = new Map<string, Set<string> | "any">();
const holds = (shapeId: string): Set<string> | "any" => {
const known = held.get(shapeId);
if (known) return known;
const out = new Set<string>();
const seen = new Set<string>();
const visit = (t: IrType): boolean => {
const key = typeKey(t);
if (seen.has(key)) return true;
seen.add(key);
switch (t.kind) {
case "record": {
out.add(t.shapeId);
const shape = shapes.get(t.shapeId);
if (shape?.indexValue !== undefined && !visit(shape.indexValue)) return false;
if (!(sources.get(t.shapeId) ?? []).every((id) => visit({ kind: "record", shapeId: id })))
return false;
return (shape?.fields ?? []).every((field) => visit(field.type));
}
case "array":
case "set":
return visit(t.elem);
case "map":
return visit(t.key) && visit(t.value);
case "union":
return (unions.get(t.unionId)?.arms ?? []).every(visit);
default:
return LEAF_KINDS.has(t.kind);
}
};
const result = visit({ kind: "record", shapeId }) ? out : "any";
held.set(shapeId, result);
return result;
};
const checked: IrType[] = [];
const boxed: IrType[] = [];
for (const fn of mod.functions)
everyStmtList(fn.body, {
stmt: () => true,
expr: (e) => {
if (e.kind === "dynCheck") checked.push(e.type);
else if (e.kind === "dynFrom" && e.liveRef === true) boxed.push(e.value.type);
return true;
},
});
if (checked.length === 0 || boxed.length === 0) return NO_RECORD_VIEWS;
const reachable = closure(checked);
const live = closure(boxed);
if (reachable.size === 0) return NO_RECORD_VIEWS;
for (const id of reachable) {
const target = shapes.get(id);
if (!target || !plainShape(target) || target.freezable === true) continue;
if (options.traced(id) || options.excluded(id)) continue;
const found: string[] = [];
for (const source of shapes.values()) {
if (source.id === id || !live.has(source.id) || !plainShape(source)) continue;
// A view writes the source's slots through the target type, which has
// no frozen-write guard, so a source that may be frozen keeps the copy.
if (source.freezable === true) continue;
if (covers(source, target)) found.push(source.id);
}
if (found.length > 0) sources.set(id, found);
}
// A view's record-source entry owns the source's capsule, an edge the
// cycle analysis does not see. A view stays off every cycle only when its
// source cannot hold a value of the target type, such as the view itself,
// including through other views. Dropping a pair removes edges, so the
// check repeats until no pair is dropped.
for (let changed = true; changed;) {
changed = false;
held.clear();
for (const [id, list] of sources) {
const kept = list.filter((source) => {
const sourceHolds = holds(source);
return sourceHolds !== "any" && !sourceHolds.has(id);
});
if (kept.length === list.length) continue;
changed = true;
if (kept.length > 0) sources.set(id, kept);
else sources.delete(id);
}
}
if (sources.size === 0) return NO_RECORD_VIEWS;
const targets = new Set(sources.keys());
return { targets, sources: (target) => sources.get(target) ?? [] };
}
/** Lines setting `dest` (i1) to whether the record `obj` of a view target
* type is a view. A view's rc word carries the frozen bit (scr_rc.h
* SCR_RC_FROZEN), which no other value of such a type has: view targets
* are never freezable, and a word with the program-lifetime bit (an
* immortal one included) is no view, so a view's word is the frozen bit
* plus a count. The
*/
export function recordViewTestLines(sizeType: "i32" | "i64", obj: string, dest: string): string[] {
// The frozen bit with a count and no program-lifetime bit: one range.
const [bit, span] =
sizeType === "i64" ? ["4611686018427387904", "4294967296"] : ["1073741824", "1073741824"];
return [
` ${dest}.w = load ${sizeType}, ptr ${obj}`,
` ${dest}.d = sub ${sizeType} ${dest}.w, ${bit}`,
` ${dest} = icmp ult ${sizeType} ${dest}.d, ${span}`,
];
}
/** The address of field `position` (declaration index) of the record
* `obj` of view-target shape `struct`: the slot itself, or the viewed
* record's slot when `obj` is a view. Optimized builds test the mark
* inline and look the slot up out of line; unoptimized builds, which give
* every value a frame slot, make the address one call. */
export function recordViewFieldPtr(
B: BlockBuilder,
sizeType: "i32" | "i64",
struct: string,
obj: string,
slotIndex: number,
position: number,
declare: (decl: string) => void,
outOfLine: boolean,
): string {
if (outOfLine) {
declare(`declare ptr @scr_record_view_field(ptr, ${sizeType}, ${sizeType})`);
const ptr = B.tmp();
B.line(
`${ptr} = call ptr @scr_record_view_field(ptr ${obj}, ${sizeType} ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 0, i32 ${slotIndex}) to ${sizeType}), ${sizeType} ${position})`,
);
return ptr;
}
const own = B.tmp();
B.line(`${own} = getelementptr inbounds %${struct}, ptr ${obj}, i64 0, i32 ${slotIndex}`);
const isView = B.tmp();
for (const line of recordViewTestLines(sizeType, obj, isView)) B.line(line.trimStart());
const from = B.currentLabel();
const view = B.newLabel("rv.view");
const join = B.newLabel("rv.join");
// Views are rare: the slot lookup stays out of line.
B.condBr(isView, view, join, `, !prof !{!"branch_weights", i32 1, i32 1000}`);
B.startBlock(view);
declare(`declare ptr @scr_record_view_slot(ptr, ${sizeType})`);
const viewed = B.tmp();
B.line(`${viewed} = call ptr @scr_record_view_slot(ptr ${obj}, ${sizeType} ${position})`);
B.br(join);
B.startBlock(join);
const ptr = B.tmp();
B.line(`${ptr} = phi ptr [ ${own}, %${from} ], [ ${viewed}, %${view} ]`);
return ptr;
}
+30 -6
View File
@@ -69,6 +69,7 @@ import {
disposeFieldLines,
plainRefType,
} from "./arenas.js";
import { recordViewTestLines, type RecordViews } from "./record-views.js";
import { recordMayKeepSource } from "./record-sources.js";
import { LlvmUnsupportedError } from "./unsupported.js";
@@ -151,6 +152,8 @@ export interface ShapeHost {
/** Record shapes and classes with an arena-aware constructor
* (arenas.ts). */
arenaShapes?: ReadonlySet<string>;
/** Record view targets and their sources (record-views.ts). */
recordViews?: RecordViews;
arenaClasses?: ReadonlySet<string>;
/** Every arena the program opens is closed (ArenaPlan.allClosed): the
* arena-aware constructors take no dispose list. */
@@ -451,7 +454,11 @@ export function emitShapeUnionHelpers(host: ShapeHost): string[] {
const traced = union.arms.map((arm) => host.tracedShapes.has(`record:${arm.shapeId}`));
// A freezable arm's count is the word without its frozen bit, as in
// releaseFastPath: a frozen record's last reference takes the slow path.
const frozen = union.arms.some((arm) => host.recordsById.get(arm.shapeId)?.freezable === true);
const frozen = union.arms.some(
(arm) =>
host.recordsById.get(arm.shapeId)?.freezable === true ||
host.recordViews?.targets.has(arm.shapeId) === true,
);
const notFrozen = ~recordFrozenBit(S);
const release = shapeReleaseFn(union.unionId);
if (traced.every((t) => t === traced[0]))
@@ -2291,7 +2298,9 @@ export function emitRecordShapes(
false,
chained,
segmented,
shape.freezable === true,
// A record view's word carries the frozen bit as its view mark
// (record-views.ts), so view targets release like freezable shapes.
shape.freezable === true || host.recordViews?.targets.has(shape.id) === true,
iterative
? iterativeTeardownDefinitions(
host,
@@ -2390,10 +2399,25 @@ export function emitRecordShapes(
const index = i + base;
const inline = host.inlineUnions.record(field.type);
const fieldTy = llFieldType(field.type, inline);
clone.push(
` %sp${i} = getelementptr inbounds %${struct}, ptr %src, i64 0, i32 ${index}`,
` %sv${i} = load ${fieldTy}, ptr %sp${i}`,
);
if (host.recordViews?.targets.has(shape.id) === true) {
// A view's fields live in the viewed record (record-views.ts).
host.declare(`declare ptr @scr_record_view_slot(ptr, ${host.sizeType})`);
clone.push(
` %so${i} = getelementptr inbounds %${struct}, ptr %src, i64 0, i32 ${index}`,
...recordViewTestLines(host.sizeType, "%src", `%sr${i}v`),
` br i1 %sr${i}v, label %sv${i}.view, label %sv${i}.join`,
`sv${i}.view:`,
` %sw${i} = call ptr @scr_record_view_slot(ptr %src, ${host.sizeType} ${i})`,
` br label %sv${i}.join`,
`sv${i}.join:`,
` %sp${i} = phi ptr [ %so${i}, %${i === 0 ? "entry" : `sv${i - 1}.join`} ], [ %sw${i}, %sv${i}.view ]`,
` %sv${i} = load ${fieldTy}, ptr %sp${i}`,
);
} else
clone.push(
` %sp${i} = getelementptr inbounds %${struct}, ptr %src, i64 0, i32 ${index}`,
` %sv${i} = load ${fieldTy}, ptr %sp${i}`,
);
const owned = isRefCounted(field.type) && !inline;
const stored = owned ? `%sr${i}` : `%sv${i}`;
if (owned) {
+20 -4
View File
@@ -1,4 +1,5 @@
import { emitPerKeyOrder } from "./key-orders.js";
import { recordViewFieldPtr } from "./record-views.js";
import { f64Lit } from "./common.js";
import { InternalCompilerError } from "../../errors.js";
/* Structure-walking helper EMITTERS for the LLVM backend — the .ll mirror
@@ -280,10 +281,25 @@ export class LlWalkers {
index: number,
t: IrType,
): string {
const p = B.tmp();
B.line(
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${recName}, i64 0, i32 ${index}`,
);
const base = this.host.nullableUnions.fieldBase(shapeId);
let p: string;
if (this.host.recordViews?.targets.has(shapeId) === true) {
p = recordViewFieldPtr(
B,
this.S,
mangleRecordStruct(shapeId),
recName,
index,
index - base,
(decl) => this.host.declare(decl),
this.host.rcMode === undefined,
);
} else {
p = B.tmp();
B.line(
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${recName}, i64 0, i32 ${index}`,
);
}
// An inline union field lends a private stack box (ABSENT kept).
if (this.host.inlineUnions.record(t)) return loadStructField(B, this.S, p, t, true);
const fieldTy = llFieldType(t, false);
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/record-layout-views.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/record-view-extras-release.ts"
],
"diags": []
}
+14 -1
View File
@@ -301,6 +301,9 @@ typedef struct ScrDynTypedCast {
void *(*retain)(void *);
void (*release)(void *);
bool traced;
/* A record view (scr_dyn_record_view): the view's own record-source
* entry owns the capsule, so the list neither owns nor releases it. */
bool borrowed;
struct ScrDynTypedCast *next;
} ScrDynTypedCast;
@@ -1315,7 +1318,17 @@ bool scr_dyn_typed_ref_is(
const ScrDyn *d, const char *type_key, size_t type_key_len);
bool scr_dyn_typed_ref_is_key(const ScrDyn *d, const ScrStr *type_key);
void *scr_dyn_typed_ref_unbox(const ScrDyn *d); /* +1 */
void *scr_dyn_typed_ref_prefix_view(const ScrDyn *d); /* +1 or NULL */
/* Record views (scr_json.c): a checked conversion's view of the record a
* capsule holds, or NULL. */
typedef struct {
const char *key;
size_t key_len;
const size_t *offsets;
} ScrRecordViewSource;
void *scr_record_view_slot(void *view, size_t position);
void *scr_record_view_field(void *value, size_t offset, size_t position);
void *scr_dyn_record_view(ScrDyn *d, const ScrRecordViewSource *sources, size_t count,
const char *key, size_t key_len, void *(*alloc)(void));
ScrDyn *scr_dyn_class_view_unavailable(void *ptr);
ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d); /* +1 */
ScrDyn *scr_dyn_typed_ref_peek(const ScrDyn *d); /* +1 */
+125 -75
View File
@@ -1526,7 +1526,7 @@ static void scr_dyn_dispose(ScrDyn *d, bool collected) {
while (d->v.typed_ref.casts) {
ScrDynTypedCast *cast = d->v.typed_ref.casts;
d->v.typed_ref.casts = cast->next;
if (!collected || !cast->traced) cast->release(cast->ptr);
if (!cast->borrowed && (!collected || !cast->traced)) cast->release(cast->ptr);
free(cast);
}
if (!collected || !d->v.typed_ref.traced) d->v.typed_ref.release(d->v.typed_ref.ptr);
@@ -3759,24 +3759,17 @@ typedef struct {
const char *names;
const char *lens;
size_t name_stride, len_stride, count;
/* A record that a narrower record type also references (a prefix view,
* scr_dyn_typed_ref_prefix_view) has an entry with no view and this, its
* own capsule's parameters: every capsule of the record then answers
* for its whole storage (scr_dyn_typed_ref_new), and reflective surfaces
* of the narrower type go through that capsule (scr_record_source_has).
* Owned; NULL for an ordinary source. */
struct ScrRecordPrefix *prefix;
/* A record view (scr_dyn_record_view) has an entry with no view and
* this: the capsule of the record it views (+1). Boxing the view answers
* that capsule (scr_dyn_typed_ref_new), and the view type's reflective
* surfaces go through it (scr_record_source_has). NULL for an ordinary
* source. */
ScrDyn *capsule;
/* A view's record (borrowed: the capsule holds it) and its byte offsets
* for the view type's fields. */
void *viewed;
const size_t *offsets;
} ScrRecordSource;
typedef struct ScrRecordPrefix {
void *(*retain)(void *);
void (*release)(void *);
const char *type_key;
size_t type_key_len;
ScrDyn *(*materialize)(void *);
void (*commit)(void *, const ScrDyn *);
const ScrDynTypedArrayOps *ops; /* the record's ScrDynTypedRecordOps */
bool traced, record;
} ScrRecordPrefix;
static SCR_TL ScrRecordSource *scr_record_sources;
static SCR_TL size_t scr_record_source_capacity, scr_record_source_count;
/* Deleted slots keep this marker so probe runs stay intact without moving
@@ -3817,17 +3810,20 @@ static ScrRecordSource *scr_record_source_slot(const void *rec) {
return NULL;
}
/* The record's kept source, or NULL (also for a prefix-view entry). */
/* The record's kept source, or NULL (also for a record view's entry). */
static ScrRecordSource *scr_record_source_find(const void *rec) {
ScrRecordSource *entry = scr_record_source_slot(rec);
return entry && !entry->prefix ? entry : NULL;
return entry && !entry->capsule ? entry : NULL;
}
static const ScrRecordPrefix *scr_record_prefix_find(const void *rec) {
/* The capsule a record view views (borrowed), or NULL. */
static ScrDyn *scr_record_view_capsule(const void *rec) {
ScrRecordSource *entry = scr_record_source_slot(rec);
return entry ? entry->prefix : NULL;
return entry ? entry->capsule : NULL;
}
static void scr_record_view_forget(ScrDyn *capsule, const void *view);
/* The retained view of a record with undeclared keys (borrowed), or NULL. */
ScrDyn *scr_record_source_view(const void *rec) {
ScrRecordSource *entry = scr_record_source_find(rec);
@@ -3843,11 +3839,16 @@ bool scr_record_source_has(const void *rec) {
* the dispose hook sees a consistent table. */
static void scr_record_source_drop(ScrRecordSource *found) {
ScrDyn *view = found->view;
free(found->prefix);
ScrDyn *capsule = found->capsule;
void *rec = found->rec;
*found = (ScrRecordSource){ .rec = SCR_RECORD_SOURCE_DELETED };
scr_record_source_count--;
scr_record_source_deleted++;
scr_dyn_release(view);
if (capsule) {
scr_record_view_forget(capsule, rec);
scr_dyn_release(capsule);
}
}
static void scr_record_source_disposed(void *rec) {
@@ -3927,8 +3928,8 @@ static void scr_record_source_teardown(void) {
scr_record_source_filter = NULL;
for (size_t i = 0; i < capacity; i++) {
if (table[i].rec && table[i].rec != SCR_RECORD_SOURCE_DELETED) {
free(table[i].prefix);
scr_dyn_release(table[i].view);
scr_dyn_release(table[i].capsule);
}
}
free(table);
@@ -4005,6 +4006,14 @@ void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *nam
void scr_record_source_spread(void *dst, const void *src, const char *const *names,
const size_t *lens, size_t count) {
if (!scr_record_source_count || !dst || !src) return;
ScrDyn *viewed = scr_record_view_capsule(src);
if (viewed) {
/* A record view spreads every property of the record it views. */
ScrDyn *snapshot = scr_dyn_typed_ref_peek(viewed);
if (!scr_exc_pending()) scr_record_source_note(dst, snapshot, names, lens, count);
scr_dyn_release(snapshot);
return;
}
ScrRecordSource *found = scr_record_source_find(src);
if (!found) return;
ScrRecordSource probe = { dst, NULL, (const char *)names, (const char *)lens,
@@ -4047,9 +4056,9 @@ void scr_record_source_move(void *from, void *to) {
if (!scr_record_source_count) return;
ScrRecordSource *old = scr_record_source_slot(to);
ScrRecordSource *found = scr_record_source_find(from);
if (old && old->prefix) {
/* A prefix-viewed record keeps its entry: its capsule's view holds the
* undeclared properties, and narrower names read them through it. */
if (old && old->capsule) {
/* A record view keeps its entry: the viewed capsule holds every
* property, and the view's name reads them through it. */
if (found) scr_record_source_drop(found);
return;
}
@@ -4113,7 +4122,7 @@ ScrDyn *scr_record_source_apply(ScrDyn *snapshot, const void *rec) {
}
static ScrDyn *scr_json_member(const ScrDyn *holder, const ScrStr *key);
static void scr_jb_put_record_prefix(ScrJsonBuf *b, void *rec);
static void scr_jb_put_record_view(ScrJsonBuf *b, const ScrDyn *capsule);
static bool scr_jb_try_pure_member(ScrJsonBuf *b, const char *key, size_t key_len,
const ScrStr *key_str, const ScrDyn *value, bool *first);
static bool scr_jb_put_member(ScrJsonBuf *b, const ScrStr *key, ScrDyn *value, bool *first,
@@ -4191,8 +4200,9 @@ static void scr_record_source_key_order(const ScrDyn *view, size_t *out) {
bool scr_jb_try_record_source(ScrJsonBuf *b, void *rec,
bool (*field)(ScrJsonBuf *, void *, size_t, bool)) {
if (scr_record_prefix_find(rec)) {
scr_jb_put_record_prefix(b, rec);
ScrDyn *viewed = scr_record_view_capsule(rec);
if (viewed) {
scr_jb_put_record_view(b, viewed);
return true;
}
ScrRecordSource *found = scr_record_source_find(rec);
@@ -4292,20 +4302,12 @@ static ScrDyn *scr_dyn_typed_ref_new(
const char *type_key, size_t type_key_len,
ScrDyn *(*materialize)(void *),
void (*commit)(void *, const ScrDyn *), bool traced, bool observed) {
const ScrRecordPrefix *prefix =
scr_record_source_count && type_key_len >= 7 && memcmp(type_key, "record:", 7) == 0
? scr_record_prefix_find(ptr)
: NULL;
if (prefix) {
/* A record seen through a narrower type: its capsule is its own. */
retain = prefix->retain;
release = prefix->release;
type_key = prefix->type_key;
type_key_len = prefix->type_key_len;
materialize = prefix->materialize;
commit = prefix->commit;
traced = traced || prefix->traced;
}
/* A record view boxes as the record it views. */
ScrDyn *viewed = scr_record_source_count && type_key_len >= 7 &&
memcmp(type_key, "record:", 7) == 0
? scr_record_view_capsule(ptr)
: NULL;
if (viewed) return scr_dyn_retain(viewed);
scr_typed_ref_reserve();
size_t slot = scr_typed_ref_bucket(ptr);
for (ScrDyn *value; (value = scr_typed_ref_slots[slot]); slot = (slot + 1) & (scr_typed_ref_capacity - 1)) {
@@ -4341,11 +4343,6 @@ static ScrDyn *scr_dyn_typed_ref_new(
d->v.typed_ref.record = false;
d->v.typed_ref.klass = false;
d->v.typed_ref.bag_class = false;
if (prefix) {
/* Bound here: the narrower name's bind keeps them (bind_record). */
d->v.typed_ref.array = prefix->ops;
d->v.typed_ref.record = prefix->record;
}
scr_typed_ref_slots[slot] = d; /* the empty slot that ended the probe */
scr_typed_ref_count++;
return d;
@@ -4392,29 +4389,83 @@ void *scr_dyn_typed_ref_unbox(const ScrDyn *d) {
return d->v.typed_ref.retain(d->v.typed_ref.ptr);
}
/* A record capsule's native record for a conversion into a type whose
* storage is a prefix of the record's (the compiler's prefix-view sources):
* the pointer itself (+1), so both names keep one object. The record
* remembers its own capsule parameters (ScrRecordPrefix), so every later
* capsule of it, whichever name boxes it, answers for its whole storage.
* NULL while the record keeps undeclared source properties, whose table
* is keyed by the record's own declaration; the conversion then builds
* its copy as before. */
void *scr_dyn_typed_ref_prefix_view(const ScrDyn *d) {
/* Record views (compiler record-views.ts). A checked conversion of a
* capsule of record A into record type B, when A has all of B's fields,
* answers a view (+1): a B object (allocated by `alloc`, zeroed, its rc
* word marked with the frozen bit) whose record-source entry names A
* (borrowed) and A's byte offsets for B's fields, so every field access of
* the B value reaches A. `sources` lists, for each A the compiler allows,
* its type key and offset table. The entry owns A's capsule: boxing the
* view answers that capsule, and the entry dies with the view. One view
* per capsule and B, found again through the capsule's cast list, where it
* is borrowed. NULL for any other capsule and for a frozen A, whose
* conversion keeps its copy. */
static void *scr_record_view_retain(void *view);
void *scr_dyn_record_view(ScrDyn *d, const ScrRecordViewSource *sources, size_t count,
const char *key, size_t key_len, void *(*alloc)(void)) {
if (d->kind != SCR_DYN_TYPED_REF) return NULL;
const ScrRecordViewSource *source = NULL;
for (size_t i = 0; i < count && !source; i++)
if (sources[i].key_len == d->v.typed_ref.type_key_len &&
memcmp(sources[i].key, d->v.typed_ref.type_key, sources[i].key_len) == 0)
source = &sources[i];
void *record = d->v.typed_ref.ptr;
if (!scr_record_prefix_find(record)) {
if (scr_record_source_find(record) || !d->v.typed_ref.record) return NULL;
ScrRecordPrefix *prefix = malloc(sizeof *prefix);
if (!prefix) scr_json_oom();
*prefix = (ScrRecordPrefix){
d->v.typed_ref.retain, d->v.typed_ref.release, d->v.typed_ref.type_key,
d->v.typed_ref.type_key_len, d->v.typed_ref.materialize, d->v.typed_ref.commit,
d->v.typed_ref.array, d->v.typed_ref.traced, d->v.typed_ref.record,
};
ScrRecordSource entry = { .rec = record, .prefix = prefix };
scr_record_source_put(&entry);
if (!source || scr_rc_frozen(record)) return NULL;
for (ScrDynTypedCast *cast = d->v.typed_ref.casts; cast; cast = cast->next)
if (cast->borrowed && cast->type_key_len == key_len && memcmp(cast->type_key, key, key_len) == 0)
return cast->retain(cast->ptr);
void *view = alloc();
/* The frozen bit marks a view: view target types are never freezable,
* and their emitted releases ignore the bit (record-views.ts). */
*(size_t *)view |= SCR_RC_FROZEN;
ScrRecordSource entry = { .rec = view, .capsule = scr_dyn_retain(d), .viewed = record,
.offsets = source->offsets };
scr_record_source_put(&entry);
ScrDynTypedCast *cast = malloc(sizeof *cast);
if (!cast) scr_trap("scriptc: out of memory\n");
*cast = (ScrDynTypedCast){ .type_key = key, .type_key_len = key_len, .ptr = view,
.retain = scr_record_view_retain, .release = NULL,
.borrowed = true, .next = d->v.typed_ref.casts };
d->v.typed_ref.casts = cast;
return view;
}
/* The slot of field `position` of a view (emitted view guards, cold). */
void *scr_record_view_slot(void *view, size_t position) {
ScrRecordSource *entry = scr_record_source_slot(view);
return (char *)entry->viewed + entry->offsets[position];
}
/* The address of field `position` of a value of a view target type, whose
* own slot is at byte `offset` (the unoptimized builds' guard): the viewed
* record's slot for a view, whose word is the frozen bit plus a count. */
void *scr_record_view_field(void *value, size_t offset, size_t position) {
size_t word = *(const size_t *)value;
#if SIZE_MAX > UINT32_MAX
bool view = word - SCR_RC_FROZEN < ((size_t)1 << 32);
#else
bool view = word - SCR_RC_FROZEN < SCR_RC_FROZEN;
#endif
return view ? scr_record_view_slot(value, position) : (char *)value + offset;
}
/* A view itself counts like any record (its target's retain). */
static void *scr_record_view_retain(void *view) {
if (!scr_rc_immortal(view)) SCR_RC_COUNT(view)++;
return view;
}
/* A view left the table: its capsule's cast list stops naming it. */
static void scr_record_view_forget(ScrDyn *capsule, const void *view) {
for (ScrDynTypedCast **at = &capsule->v.typed_ref.casts; *at; at = &(*at)->next) {
if ((*at)->borrowed && (*at)->ptr == view) {
ScrDynTypedCast *cast = *at;
*at = cast->next;
free(cast);
return;
}
}
return d->v.typed_ref.retain(record);
}
@@ -5002,7 +5053,7 @@ void *scr_dyn_typed_ref_cached_cast(
const ScrDyn *d, const char *type_key, size_t type_key_len) {
for (ScrDynTypedCast *cast = d->v.typed_ref.casts; cast;
cast = cast->next) {
if (cast->type_key_len == type_key_len &&
if (!cast->borrowed && cast->type_key_len == type_key_len &&
memcmp(cast->type_key, type_key, type_key_len) == 0) {
return cast->retain(cast->ptr);
}
@@ -5022,6 +5073,7 @@ void scr_dyn_typed_ref_cache_cast(
cast->retain = retain;
cast->release = release;
cast->traced = traced;
cast->borrowed = false;
cast->next = d->v.typed_ref.casts;
d->v.typed_ref.casts = cast;
}
@@ -14859,11 +14911,9 @@ ScrStr *scr_util_format_j_text(ScrStr *text) {
/* The gap of a compact walk; the walks read only its length. */
static const ScrStr scr_json_compact_gap = { .rc = SIZE_MAX, .len = 0 };
/* JSON for a prefix-viewed record: its own capsule's. */
static void scr_jb_put_record_prefix(ScrJsonBuf *b, void *rec) {
ScrDyn *capsule = scr_dyn_new_typed_ref(rec, NULL, NULL, "record:", 7, NULL, NULL);
/* JSON for a record view: the viewed capsule's. */
static void scr_jb_put_record_view(ScrJsonBuf *b, const ScrDyn *capsule) {
scr_json_replaced_write(b, capsule, scr_dyn_undefined(), NULL, &scr_json_compact_gap, b->seen_len);
scr_dyn_release(capsule);
}
#define SCR_JSON_PURE_DEPTH 32
+1 -1
View File
@@ -871,7 +871,7 @@ static void scr_rgn_walk_dyn(ScrRgnWalk *w, ScrDyn *d) {
scr_region_slot(w, d->v.typed_ref.ptr, d->v.typed_ref.release);
scr_rgn_enter(w, d->v.typed_ref.materialized, SCR_RGN_W_DYN, NULL);
for (ScrDynTypedCast *cast = d->v.typed_ref.casts; cast; cast = cast->next)
scr_region_slot(w, cast->ptr, cast->release);
if (!cast->borrowed) scr_region_slot(w, cast->ptr, cast->release);
break;
case SCR_DYN_PROXY:
scr_rgn_enter(w, d->v.proxy.target, SCR_RGN_W_DYN, NULL);
+26
View File
@@ -0,0 +1,26 @@
// A native record converted from an untyped value into another record type
// that names some of its fields, in any order, is the same object: writes
// through either name show through the other, identity holds, and the
// untyped views, JSON and spreads of the narrower name see every field.
type Wide = { a: number; b: string; c: boolean; d: number[] };
type AC = { a: number; c: boolean };
type DB = { d: number[]; b: string };
const wide: Wide = { a: 1, b: "x", c: true, d: [1] };
const o: unknown = wide;
const v = o as AC;
v.c = false;
console.log((o as Wide).c, wide.c, o === v, (v as unknown) === o);
wide.a = 5;
console.log(v.a, JSON.stringify(v), Object.keys(v).join(","));
const w = o as DB;
w.d.push(2);
w.b = "y";
w.d = [7, 8];
console.log(wide.b, wide.d.length, JSON.stringify(o), w === (o as DB), v === (o as AC));
const v2 = { ...v };
console.log(JSON.stringify(v2));
function bump(x: AC): void { x.a += 1; }
bump(v);
console.log(wide.a, v.a);
const list: AC[] = [v, { a: 9, c: true }];
console.log(list.map((x) => x.a).join(","), JSON.stringify(list));
@@ -0,0 +1,44 @@
// Views of records whose other fields hold strings, arrays and records: every view and every record is released exactly once, whichever
// name drops the last reference (the sanitized lane checks for leaks).
type Node = { id: number; label: string; children: Node[]; meta: { tag: string }; seen: { key: string; count: number }[] };
type Head = { id: number; meta: { tag: string } };
type Tail = { children: Node[]; label: string };
function make(id: number): Node {
return { id, label: "n" + id, children: [], meta: { tag: "t" + id }, seen: [{ key: "k", count: id }] };
}
function asHead(value: unknown): Head {
return value as Head;
}
function asTail(value: unknown): Tail {
return value as Tail;
}
let total = 0;
for (let round = 0; round < 200; round++) {
const node = make(round);
node.children.push(make(round + 1000));
const head = asHead(node);
const tail = asTail(node);
head.meta = { tag: "h" + round };
tail.label = tail.label + "!";
tail.children.push(make(round + 2000));
total += head.id + tail.children.length + node.seen[0]!.count;
if (round === 199) console.log(JSON.stringify(head), node.label, node.meta.tag, tail.children.length);
}
console.log(total);
// The last reference is the view's.
let kept: Head | undefined;
for (let round = 0; round < 100; round++) kept = asHead(make(round));
if (kept) console.log(kept.id, kept.meta.tag, JSON.stringify(kept));
kept = undefined;
// The last reference is the record's.
const records: Node[] = [];
for (let round = 0; round < 100; round++) {
const node = make(round);
asTail(node).label = "v" + round;
records.push(node);
}
console.log(records.map((node) => node.label).slice(0, 3).join(","), records.length);
@@ -0,0 +1 @@
{ "plain": 3000, "san": 3600 }
@@ -0,0 +1 @@
{ "plain": 3000, "san": 3600 }