mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Forward pointer-union wraps of call arguments to the callee's parameter proof (#2048)
This commit is contained in:
@@ -760,3 +760,154 @@ test("a parameter stored as its last use takes the owned convention", () => {
|
||||
const passed = ctor([{ kind: "exprStmt", expr: call("use", [ref("item", cell)]), loc }]);
|
||||
expect(analyze(passed).borrowed.get("%Box.constructor")).toEqual(new Set([0, 1]));
|
||||
});
|
||||
|
||||
test("a wrap into a pointer union forwards the wrapped binding to the callee's parameter", () => {
|
||||
const wrap = (id: string): IrExpr => ({
|
||||
kind: "unionWrap",
|
||||
unionId: "optional",
|
||||
tag: 0,
|
||||
value: ref(id, record),
|
||||
type: optional,
|
||||
loc,
|
||||
});
|
||||
const leaf = helper("leaf");
|
||||
const outer = helper("outer");
|
||||
outer.params[0]!.type = outer.locals[0]!.type = record;
|
||||
outer.locals.push(local("cell", record));
|
||||
outer.body = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "cell",
|
||||
init: { kind: "recordLit", fields: [], type: record, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "exprStmt", expr: call("leaf", [wrap("value")]), loc },
|
||||
ret(call("leaf", [wrap("cell")])),
|
||||
];
|
||||
const functions = new Map([leaf, outer].map((fn) => [fn.name, fn]));
|
||||
const analyzeWith = (pointer: boolean) =>
|
||||
analyzeCallLifetimes(
|
||||
functions,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
() => pointer,
|
||||
);
|
||||
// A boxed union builds a new box around the payload: a whole-value use.
|
||||
expect(analyzeWith(false).parameters.get("outer")).toBeUndefined();
|
||||
expect(analyzeWith(false).locals.get("outer")).toEqual(new Set());
|
||||
// A pointer union passes the payload pointer itself.
|
||||
expect(analyzeWith(true).parameters.get("outer")).toEqual(new Set([0]));
|
||||
expect(analyzeWith(true).locals.get("outer")).toEqual(new Set(["cell"]));
|
||||
// A callee that keeps its parameter keeps the wrapped binding too.
|
||||
leaf.body.push(ret(ref("value")));
|
||||
expect(analyzeWith(true).parameters.get("outer")).toBeUndefined();
|
||||
expect(analyzeWith(true).locals.get("outer")).toEqual(new Set());
|
||||
});
|
||||
|
||||
test("a pointer-wrapped binding needs a callee that keeps the narrowed payload in place", () => {
|
||||
const wrapCell: IrExpr = {
|
||||
kind: "unionWrap",
|
||||
unionId: "optional",
|
||||
tag: 0,
|
||||
value: ref("cell", record),
|
||||
type: optional,
|
||||
loc,
|
||||
};
|
||||
const outer = helper("outer", []);
|
||||
outer.locals.push(local("cell", record));
|
||||
outer.body = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "cell",
|
||||
init: { kind: "recordLit", fields: [], type: record, loc },
|
||||
loc,
|
||||
},
|
||||
ret(call("leaf", [wrapCell])),
|
||||
];
|
||||
const read = (value: IrExpr): IrExpr => ({
|
||||
kind: "recordGet",
|
||||
obj: value,
|
||||
shapeId: "cell",
|
||||
field: "x",
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const keeps = (leaf: IrFunction, ...more: IrFunction[]): boolean => {
|
||||
const result = analyzeCallLifetimes(
|
||||
new Map([outer, leaf, ...more].map((fn) => [fn.name, fn])),
|
||||
(className) => className === "Node",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
() => true,
|
||||
);
|
||||
return result.locals.get("outer")?.has("cell") !== true;
|
||||
};
|
||||
const leafWith = (body: IrStmt[]): IrFunction => ({ ...helper("leaf"), body });
|
||||
// Tag tests and field reads of the narrowed payload leave it in place.
|
||||
expect(keeps(leafWith([{ kind: "exprStmt", expr: tag("value"), loc }]))).toBe(false);
|
||||
expect(keeps(leafWith([ret(read(narrow("value")))]))).toBe(false);
|
||||
// So do field reads through a checked narrowing, which throws on the
|
||||
// other arm (`value.x` of a `T | undefined` parameter).
|
||||
const checked: IrExpr = {
|
||||
kind: "ternary",
|
||||
cond: tag("value"),
|
||||
then: { kind: "libCall", fn: "error.nodeThrow", args: [], type: record, loc },
|
||||
else_: narrow("value"),
|
||||
type: record,
|
||||
loc,
|
||||
};
|
||||
expect(keeps(leafWith([ret(read(checked))]))).toBe(false);
|
||||
// Returning it keeps the payload.
|
||||
const returnsChecked = leafWith([ret(checked)]);
|
||||
returnsChecked.returnType = record;
|
||||
expect(keeps(returnsChecked)).toBe(true);
|
||||
// Returning the narrowed payload, or a narrowed nullish read, keeps it,
|
||||
// although the union itself is only projected.
|
||||
const returned = leafWith([ret(narrow("value"))]);
|
||||
returned.returnType = record;
|
||||
expect(analyze(returned).parameters.get("leaf")).toEqual(new Set([0]));
|
||||
expect(keeps(returned)).toBe(true);
|
||||
const defaulted = leafWith([
|
||||
ret({ kind: "nullish", left: ref("value"), right: ref("other", record), type: record, loc }),
|
||||
]);
|
||||
defaulted.locals.push(local("other", record));
|
||||
expect(keeps(defaulted)).toBe(true);
|
||||
// So does a nullable-pointer field store of the union.
|
||||
const self: IrType = { kind: "object", className: "Node" };
|
||||
const stored = leafWith([
|
||||
{
|
||||
kind: "fieldSet",
|
||||
obj: ref("this", self),
|
||||
className: "Node",
|
||||
field: "next",
|
||||
value: ref("value"),
|
||||
loc,
|
||||
},
|
||||
]);
|
||||
stored.params = [{ localId: "value", name: "value", type: optional }];
|
||||
stored.locals.push(local("this", self));
|
||||
expect(keeps(stored)).toBe(true);
|
||||
// A narrowed payload passed on, or the union passed on, is in place when
|
||||
// the next callee keeps it in place.
|
||||
const inner = (body: IrStmt[], type: IrType = record): IrFunction => {
|
||||
const fn = helper("inner");
|
||||
fn.params[0]!.type = fn.locals[0]!.type = type;
|
||||
fn.body = body;
|
||||
return fn;
|
||||
};
|
||||
const passNarrowed = leafWith([ret(call("inner", [narrow("value")]))]);
|
||||
expect(keeps(passNarrowed, inner([ret(read(ref("value", record)))]))).toBe(false);
|
||||
const returnsRecord = inner([ret(ref("value", record))]);
|
||||
returnsRecord.returnType = record;
|
||||
expect(keeps(passNarrowed, returnsRecord)).toBe(true);
|
||||
const passUnion = leafWith([ret(call("inner", [ref("value")]))]);
|
||||
expect(keeps(passUnion, inner([ret(read(narrow("value")))], optional))).toBe(false);
|
||||
const innerReturned = inner([ret(narrow("value"))], optional);
|
||||
innerReturned.returnType = record;
|
||||
expect(keeps(passUnion, innerReturned)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -14,6 +14,11 @@ import { storedLastUseRead } from "./local-extents.js";
|
||||
interface ForwardedUse {
|
||||
callee: string;
|
||||
index: number;
|
||||
/** The binding reached the call inside a nullable-pointer union
|
||||
* (PointerWrapTest): the callee's parameter holds the binding itself as
|
||||
* its payload, so the callee must keep the payload, not only the union,
|
||||
* in place. */
|
||||
wrapped: boolean;
|
||||
}
|
||||
|
||||
interface Uses {
|
||||
@@ -21,6 +26,13 @@ interface Uses {
|
||||
written: Set<string>;
|
||||
declarations: Map<string, number>;
|
||||
forwards: Map<string, ForwardedUse[]>;
|
||||
/** Union bindings whose payload a use may keep: a narrowed read that is
|
||||
* not projected or passed on, a narrowed nullish read, a store into a
|
||||
* nullable-pointer field. The union itself can still be only projected. */
|
||||
payloadKept: Set<string>;
|
||||
/** Narrowed payloads of union bindings passed directly as call arguments:
|
||||
* the payload stays in place if the callee's parameter is proved. */
|
||||
payloadForwards: Map<string, ForwardedUse[]>;
|
||||
}
|
||||
|
||||
interface Parameter {
|
||||
@@ -99,19 +111,55 @@ const NO_INLINE_FIELDS: InlineFieldTest = { record: () => false, classField: ()
|
||||
* reads the record and keeps no reference to it. */
|
||||
export type RecordDynCopyTest = (shapeId: string) => boolean;
|
||||
|
||||
/** Wraps into a nullable-pointer union whose value is the payload pointer
|
||||
* itself (LlEmitter.borrowableNullableWrap): the wrapped binding reaches the
|
||||
* callee's parameter unchanged. */
|
||||
export type PointerWrapTest = (wrap: IrExpr & { kind: "unionWrap" }) => boolean;
|
||||
|
||||
function collectUses(
|
||||
fn: IrFunction,
|
||||
nullableField: NullableFieldTest,
|
||||
registerReturn: boolean,
|
||||
inlineField: InlineFieldTest,
|
||||
recordDynCopy: RecordDynCopyTest,
|
||||
pointerWrap: PointerWrapTest,
|
||||
): Uses {
|
||||
const uses: Uses = {
|
||||
invalid: new Set(),
|
||||
written: new Set(),
|
||||
declarations: new Map(),
|
||||
forwards: new Map(),
|
||||
payloadKept: new Set(),
|
||||
payloadForwards: new Map(),
|
||||
};
|
||||
const forward = (
|
||||
table: Map<string, ForwardedUse[]>,
|
||||
localId: string,
|
||||
use: ForwardedUse,
|
||||
): void => {
|
||||
let forwards = table.get(localId);
|
||||
if (!forwards) table.set(localId, (forwards = []));
|
||||
forwards.push(use);
|
||||
};
|
||||
/** The union binding `x` whose payload `value` is: `unionNarrow(x)`, or a
|
||||
* checked narrowing `unionIsTag(x) ? <libCall> : unionNarrow(x)` (either
|
||||
* order), whose other branch is visited as any use. */
|
||||
function narrowedBinding(value: IrExpr): string | null {
|
||||
if (value.kind === "unionNarrow")
|
||||
return value.value.kind === "varRef" ? value.value.localId : null;
|
||||
if (
|
||||
value.kind !== "ternary" ||
|
||||
value.cond.kind !== "unionIsTag" ||
|
||||
value.cond.value.kind !== "varRef"
|
||||
)
|
||||
return null;
|
||||
const id = value.cond.value.localId;
|
||||
if (value.else_.kind === "libCall" && narrowedBinding(value.then) === id)
|
||||
return expr(value.else_) ? id : null;
|
||||
if (value.then.kind === "libCall" && narrowedBinding(value.else_) === id)
|
||||
return expr(value.then) ? id : null;
|
||||
return null;
|
||||
}
|
||||
const aliases: { alias: string; source: string }[] = [];
|
||||
const stringInput = (value: IrExpr): boolean => {
|
||||
if (value.kind === "varRef" && value.type.kind === "string") return true;
|
||||
@@ -145,6 +193,12 @@ function collectUses(
|
||||
if (borrowsMapReadInputs(node)) return expr(node.receiver) && node.args.every(stringInput);
|
||||
break;
|
||||
case "unionNarrow":
|
||||
// The payload may escape; the union itself does not.
|
||||
if (node.value.kind === "varRef") {
|
||||
uses.payloadKept.add(node.value.localId);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case "unionIsTag":
|
||||
if (node.value.kind === "varRef") return true;
|
||||
break;
|
||||
@@ -161,12 +215,16 @@ function collectUses(
|
||||
node.left.kind === "varRef" &&
|
||||
node.left.type.kind === "union" &&
|
||||
node.type.kind !== "union"
|
||||
)
|
||||
) {
|
||||
uses.payloadKept.add(node.left.localId);
|
||||
return expr(node.right);
|
||||
}
|
||||
break;
|
||||
case "fieldGet":
|
||||
case "recordGet":
|
||||
if (node.obj.kind === "varRef") return true;
|
||||
// A field read of a binding, or of a union binding's narrowed
|
||||
// payload, keeps neither.
|
||||
if (node.obj.kind === "varRef" || narrowedBinding(node.obj) !== null) return true;
|
||||
break;
|
||||
case "dynFrom":
|
||||
// A copied record converts field by field; a live capsule keeps it.
|
||||
@@ -200,13 +258,19 @@ function collectUses(
|
||||
return node.args.every(expr);
|
||||
case "call":
|
||||
node.args.forEach((arg, index) => {
|
||||
if (arg.kind !== "varRef") {
|
||||
const narrowed = narrowedBinding(arg);
|
||||
if (narrowed !== null) {
|
||||
forward(uses.payloadForwards, narrowed, { callee: node.callee, index, wrapped: false });
|
||||
return;
|
||||
}
|
||||
const wrapped =
|
||||
arg.kind === "unionWrap" && arg.value.kind === "varRef" && pointerWrap(arg);
|
||||
const value = wrapped ? arg.value : arg;
|
||||
if (value.kind !== "varRef") {
|
||||
expr(arg);
|
||||
return;
|
||||
}
|
||||
let forwards = uses.forwards.get(arg.localId);
|
||||
if (!forwards) uses.forwards.set(arg.localId, (forwards = []));
|
||||
forwards.push({ callee: node.callee, index });
|
||||
forward(uses.forwards, value.localId, { callee: node.callee, index, wrapped });
|
||||
});
|
||||
return true;
|
||||
case "assignExpr":
|
||||
@@ -250,8 +314,11 @@ function collectUses(
|
||||
node.value.kind === "varRef" &&
|
||||
node.value.type.kind === "union" &&
|
||||
nullableField(node.className, node.field)
|
||||
)
|
||||
) {
|
||||
// The field retains the payload.
|
||||
uses.payloadKept.add(node.value.localId);
|
||||
return expr(node.obj);
|
||||
}
|
||||
const stored = fieldStore(
|
||||
node.obj,
|
||||
node.value,
|
||||
@@ -345,10 +412,14 @@ export function analyzeCallLifetimes(
|
||||
registerReturns: ReadonlySet<string> = new Set(),
|
||||
inlineField: InlineFieldTest = NO_INLINE_FIELDS,
|
||||
recordDynCopy: RecordDynCopyTest = () => false,
|
||||
pointerWrap: PointerWrapTest = () => false,
|
||||
): CallLifetimes {
|
||||
const usesByFunction = new Map<string, Uses>();
|
||||
const lazyCaptures = new LazyCaptures(functions);
|
||||
const nodes = new Map<string, Parameter[]>();
|
||||
/** Union parameters that keep their payload in place too, which a
|
||||
* pointer-wrapped argument needs (ForwardedUse.wrapped). */
|
||||
const payloadNodes = new Map<string, (Parameter | undefined)[]>();
|
||||
const unsafe: Parameter[] = [];
|
||||
const consumed: { fn: string; index: number }[] = [];
|
||||
const result: CallLifetimes = {
|
||||
@@ -361,7 +432,7 @@ export function analyzeCallLifetimes(
|
||||
};
|
||||
for (const fn of functions.values()) {
|
||||
if (fn.generator !== undefined && fn.classCaptures === undefined) {
|
||||
const uses = collectUses(fn, nullableField, false, inlineField, recordDynCopy);
|
||||
const uses = collectUses(fn, nullableField, false, inlineField, recordDynCopy, pointerWrap);
|
||||
const locals = new Map(fn.locals.map((local) => [local.id, local]));
|
||||
const stable = new Set<string>();
|
||||
for (const param of fn.params) {
|
||||
@@ -386,6 +457,7 @@ export function analyzeCallLifetimes(
|
||||
registerReturns.has(fn.name),
|
||||
inlineField,
|
||||
recordDynCopy,
|
||||
pointerWrap,
|
||||
);
|
||||
usesByFunction.set(fn.name, uses);
|
||||
// Class-capturing bodies get only local facts (computed below from
|
||||
@@ -460,23 +532,50 @@ export function analyzeCallLifetimes(
|
||||
return node;
|
||||
}),
|
||||
);
|
||||
payloadNodes.set(
|
||||
fn.name,
|
||||
fn.params.map((param) => {
|
||||
if (param.type.kind !== "union") return undefined;
|
||||
const node: Parameter = { safe: !uses.payloadKept.has(param.localId), callers: new Set() };
|
||||
if (!node.safe) unsafe.push(node);
|
||||
return node;
|
||||
}),
|
||||
);
|
||||
}
|
||||
/** The fact a forwarded use needs from the callee's parameter. */
|
||||
const target = (use: ForwardedUse): Parameter | undefined =>
|
||||
use.wrapped ? payloadNodes.get(use.callee)?.[use.index] : nodes.get(use.callee)?.[use.index];
|
||||
for (const fn of functions.values()) {
|
||||
const params = nodes.get(fn.name);
|
||||
const uses = usesByFunction.get(fn.name);
|
||||
if (!params || !uses) continue;
|
||||
fn.params.forEach((param, index) => {
|
||||
const node = params[index]!;
|
||||
const depend = (node: Parameter, callees: (Parameter | undefined)[]): void => {
|
||||
if (!node.safe) return;
|
||||
for (const use of uses.forwards.get(param.localId) ?? []) {
|
||||
const callee = nodes.get(use.callee)?.[use.index];
|
||||
for (const callee of callees) {
|
||||
if (!callee) {
|
||||
node.safe = false;
|
||||
unsafe.push(node);
|
||||
break;
|
||||
return;
|
||||
}
|
||||
callee.callers.add(node);
|
||||
}
|
||||
};
|
||||
fn.params.forEach((param, index) => {
|
||||
const node = params[index]!;
|
||||
const forwards = uses.forwards.get(param.localId) ?? [];
|
||||
depend(node, forwards.map(target));
|
||||
const payload = payloadNodes.get(fn.name)?.[index];
|
||||
if (!payload) return;
|
||||
// The payload stays in place when the union does, when every callee
|
||||
// the union reaches keeps its payload too, and when every callee its
|
||||
// narrowed payload reaches proves that parameter.
|
||||
depend(payload, [
|
||||
node,
|
||||
...forwards.map((use) => payloadNodes.get(use.callee)?.[use.index]),
|
||||
...(uses.payloadForwards.get(param.localId) ?? []).map(
|
||||
(use) => nodes.get(use.callee)?.[use.index],
|
||||
),
|
||||
]);
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < unsafe.length; i++) {
|
||||
@@ -508,7 +607,7 @@ export function analyzeCallLifetimes(
|
||||
)
|
||||
continue;
|
||||
const forwards = uses.forwards.get(local.id) ?? [];
|
||||
if (forwards.every((use) => result.parameters.get(use.callee)?.has(use.index) === true)) {
|
||||
if (forwards.every((use) => target(use)?.safe === true)) {
|
||||
projected.add(local.id);
|
||||
if (eligible(local) && !uses.written.has(local.id)) safe.add(local.id);
|
||||
}
|
||||
|
||||
@@ -1781,6 +1781,7 @@ export class LlEmitter {
|
||||
const shape = this.recordsById.get(shapeId);
|
||||
return shape !== undefined && !recordDynKeepsIdentity(shape);
|
||||
},
|
||||
(wrap) => this.borrowableNullableWrap(wrap) === "ref",
|
||||
);
|
||||
this.lentStringParameters =
|
||||
this.debug === null ? findLentStringParameters(this.fnByName, this.callLifetimes) : new Map();
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7660-pointer-union-forwarded-arguments.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7663-pointer-union-stored-payload.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// Records and class instances passed to `T | undefined` and `T | null`
|
||||
// parameters. The callee receives the object pointer itself, so a binding
|
||||
// that the callee only reads keeps its frame storage; callees that keep,
|
||||
// return or compare the argument still see one shared object.
|
||||
interface Vec {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
interface Tagged {
|
||||
name: string;
|
||||
at: Vec;
|
||||
}
|
||||
function vec(x: number, y: number): Vec {
|
||||
return { x, y };
|
||||
}
|
||||
function len(v: Vec | undefined): number {
|
||||
return v === undefined ? -1 : Math.sqrt(v.x * v.x + v.y * v.y);
|
||||
}
|
||||
function sum(a: Vec | undefined, b: Vec): Vec {
|
||||
return a === undefined ? b : { x: a.x + b.x, y: a.y + b.y };
|
||||
}
|
||||
function label(t: Tagged | undefined): string {
|
||||
return t === undefined ? "none" : t.name + "@" + t.at.x + "," + t.at.y;
|
||||
}
|
||||
function depth(v: Vec | undefined, n: number): number {
|
||||
if (v === undefined || n === 0) return 0;
|
||||
return v.x + depth(v, n - 1);
|
||||
}
|
||||
|
||||
const kept: (Vec | undefined)[] = [];
|
||||
function keep(v: Vec | undefined): number {
|
||||
kept.push(v);
|
||||
return kept.length;
|
||||
}
|
||||
function same(a: Vec | undefined, b: Vec): boolean {
|
||||
return a === b;
|
||||
}
|
||||
function orNull(v: Vec | undefined): Vec | undefined {
|
||||
return v !== undefined && v.x < 0 ? undefined : v;
|
||||
}
|
||||
|
||||
class Cell {
|
||||
value: number;
|
||||
next: Cell | null;
|
||||
constructor(value: number, next: Cell | null) {
|
||||
this.value = value;
|
||||
this.next = next;
|
||||
}
|
||||
}
|
||||
function cellTotal(c: Cell | null): number {
|
||||
let total = 0;
|
||||
for (let at = c; at !== null; at = at.next) total += at.value;
|
||||
return total;
|
||||
}
|
||||
function fail(v: Vec | undefined): number {
|
||||
if (v !== undefined && v.x > 100) throw new Error("too far: " + v.x);
|
||||
return v === undefined ? 0 : v.x;
|
||||
}
|
||||
|
||||
function projections(): void {
|
||||
const a = vec(3, 4);
|
||||
const b = sum(a, vec(1, 1));
|
||||
console.log("len", len(a), len(b), len(undefined));
|
||||
const c = sum(sum(a, b), a);
|
||||
console.log("sum", c.x, c.y, sum(undefined, a).x);
|
||||
console.log("depth", depth(a, 5), depth(undefined, 3));
|
||||
let total = 0;
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const p = vec(i, i + 1);
|
||||
total += len(p) + len(sum(p, a));
|
||||
}
|
||||
console.log("loop", total.toFixed(6));
|
||||
}
|
||||
|
||||
function mutated(): void {
|
||||
const p = vec(1, 2);
|
||||
console.log("before", len(p));
|
||||
p.x = 6;
|
||||
p.y = 8;
|
||||
console.log("after", len(p));
|
||||
}
|
||||
|
||||
function strings(): void {
|
||||
const prefix = ["a", "b", "c"].join("-");
|
||||
const t: Tagged = { name: prefix + "!", at: vec(2, 5) };
|
||||
console.log("label", label(t), label(undefined));
|
||||
const u: Tagged = { name: label(t), at: t.at };
|
||||
console.log("nested", label(u));
|
||||
}
|
||||
|
||||
function escapes(): void {
|
||||
const a = vec(7, 8);
|
||||
const count = keep(a);
|
||||
const b = vec(9, 10);
|
||||
console.log("kept", count, kept[0] === a, kept[0]!.x, keep(b), kept[1]!.y);
|
||||
a.x = 70;
|
||||
console.log("shared", kept[0]!.x);
|
||||
const c = vec(1, 1);
|
||||
console.log("same", same(c, c), same(c, vec(1, 1)), same(undefined, c));
|
||||
const d = vec(-1, 2);
|
||||
const e = vec(4, 5);
|
||||
console.log("returned", orNull(d), orNull(e) === e, orNull(e)!.y);
|
||||
}
|
||||
|
||||
function classes(): void {
|
||||
const tail = new Cell(3, null);
|
||||
const list = new Cell(1, new Cell(2, tail));
|
||||
console.log("cells", cellTotal(list), cellTotal(tail), cellTotal(null));
|
||||
}
|
||||
|
||||
function throwing(): void {
|
||||
const near = vec(5, 0);
|
||||
const far = vec(500, 0);
|
||||
try {
|
||||
console.log("near", fail(near));
|
||||
console.log("far", fail(far));
|
||||
} catch (e) {
|
||||
console.log("caught", (e as Error).message, near.x, far.x);
|
||||
}
|
||||
}
|
||||
|
||||
projections();
|
||||
mutated();
|
||||
strings();
|
||||
escapes();
|
||||
classes();
|
||||
throwing();
|
||||
@@ -0,0 +1,83 @@
|
||||
// Records passed to `T | undefined` parameters whose callee keeps the
|
||||
// narrowed record: pushed into a global array (also through a non-null
|
||||
// assertion), kept through `??`, stored in a nullable class field or a record
|
||||
// field, or passed on to a callee that keeps it. Each record must outlive its
|
||||
// caller's frame. A callee that only reads the narrowed record lets it stay
|
||||
// where it is.
|
||||
interface Tagged {
|
||||
id: number;
|
||||
tags: string[];
|
||||
}
|
||||
interface Slot {
|
||||
held: Tagged | undefined;
|
||||
}
|
||||
const kept: Tagged[] = [];
|
||||
const slot: Slot = { held: undefined };
|
||||
function pushNarrowed(t: Tagged | undefined): void {
|
||||
if (t !== undefined) kept.push(t);
|
||||
}
|
||||
function pushAsserted(t: Tagged | undefined): void {
|
||||
kept.push(t!);
|
||||
}
|
||||
function keepDefaulted(t: Tagged | undefined): void {
|
||||
kept.push(t ?? { id: -1, tags: [] });
|
||||
}
|
||||
function storeInSlot(t: Tagged | undefined): void {
|
||||
if (t !== undefined) slot.held = t;
|
||||
}
|
||||
function keepRecord(t: Tagged): void {
|
||||
kept.push(t);
|
||||
}
|
||||
function passOn(t: Tagged | undefined): void {
|
||||
if (t !== undefined) keepRecord(t);
|
||||
}
|
||||
function readOnly(t: Tagged | undefined): number {
|
||||
return t === undefined ? 0 : t.id + t.tags.length;
|
||||
}
|
||||
class Holder {
|
||||
last: Tagged | undefined = undefined;
|
||||
take(t: Tagged | undefined): void {
|
||||
this.last = t;
|
||||
}
|
||||
}
|
||||
const holder = new Holder();
|
||||
function fill(round: number): number {
|
||||
let read = 0;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const a: Tagged = { id: round * 10 + i, tags: ["a", String(i)] };
|
||||
pushNarrowed(a);
|
||||
const b: Tagged = { id: round * 10 + i + 100, tags: ["b"] };
|
||||
keepDefaulted(b);
|
||||
const c: Tagged = { id: round * 10 + i + 200, tags: ["c"] };
|
||||
storeInSlot(c);
|
||||
const d: Tagged = { id: round * 10 + i + 300, tags: ["d", "d"] };
|
||||
holder.take(d);
|
||||
const e: Tagged = { id: round * 10 + i + 400, tags: [] };
|
||||
passOn(e);
|
||||
const g: Tagged = { id: round * 10 + i + 600, tags: ["g", "g", "g"] };
|
||||
pushAsserted(g);
|
||||
const f: Tagged = { id: round * 10 + i + 500, tags: ["f"] };
|
||||
read += readOnly(f);
|
||||
}
|
||||
return read;
|
||||
}
|
||||
// Reuses the stack the callers above ran on, and allocates collectable
|
||||
// records, before the kept ones are read.
|
||||
function churn(n: number): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const t: Tagged = { id: i, tags: [String(i), String(i + 1)] };
|
||||
total += t.tags.length + readOnly(t);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
let reads = 0;
|
||||
for (let round = 0; round < 4; round++) {
|
||||
reads += fill(round);
|
||||
reads += churn(200);
|
||||
}
|
||||
console.log("reads", reads);
|
||||
console.log("kept", kept.length);
|
||||
for (const t of kept) console.log(t.id, t.tags.join("+"));
|
||||
console.log("slot", slot.held === undefined ? "none" : slot.held.id, slot.held?.tags.join("+"));
|
||||
console.log("holder", holder.last === undefined ? "none" : holder.last.id, holder.last?.tags.join("+"));
|
||||
Reference in New Issue
Block a user