mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Answer cross-layout record casts with views of the original record (#2419)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/record-layout-views.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/record-view-extras-release.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user