mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
Borrow final reference fields without a retain
A reference field that only its class family's constructors write, through their own `this`, never changes outside them. While its object is held, the field's value is held too, so `this.scanner.scan()` and `const units = this.units` need no retain and release (and no collector buffering on the release). analyzeFinalFields finds such fields over the whole program. It looks through the derived constructor's guards on `this`, and disqualifies fields that live dynamic views can store into (the detection int32 slots already uses, now shared as dynamicFieldStores), fields of runtime-provided or generic classes, and every field in library builds. canBorrowCallArgument accepts a final field of a borrowable object, except in constructors, async functions and generators, and except for nullable-pointer fields, which have no box to lend.
This commit is contained in:
@@ -99,6 +99,7 @@ import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js"
|
||||
import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
|
||||
import { analyzeIntegerRanges, INT32_RANGE, type IntegerRanges } from "../../ir/integer-ranges.js";
|
||||
import { analyzeInt32Slots, type Int32Slots } from "../../ir/int32-slots.js";
|
||||
import { analyzeFinalFields, constructorClass, type FinalFields } from "../../ir/final-fields.js";
|
||||
import { findIntegerViews } from "./integer-views.js";
|
||||
import { findInitializerBindings, withInitializerBindings } from "../../ir/initializer-bindings.js";
|
||||
import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js";
|
||||
@@ -734,6 +735,11 @@ export class LlEmitter {
|
||||
{ slot: string; type: "i32" | "i64"; signed?: boolean; range?: { min: number; max: number } }
|
||||
>();
|
||||
private countedLoopsEnabled = false;
|
||||
/** Whole-program final reference fields (ir/final-fields.ts). */
|
||||
private finalFields!: FinalFields;
|
||||
/** The current function may borrow final fields of borrowable objects:
|
||||
* not a constructor (where they change), not async or a generator. */
|
||||
private finalFieldBorrows = false;
|
||||
integerRanges: IntegerRanges = new Map();
|
||||
private byteWindowsEnabled = true;
|
||||
private byteWindowEntries: ReturnType<typeof findInitializedByteLoopBindings> = new Map();
|
||||
@@ -808,6 +814,7 @@ export class LlEmitter {
|
||||
this.constantNumericTables = findConstantNumericTables(mod);
|
||||
this.initializerBindings = findInitializerBindings(mod);
|
||||
this.int32Slots = analyzeInt32Slots(mod);
|
||||
this.finalFields = analyzeFinalFields(mod);
|
||||
this.sizeType = options.pointerBits === 32 ? "i32" : "i64";
|
||||
this.ffiExtendNarrowIntegers =
|
||||
options.wasi === true || ffiExtendsNarrowIntegers(options.targetTriple);
|
||||
@@ -4942,6 +4949,8 @@ export class LlEmitter {
|
||||
);
|
||||
this.integerLoopBindings.clear();
|
||||
this.countedLoopsEnabled = this.debug === null && !fn.async && !fn.generator;
|
||||
this.finalFieldBorrows =
|
||||
!fn.async && !fn.generator && constructorClass(fn.name) === null && this.finalFields.size > 0;
|
||||
this.byteWindowsEnabled = this.countedLoopsEnabled;
|
||||
this.byteWindowEntries =
|
||||
this.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes")
|
||||
@@ -5311,6 +5320,17 @@ export class LlEmitter {
|
||||
// stable owner. The throwing arm's typed dummy owns nothing.
|
||||
if (value.kind === "unionNarrow") return this.canBorrowCallArgument(value.value);
|
||||
if (value.kind === "libCall") return value.fn === "error.nodeThrow";
|
||||
// A final field never changes outside constructors: while its object is
|
||||
// borrowable, so is the field's value. Nullable-pointer fields have no
|
||||
// box to lend and keep their owning path.
|
||||
if (
|
||||
value.kind === "fieldGet" &&
|
||||
this.finalFieldBorrows &&
|
||||
isRefCounted(value.type) &&
|
||||
this.finalFields.isFinal(value.className, value.field) &&
|
||||
!this.nullableFieldGet(value)
|
||||
)
|
||||
return this.canBorrowReceiver(value.obj) && this.canBorrowCallArgument(value.obj);
|
||||
if (value.kind === "ternary")
|
||||
return (
|
||||
this.canBorrowReceiver(value.then) &&
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { analyzeFinalFields, constructorClass } from "./final-fields.js";
|
||||
import {
|
||||
BOOL,
|
||||
STRING,
|
||||
VOID,
|
||||
type IrClassDef,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "./ir.js";
|
||||
|
||||
const loc = { file: "final-fields.ts", start: 0, end: 0 };
|
||||
const obj = (className: string): IrType => ({ kind: "object", className });
|
||||
const ref = (localId: string, type: IrType): IrExpr => ({ loc, kind: "varRef", localId, type });
|
||||
const str = (value: string): IrExpr => ({ loc, kind: "strLit", value, type: STRING });
|
||||
const set = (target: IrExpr, className: string, field: string, value: IrExpr): IrStmt => ({
|
||||
loc,
|
||||
kind: "fieldSet",
|
||||
obj: target,
|
||||
className,
|
||||
field,
|
||||
value,
|
||||
});
|
||||
const fn = (name: string, self: string | null, body: IrStmt[]): IrFunction => ({
|
||||
loc,
|
||||
name,
|
||||
params: self ? [{ localId: "this", name: "this", type: obj(self) }] : [],
|
||||
locals: self ? [{ id: "this", name: "this", type: obj(self), mutable: false }] : [],
|
||||
returnType: VOID,
|
||||
body,
|
||||
});
|
||||
const cls = (name: string, fields: string[], base?: string): IrClassDef => ({
|
||||
loc,
|
||||
name,
|
||||
...(base ? { base } : {}),
|
||||
fields: fields.map((field) => ({ name: field, type: STRING })),
|
||||
});
|
||||
const mod = (classes: IrClassDef[], functions: IrFunction[]): IrModule => ({
|
||||
irVersion: 15,
|
||||
sourceFile: "final-fields.ts",
|
||||
entry: "%main",
|
||||
classes,
|
||||
functions: [fn("%main", null, []), ...functions],
|
||||
records: [],
|
||||
unions: [],
|
||||
globals: [],
|
||||
});
|
||||
const self = (className: string): IrExpr => ref("this", obj(className));
|
||||
|
||||
test("constructor names map to their class", () => {
|
||||
expect(constructorClass("%%m1.Parser.constructor")).toBe("%m1.Parser");
|
||||
expect(constructorClass("%%m1.Parser.parse")).toBeNull();
|
||||
});
|
||||
|
||||
test("fields written only through their constructors' this are final", () => {
|
||||
const finals = analyzeFinalFields(
|
||||
mod(
|
||||
[cls("A", ["kept", "moved"]), cls("B", ["kept", "moved", "own"], "A")],
|
||||
[
|
||||
fn("%A.constructor", "A", [
|
||||
set(self("A"), "A", "kept", str("a")),
|
||||
set(self("A"), "A", "kept", str("twice")),
|
||||
set(self("A"), "A", "moved", str("a")),
|
||||
]),
|
||||
// A subclass constructor initializes its base's field through a guard.
|
||||
fn("%B.constructor", "B", [
|
||||
set(
|
||||
{
|
||||
loc,
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{
|
||||
loc,
|
||||
kind: "if",
|
||||
cond: {
|
||||
loc,
|
||||
kind: "unary",
|
||||
op: "!",
|
||||
operand: ref("superCalled", BOOL),
|
||||
type: BOOL,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
loc,
|
||||
kind: "libCall",
|
||||
fn: "error.nodeThrow",
|
||||
args: [str("ReferenceError")],
|
||||
type: VOID,
|
||||
},
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
},
|
||||
],
|
||||
result: self("B"),
|
||||
type: obj("B"),
|
||||
},
|
||||
"B",
|
||||
"own",
|
||||
str("b"),
|
||||
),
|
||||
]),
|
||||
// Any write elsewhere disqualifies the field family.
|
||||
fn("%A.reset", "A", [set(self("A"), "A", "moved", str("later"))]),
|
||||
],
|
||||
),
|
||||
);
|
||||
expect(finals.isFinal("A", "kept")).toBe(true);
|
||||
expect(finals.isFinal("B", "kept")).toBe(true);
|
||||
expect(finals.isFinal("B", "own")).toBe(true);
|
||||
expect(finals.isFinal("A", "moved")).toBe(false);
|
||||
expect(finals.isFinal("B", "moved")).toBe(false);
|
||||
expect(finals.reasons.get("A\0moved")).toBe("written in %A.reset");
|
||||
});
|
||||
|
||||
test("constructors writing another object, and library builds, are not final", () => {
|
||||
const other = fn("%A.constructor", "A", [set(ref("peer", obj("A")), "A", "kept", str("a"))]);
|
||||
other.locals.push({ id: "peer", name: "peer", type: obj("A"), mutable: false });
|
||||
expect(analyzeFinalFields(mod([cls("A", ["kept"])], [other])).isFinal("A", "kept")).toBe(false);
|
||||
const library = mod([cls("A", ["kept"])], []);
|
||||
library.lib = { profile: "p" } as unknown as NonNullable<IrModule["lib"]>;
|
||||
expect(analyzeFinalFields(library).size).toBe(0);
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import { isRefCounted, type IrClassDef, type IrExpr, type IrModule, type IrStmt } from "./ir.js";
|
||||
import { dynamicFieldStores } from "./int32-slots.js";
|
||||
import { everyExprChild, everyStmtChild } from "./traverse.js";
|
||||
|
||||
/** Whole-program final reference fields.
|
||||
*
|
||||
* A reference-counted class field is final when every write the program can
|
||||
* make to it is a constructor of its family storing through that
|
||||
* constructor's own `this`. Outside the constructors such a field never
|
||||
* changes, so while its object is alive the field keeps its value alive: a
|
||||
* read of `o.f` may be borrowed for as long as `o` itself is (`this.scanner`
|
||||
* inside a method, across any call), without a retain and a release.
|
||||
* Constructors are excluded from that use, because they are where the field
|
||||
* changes; so are async functions and generators, whose suspensions let
|
||||
* other code (a constructor that is still running) continue.
|
||||
*
|
||||
* Writes the IR does not show disqualify fields up front: library builds
|
||||
* (instances cross a native boundary), live dynamic views whose stores can
|
||||
* name the field (see dynamicFieldStores), and classes with runtime-provided
|
||||
* or generic ancestors. Generated stores only initialize fresh objects or
|
||||
* tear down dead ones. */
|
||||
export class FinalFields {
|
||||
private readonly finals = new Set<string>();
|
||||
private readonly familyOf = new Map<string, string | null>();
|
||||
/** Disqualified families and the first reason, for tests. */
|
||||
readonly reasons = new Map<string, string>();
|
||||
|
||||
constructor(private readonly classes: ReadonlyMap<string, IrClassDef> = new Map()) {}
|
||||
|
||||
/** The topmost class in the base chain declaring the field, and the field. */
|
||||
family(className: string, field: string): string | null {
|
||||
const key = `${className}\0${field}`;
|
||||
const cached = this.familyOf.get(key);
|
||||
if (cached !== undefined) return cached;
|
||||
let owner: IrClassDef | undefined = this.classes.get(className);
|
||||
let result: string | null = null;
|
||||
if (owner?.fields.some((f) => f.name === field)) {
|
||||
for (;;) {
|
||||
const base: IrClassDef | undefined = owner.base ? this.classes.get(owner.base) : undefined;
|
||||
if (!base?.fields.some((f) => f.name === field)) break;
|
||||
owner = base;
|
||||
}
|
||||
result = `${owner.name}\0${field}`;
|
||||
}
|
||||
this.familyOf.set(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
isFinal(className: string, field: string): boolean {
|
||||
if (this.finals.size === 0) return false;
|
||||
const family = this.family(className, field);
|
||||
return family !== null && this.finals.has(family);
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.finals.size;
|
||||
}
|
||||
|
||||
/* ── construction (analyzeFinalFields only) ── */
|
||||
add(family: string): void {
|
||||
this.finals.add(family);
|
||||
}
|
||||
drop(family: string, why: string): void {
|
||||
if (!this.reasons.has(family)) this.reasons.set(family, why);
|
||||
this.finals.delete(family);
|
||||
}
|
||||
}
|
||||
|
||||
const CONSTRUCTOR_SUFFIX = ".constructor";
|
||||
|
||||
/** `%C.constructor`'s class name, or null for any other function. */
|
||||
export function constructorClass(fnName: string): string | null {
|
||||
return fnName.startsWith("%") && fnName.endsWith(CONSTRUCTOR_SUFFIX)
|
||||
? fnName.slice(1, -CONSTRUCTOR_SUFFIX.length)
|
||||
: null;
|
||||
}
|
||||
|
||||
export function analyzeFinalFields(mod: IrModule): FinalFields {
|
||||
const classes = new Map((mod.classes ?? []).map((c) => [c.name, c]));
|
||||
const finals = new FinalFields(classes);
|
||||
if (mod.lib) return finals;
|
||||
const dynamic = dynamicFieldStores(mod);
|
||||
if (dynamic.all) return finals;
|
||||
|
||||
const genericFamilies = new Set<string>();
|
||||
for (const cls of classes.values())
|
||||
if (cls.genericOf !== undefined) {
|
||||
genericFamilies.add(cls.name);
|
||||
genericFamilies.add(cls.genericOf);
|
||||
}
|
||||
const plainClass = (cls: IrClassDef): boolean => {
|
||||
for (let c: IrClassDef | undefined = cls; c; c = c.base ? classes.get(c.base) : undefined) {
|
||||
if (c.runtime || genericFamilies.has(c.name)) return false;
|
||||
if (c.base && !classes.has(c.base)) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const inFamily = (className: string, family: string): boolean => {
|
||||
const owner = family.slice(0, family.indexOf("\0"));
|
||||
for (let c = classes.get(className); c; c = c.base ? classes.get(c.base) : undefined)
|
||||
if (c.name === owner) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
for (const cls of classes.values())
|
||||
for (const field of cls.fields) {
|
||||
if (!isRefCounted(field.type) || field.name.startsWith("%")) continue;
|
||||
const family = finals.family(cls.name, field.name)!;
|
||||
if (finals.reasons.has(family)) continue;
|
||||
if (!plainClass(cls)) finals.drop(family, "runtime-provided or generic class");
|
||||
else if (dynamic.names.has(field.name))
|
||||
finals.drop(family, "dynamic stores can name the field");
|
||||
else finals.add(family);
|
||||
}
|
||||
if (finals.size === 0) return finals;
|
||||
|
||||
for (const fn of mod.functions) {
|
||||
const ctor = constructorClass(fn.name);
|
||||
const self = ctor !== null ? fn.params[0]?.localId : undefined;
|
||||
const selfLocal = self !== undefined ? fn.locals.find((l) => l.id === self) : undefined;
|
||||
// `this` must name the object under construction for the whole body.
|
||||
const constructing = selfLocal !== undefined && !selfLocal.boxed && !reassigns(fn.body, self!);
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "fieldSet") {
|
||||
const family = finals.family(s.className, s.field);
|
||||
if (family !== null && finals.isFinal(s.className, s.field)) {
|
||||
const own =
|
||||
constructing && ctor !== null && inFamily(ctor, family) && isSelf(s.obj, self!);
|
||||
if (!own) finals.drop(family, `written in ${fn.name}`);
|
||||
}
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
const expr = (e: IrExpr): boolean => everyExprChild(e, expr, stmt);
|
||||
for (const s of fn.body) stmt(s);
|
||||
}
|
||||
return finals;
|
||||
}
|
||||
|
||||
/** `this` itself, or behind the derived constructor's guards (`if (!super
|
||||
* called) throw`), which read no reference and write nothing. */
|
||||
function isSelf(e: IrExpr, self: string): boolean {
|
||||
if (e.kind === "varRef") return e.localId === self;
|
||||
if (e.kind !== "seqExpr") return false;
|
||||
for (const s of e.stmts) {
|
||||
if (s.kind !== "if" || s.else_ !== null || s.then.length !== 1) return false;
|
||||
const only = s.then[0]!;
|
||||
if (
|
||||
only.kind !== "exprStmt" ||
|
||||
only.expr.kind !== "libCall" ||
|
||||
only.expr.fn !== "error.nodeThrow" ||
|
||||
!only.expr.args.every((arg) => arg.kind === "numLit" || arg.kind === "strLit") ||
|
||||
!throwFreeCondition(s.cond)
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return isSelf(e.result, self);
|
||||
}
|
||||
|
||||
function throwFreeCondition(e: IrExpr): boolean {
|
||||
if (e.kind === "varRef" || e.kind === "boolLit") return e.type.kind === "bool";
|
||||
return e.kind === "unary" && throwFreeCondition(e.operand);
|
||||
}
|
||||
|
||||
function reassigns(body: IrStmt[], localId: string): boolean {
|
||||
let found = false;
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if ((e.kind === "assignExpr" || e.kind === "incDec") && e.localId === localId) found = true;
|
||||
return !found && everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "assign" && s.localId === localId && s.initializes !== true) found = true;
|
||||
return !found && everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
for (const s of body) if (!stmt(s)) break;
|
||||
return found;
|
||||
}
|
||||
@@ -271,6 +271,72 @@ function walkFunction(
|
||||
for (const s of fn.body) stmt(s);
|
||||
}
|
||||
|
||||
/** The class field names that runtime stores through live dynamic views
|
||||
* (typed-ref capsules) can write: every name when `all` (islands, jsval
|
||||
* values, or a dynamic store whose key is not a literal), otherwise the
|
||||
* literal keys of `dyn.keySet`/`dyn.keyDelete`. A view commits a store back
|
||||
* into the instance's native field slots, which the IR does not show. */
|
||||
export function dynamicFieldStores(
|
||||
mod: IrModule,
|
||||
records = new Map((mod.records ?? []).map((r) => [r.id, r])),
|
||||
unions = new Map((mod.unions ?? []).map((u) => [u.id, u])),
|
||||
): { all: boolean; names: Set<string> } {
|
||||
const dynTypes = new Map<IrType, boolean>();
|
||||
const dynNamed = new Map<string, boolean>();
|
||||
const dynish = (type: IrType): boolean => {
|
||||
if (DYN_KINDS.has(type.kind)) return true;
|
||||
const cached = dynTypes.get(type);
|
||||
if (cached !== undefined) return cached;
|
||||
let result = false;
|
||||
const named =
|
||||
type.kind === "record"
|
||||
? `r:${type.shapeId}`
|
||||
: type.kind === "union"
|
||||
? `u:${type.unionId}`
|
||||
: null;
|
||||
if (named) {
|
||||
const known = dynNamed.get(named);
|
||||
if (known !== undefined) return known;
|
||||
dynNamed.set(named, false);
|
||||
const members =
|
||||
type.kind === "record"
|
||||
? [
|
||||
...(records.get(type.shapeId)?.fields.map((f) => f.type) ?? []),
|
||||
...(records.get(type.shapeId)?.indexValue
|
||||
? [records.get(type.shapeId)!.indexValue!]
|
||||
: []),
|
||||
]
|
||||
: type.kind === "union"
|
||||
? (unions.get(type.unionId)?.arms ?? [])
|
||||
: [];
|
||||
result = members.some(dynish);
|
||||
dynNamed.set(named, result);
|
||||
} else everyTypeChild(type, (child) => !(result = dynish(child)));
|
||||
dynTypes.set(type, result);
|
||||
return result;
|
||||
};
|
||||
const dynWritten = new Set<string>();
|
||||
let dynWritesAll = false;
|
||||
for (const fn of mod.functions)
|
||||
walkFunction(fn, {
|
||||
expr: (e) => {
|
||||
if (dynWritesAll) return;
|
||||
if (ISLAND_KINDS.has(e.kind) || e.type.kind === "jsval") dynWritesAll = true;
|
||||
else if (
|
||||
e.kind === "libCall" &&
|
||||
!DYN_READ_ONLY.has(e.fn) &&
|
||||
e.args.some((a) => dynish(a.type))
|
||||
) {
|
||||
const key = e.args[1];
|
||||
if ((e.fn === "dyn.keySet" || e.fn === "dyn.keyDelete") && key?.kind === "strLit")
|
||||
dynWritten.add(key.value);
|
||||
else dynWritesAll = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
return { all: dynWritesAll, names: dynWritten };
|
||||
}
|
||||
|
||||
export function analyzeInt32Slots(mod: IrModule): Int32Slots {
|
||||
const classes = new Map((mod.classes ?? []).map((c) => [c.name, c]));
|
||||
const slots = new Int32Slots(classes);
|
||||
@@ -371,59 +437,7 @@ export function analyzeInt32Slots(mod: IrModule): Int32Slots {
|
||||
// writes every field from the view, which holds the exact snapshot value
|
||||
// of any property no store named. So reaching dynamic code disqualifies
|
||||
// exactly the field names some dynamic store in the program can name.
|
||||
const dynTypes = new Map<IrType, boolean>();
|
||||
const dynNamed = new Map<string, boolean>();
|
||||
const dynish = (type: IrType): boolean => {
|
||||
if (DYN_KINDS.has(type.kind)) return true;
|
||||
const cached = dynTypes.get(type);
|
||||
if (cached !== undefined) return cached;
|
||||
let result = false;
|
||||
const named =
|
||||
type.kind === "record"
|
||||
? `r:${type.shapeId}`
|
||||
: type.kind === "union"
|
||||
? `u:${type.unionId}`
|
||||
: null;
|
||||
if (named) {
|
||||
const known = dynNamed.get(named);
|
||||
if (known !== undefined) return known;
|
||||
dynNamed.set(named, false);
|
||||
const members =
|
||||
type.kind === "record"
|
||||
? [
|
||||
...(records.get(type.shapeId)?.fields.map((f) => f.type) ?? []),
|
||||
...(records.get(type.shapeId)?.indexValue
|
||||
? [records.get(type.shapeId)!.indexValue!]
|
||||
: []),
|
||||
]
|
||||
: type.kind === "union"
|
||||
? (unions.get(type.unionId)?.arms ?? [])
|
||||
: [];
|
||||
result = members.some(dynish);
|
||||
dynNamed.set(named, result);
|
||||
} else everyTypeChild(type, (child) => !(result = dynish(child)));
|
||||
dynTypes.set(type, result);
|
||||
return result;
|
||||
};
|
||||
const dynWritten = new Set<string>();
|
||||
let dynWritesAll = false;
|
||||
for (const fn of mod.functions)
|
||||
walkFunction(fn, {
|
||||
expr: (e) => {
|
||||
if (dynWritesAll) return;
|
||||
if (ISLAND_KINDS.has(e.kind) || e.type.kind === "jsval") dynWritesAll = true;
|
||||
else if (
|
||||
e.kind === "libCall" &&
|
||||
!DYN_READ_ONLY.has(e.fn) &&
|
||||
e.args.some((a) => dynish(a.type))
|
||||
) {
|
||||
const key = e.args[1];
|
||||
if ((e.fn === "dyn.keySet" || e.fn === "dyn.keyDelete") && key?.kind === "strLit")
|
||||
dynWritten.add(key.value);
|
||||
else dynWritesAll = true;
|
||||
}
|
||||
},
|
||||
});
|
||||
const { all: dynWritesAll, names: dynWritten } = dynamicFieldStores(mod, records, unions);
|
||||
for (const fn of mod.functions)
|
||||
walkFunction(fn, {
|
||||
expr: (e) => {
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// Reference fields written only by their class's constructors may be read
|
||||
// without a retain wherever their object is held. Every other field keeps
|
||||
// its owning reads: a callee may replace it while the caller still uses the
|
||||
// old value.
|
||||
|
||||
class Buffer2 {
|
||||
readonly items: string[] = [];
|
||||
label: string;
|
||||
constructor(label: string) {
|
||||
this.label = label;
|
||||
}
|
||||
push(item: string): number {
|
||||
this.items.push(item);
|
||||
return this.items.length;
|
||||
}
|
||||
describe(): string {
|
||||
return `${this.label}:${this.items.join(",")}`;
|
||||
}
|
||||
}
|
||||
|
||||
class Owner {
|
||||
// Final: written only here, twice, with a method call between the writes.
|
||||
readonly primary: Buffer2;
|
||||
// Not final: replaced by `rotate`.
|
||||
current: Buffer2;
|
||||
log: string[] = [];
|
||||
constructor() {
|
||||
this.primary = new Buffer2("first");
|
||||
this.current = new Buffer2("c0");
|
||||
this.log.push(this.peek());
|
||||
this.primary = new Buffer2("primary");
|
||||
this.log.push(this.peek());
|
||||
}
|
||||
peek(): string {
|
||||
const primary = this.primary;
|
||||
primary.push("peeked");
|
||||
return primary.describe();
|
||||
}
|
||||
// The callee replaces `current` while the caller's receiver is the old one.
|
||||
rotate(next: string): Buffer2 {
|
||||
const old = this.current;
|
||||
this.current = new Buffer2(next);
|
||||
return old;
|
||||
}
|
||||
useCurrent(): string {
|
||||
return this.current.push(this.rotate("c1").describe()) + " " + this.current.describe();
|
||||
}
|
||||
usePrimary(): string {
|
||||
const before = this.primary.push("a");
|
||||
const again = this.primary.push(this.rotate("c2").label);
|
||||
return `${before} ${again} ${this.primary.describe()}`;
|
||||
}
|
||||
}
|
||||
|
||||
class Derived extends Owner {
|
||||
readonly extra: Buffer2;
|
||||
constructor() {
|
||||
super();
|
||||
this.extra = new Buffer2("extra");
|
||||
this.extra.push(this.primary.describe());
|
||||
}
|
||||
report(): string {
|
||||
return `${this.extra.describe()} / ${this.primary.describe()}`;
|
||||
}
|
||||
}
|
||||
|
||||
// A field a closure created in the constructor writes later is not final.
|
||||
class Callbacks {
|
||||
target: Buffer2;
|
||||
readonly reset: () => void;
|
||||
constructor() {
|
||||
this.target = new Buffer2("t0");
|
||||
this.reset = () => {
|
||||
this.target = new Buffer2("t1");
|
||||
};
|
||||
}
|
||||
run(): string {
|
||||
return this.target.push(this.fire()) + " " + this.target.describe();
|
||||
}
|
||||
fire(): string {
|
||||
this.reset();
|
||||
return "fired";
|
||||
}
|
||||
}
|
||||
|
||||
// A field written through another object, outside any constructor.
|
||||
class Node2 {
|
||||
next: Node2 | undefined;
|
||||
readonly name: string;
|
||||
readonly payload: Buffer2;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
this.payload = new Buffer2(name);
|
||||
}
|
||||
}
|
||||
function relink(a: Node2): string {
|
||||
const first = a.next!;
|
||||
a.next = new Node2("b2");
|
||||
return first.payload.describe() + " -> " + a.next.payload.describe();
|
||||
}
|
||||
|
||||
const owner = new Owner();
|
||||
console.log(owner.log.join(" | "));
|
||||
console.log(owner.useCurrent());
|
||||
console.log(owner.usePrimary());
|
||||
console.log(owner.current.describe());
|
||||
const derived = new Derived();
|
||||
console.log(derived.report());
|
||||
const callbacks = new Callbacks();
|
||||
console.log(callbacks.run());
|
||||
const a = new Node2("a");
|
||||
a.next = new Node2("b");
|
||||
a.next.payload.push("x");
|
||||
console.log(relink(a));
|
||||
|
||||
// Async methods keep owning reads across their suspensions.
|
||||
class Pending {
|
||||
readonly box: Buffer2 = new Buffer2("async");
|
||||
async later(): Promise<string> {
|
||||
const box = this.box;
|
||||
box.push("before");
|
||||
await Promise.resolve();
|
||||
box.push("after");
|
||||
return box.describe();
|
||||
}
|
||||
}
|
||||
new Pending().later().then((text) => console.log(text));
|
||||
Reference in New Issue
Block a user