mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-06 08:23:19 +08:00
Merge pull request #848 from vercel-labs/perf/rc-elision-si
Borrow and move references whose owners provably outlive them
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { IrExpr } from "../../ir/ir.js";
|
||||
import type { Writes } from "./reference-writes.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
|
||||
/** For a runtime input whose ABI borrows rather than consumes ownership.
|
||||
@@ -14,18 +15,33 @@ export function emitBorrowedInput(host: LlvmEmitterContext, value: IrExpr): LlVa
|
||||
* while every operation preserves existing owners. Compute suffix facts
|
||||
* once, keeping left-to-right evaluation and the owning fallback for each
|
||||
* argument independently. The consumer must borrow all inputs and preserve
|
||||
* reference edges itself; mutating runtime operations use emitBorrowedInput. */
|
||||
* reference edges itself; mutating runtime operations use emitBorrowedInput.
|
||||
* With `consumerWrites` (the locations the consumer may overwrite), a
|
||||
* projection also borrows when neither the consumer nor any later operand
|
||||
* can write a location its read depends on (ReferenceWrites). */
|
||||
export function borrowableInputs(
|
||||
host: LlvmEmitterContext,
|
||||
inputs: readonly IrExpr[],
|
||||
consumerPreserves = true,
|
||||
consumerWrites?: Writes,
|
||||
): boolean[] {
|
||||
const result: boolean[] = new Array(inputs.length);
|
||||
let preserves = consumerPreserves;
|
||||
const writes = host.referenceWrites;
|
||||
let suffix: Writes =
|
||||
consumerWrites !== undefined
|
||||
? consumerWrites
|
||||
: consumerPreserves
|
||||
? null
|
||||
: (writes?.heapAll ?? null);
|
||||
for (let i = inputs.length - 1; i >= 0; i--) {
|
||||
const value = inputs[i]!;
|
||||
result[i] = host.canBorrowCallArgument(value) || (preserves && host.canBorrowReceiver(value));
|
||||
result[i] =
|
||||
host.canBorrowCallArgument(value) ||
|
||||
(preserves && host.canBorrowReceiver(value)) ||
|
||||
(writes !== null && host.canBorrowReceiver(value) && host.unwrittenRead(value, suffix));
|
||||
preserves = preserves && host.referenceEffects.preserves(value);
|
||||
if (writes !== null) suffix = writes.union([suffix, writes.expr(value)]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -36,3 +52,21 @@ export function emitBorrowedInputs(host: LlvmEmitterContext, inputs: readonly Ir
|
||||
borrowed[index] ? host.emitReadReceiver(value) : host.emitExpr(value),
|
||||
);
|
||||
}
|
||||
|
||||
/** The writes after each input: the consumer's plus every later input's
|
||||
* (null entries: nothing; an empty array without ReferenceWrites). */
|
||||
export function inputSuffixWrites(
|
||||
host: LlvmEmitterContext,
|
||||
inputs: readonly IrExpr[],
|
||||
consumerWrites: Writes,
|
||||
): Writes[] {
|
||||
const writes = host.referenceWrites;
|
||||
if (writes === null) return [];
|
||||
const out: Writes[] = new Array(inputs.length);
|
||||
let suffix = consumerWrites;
|
||||
for (let i = inputs.length - 1; i >= 0; i--) {
|
||||
out[i] = suffix;
|
||||
suffix = writes.union([suffix, writes.expr(inputs[i]!)]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,17 @@ test.each([32, 64] as const)("strict scalar reads check dense storage inline (%i
|
||||
|
||||
test("reference reads retain inline and keep a cold +1 runtime fallback", () => {
|
||||
const read: IrExpr = { kind: "arrayGet", arr: ref("s", words), index, type: STRING, loc };
|
||||
const llvm = emitLlvmModule(module("word", STRING, [{ kind: "return", value: read, loc }]));
|
||||
// An element removal in the body keeps the read from being lent as a
|
||||
// borrowed return: the result must own its reference.
|
||||
const truncate: IrStmt = {
|
||||
kind: "arraySetLength",
|
||||
arr: ref("s", words),
|
||||
length: { kind: "numLit", value: 1, type: F64, loc },
|
||||
loc,
|
||||
};
|
||||
const llvm = emitLlvmModule(
|
||||
module("word", STRING, [truncate, { kind: "return", value: read, loc }]),
|
||||
);
|
||||
const fn = body(llvm, "word");
|
||||
expect(fn).toMatch(/icmp ne ptr .*, null/);
|
||||
expect(fn).toContain("@scr_str_retain_v(");
|
||||
|
||||
@@ -159,9 +159,10 @@ import {
|
||||
} from "./map-read-lifetimes.js";
|
||||
import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { analyzeWalks, findWalkBorrows } from "./walk-borrows.js";
|
||||
import { analyzeWalks, findWalkBorrows, type WalkBorrowHost } from "./walk-borrows.js";
|
||||
import { findReboundParameters } from "./rebound-parameters.js";
|
||||
import { ReferenceEffects } from "./reference-effects.js";
|
||||
import { ReferenceWrites, type Writes } from "./reference-writes.js";
|
||||
import { LlvmDebugInfo } from "./debug-info.js";
|
||||
import { StackCallbacks } from "./stack-callbacks.js";
|
||||
import { findLoopArrayBorrows } from "./loop-array-borrows.js";
|
||||
@@ -583,6 +584,12 @@ export class LlEmitter {
|
||||
* fiberless-async analysis scans it for suspension entry points. */
|
||||
private readonly fnDefText = new Map<string, string>();
|
||||
readonly referenceEffects: ReferenceEffects;
|
||||
/** Field-sensitive reference writes (null in debug builds). */
|
||||
referenceWrites: ReferenceWrites | null = null;
|
||||
private readonly borrowedReturnKeyCache = new Map<string, string[]>();
|
||||
/** The statement list being emitted and the current index, for blocks
|
||||
* whose bindings end with the list (emitBlock and function bodies). */
|
||||
private stmtCursor: { stmts: IrStmt[]; index: number } | null = null;
|
||||
readonly optionalArrayReads: OptionalArrayReads;
|
||||
readonly checkedNarrows: CheckedNarrows;
|
||||
callArrayReads = new Map<IrExpr, LocalArrayRead>();
|
||||
@@ -891,6 +898,15 @@ export class LlEmitter {
|
||||
(call) => this.optionalArrayReads.get(call) !== null,
|
||||
mod.classes ?? [],
|
||||
);
|
||||
if (this.debug === null)
|
||||
this.referenceWrites = new ReferenceWrites(
|
||||
this.fnByName,
|
||||
mod.classes ?? [],
|
||||
(call) => this.optionalArrayReads.get(call) !== null,
|
||||
new Set((mod.globals ?? []).map((g) => g.id)),
|
||||
{ unions: mod.unions, records: mod.records },
|
||||
this.immutableFields,
|
||||
);
|
||||
const walkParameters = new Map<string, Set<number>>();
|
||||
if (this.debug === null) {
|
||||
const host = {
|
||||
@@ -898,8 +914,9 @@ export class LlEmitter {
|
||||
borrowsWithoutOwning: (e: IrExpr) => this.borrowsWithoutOwning(e),
|
||||
};
|
||||
for (const fn of this.fnByName.values()) {
|
||||
if (!this.referenceEffects.functions.has(fn.name)) continue;
|
||||
const walks = findWalkBorrows(fn, host);
|
||||
const preserving = this.referenceEffects.functions.has(fn.name);
|
||||
if (!preserving && this.referenceWrites === null) continue;
|
||||
const walks = findWalkBorrows(fn, preserving ? host : this.unwrittenWalkHost(fn, host));
|
||||
if (walks.size === 0) continue;
|
||||
this.walkBorrowsByFunction.set(fn.name, walks);
|
||||
const indexes = new Set<number>();
|
||||
@@ -4948,6 +4965,7 @@ export class LlEmitter {
|
||||
const numericFn = withInitializerBindings(fn, initializerBindings);
|
||||
this.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l]));
|
||||
this.borrowedParameters.clear();
|
||||
this.referenceWrites?.enter(fn);
|
||||
this.reboundOwnerSlots.clear();
|
||||
this.projectedParameters.clear();
|
||||
const projectedParameterIndexes = this.callLifetimes.parameters.get(fn.name);
|
||||
@@ -4988,6 +5006,7 @@ export class LlEmitter {
|
||||
this.callLifetimes,
|
||||
this.referenceEffects,
|
||||
this.optionalArrayReads,
|
||||
this.referenceWrites,
|
||||
)
|
||||
: new Set();
|
||||
this.localArrayReads = findLocalArrayReads(
|
||||
@@ -5192,7 +5211,7 @@ export class LlEmitter {
|
||||
}
|
||||
if (this.stackChecks?.has(fn.name)) this.emitStackCheck();
|
||||
if (this.workerEntryPolls?.has(fn.name)) this.emitPendingCheck();
|
||||
this.emitStmts(fn.body);
|
||||
this.emitStmts(fn.body, true);
|
||||
// Implicit exit of a void function: release the function scope unless
|
||||
// the body already terminated its final block (return, or a throw
|
||||
// whose unwind released everything down to depth 0).
|
||||
@@ -5395,15 +5414,202 @@ export class LlEmitter {
|
||||
: this.immutableFields.recordField(e.shapeId, e.field);
|
||||
}
|
||||
|
||||
/** A projection read whose locations nothing writes while a borrowed
|
||||
* read of it is in use (ReferenceWrites), given the writes of that
|
||||
* extent. The root binding itself must stay alive across it too, which
|
||||
* its own location (`l:`, `g:`, `bx`) covers. */
|
||||
unwrittenRead(e: IrExpr, writes: Writes): boolean {
|
||||
const rw = this.referenceWrites;
|
||||
if (rw === null) return false;
|
||||
const keys = rw.readKeys(e, (call) => this.borrowedReturnKeys(call.callee));
|
||||
return keys !== null && !rw.conflicts(keys, writes);
|
||||
}
|
||||
|
||||
/** A stable binding initialized by a projection can borrow it when the
|
||||
* rest of its block (its whole lifetime) writes none of the projection's
|
||||
* locations: the source edge then keeps the value alive, and uses that
|
||||
* escape still retain their own copies. */
|
||||
private unwrittenAlias(s: IrStmt & { kind: "varDecl" }): boolean {
|
||||
const cursor = this.stmtCursor;
|
||||
const rw = this.referenceWrites;
|
||||
if (rw === null || s.init === null || cursor === null || cursor.stmts[cursor.index] !== s)
|
||||
return false;
|
||||
if (
|
||||
!this.plainReference(s.init.type) ||
|
||||
!this.canBorrowReceiver(s.init) ||
|
||||
!this.unownedWalk(s.init)
|
||||
)
|
||||
return false;
|
||||
const writes = rw.union([rw.expr(s.init), rw.stmts(cursor.stmts.slice(cursor.index + 1))]);
|
||||
return this.unwrittenRead(s.init, writes);
|
||||
}
|
||||
|
||||
/** An optional element read into a projection-only local can borrow the
|
||||
* element when the rest of the block writes no array element and none of
|
||||
* the locations keeping the array alive (which must itself be read
|
||||
* without a frame owner). */
|
||||
private unwrittenLocalArrayRead(s: IrStmt & { kind: "varDecl" }, read: LocalArrayRead): boolean {
|
||||
const cursor = this.stmtCursor;
|
||||
const rw = this.referenceWrites;
|
||||
if (rw === null || s.init === null || cursor === null || cursor.stmts[cursor.index] !== s)
|
||||
return false;
|
||||
if (!this.unownedWalk(read.array)) return false;
|
||||
const keys = rw.readKeys(read.array);
|
||||
if (keys === null || rw.expr(read.index) !== null) return false;
|
||||
const writes = rw.union([rw.expr(s.init), rw.stmts(cursor.stmts.slice(cursor.index + 1))]);
|
||||
return !rw.conflicts(["a", ...keys], writes);
|
||||
}
|
||||
|
||||
/** An optional element read passed to a projection-only parameter can
|
||||
* borrow the element when the callee and later arguments (`writes`)
|
||||
* write no array element and nothing keeping the array alive. */
|
||||
unwrittenCallArrayRead(read: LocalArrayRead, writes: Writes): boolean {
|
||||
const rw = this.referenceWrites;
|
||||
if (rw === null) return false;
|
||||
const keys = rw.readKeys(read.array);
|
||||
if (keys === null || rw.expr(read.index) !== null) return false;
|
||||
return !rw.conflicts(["a", ...keys], writes);
|
||||
}
|
||||
|
||||
/** `return x` of a local whose slot is an owned scope entry of the same
|
||||
* type: the return can take over the local's reference. */
|
||||
private movableReturn(
|
||||
value: IrExpr | null,
|
||||
): { scope: LlScopeEntry[]; entry: LlScopeEntry; index: number } | null {
|
||||
if (value === null || value.kind !== "varRef" || !isRefCounted(value.type)) return null;
|
||||
if (this.currentBorrowedReturn || this.currentWasiCoro !== null) return null;
|
||||
const binding = this.binding(value.localId);
|
||||
if (binding.kind !== "local" || binding.local === undefined || binding.local.boxed) return null;
|
||||
if (!typeEquals(binding.type, value.type) || !typeEquals(this.currentReturnType, value.type))
|
||||
return null;
|
||||
for (const scope of this.scopes) {
|
||||
const index = scope.findIndex((entry) => entry.slot === binding.slot);
|
||||
if (index < 0) continue;
|
||||
const entry = scope[index]!;
|
||||
if (entry.boxed || entry.lazy || !typeEquals(entry.type, value.type)) return null;
|
||||
return { scope, entry, index };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A walk host for a body that may write the heap: each walk must read
|
||||
* only locations the whole body (with its callees) never writes;
|
||||
* rebinding the walk locals themselves releases nothing. */
|
||||
private unwrittenWalkHost(fn: IrFunction, host: WalkBorrowHost): WalkBorrowHost {
|
||||
return {
|
||||
...host,
|
||||
unwritten: (value, candidates) =>
|
||||
this.referenceWrites?.unwrittenWalk(fn, value, candidates, (call) =>
|
||||
this.borrowedReturnKeys(call.callee),
|
||||
) ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
/** emitReadReceiver yields a plain pointer for `e` that no statement
|
||||
* frame owns, so it can outlive the statement (walk-borrows' walks, from
|
||||
* any unboxed plain-pointer binding, plus element reads). */
|
||||
private unownedWalk(e: IrExpr): boolean {
|
||||
switch (e.kind) {
|
||||
case "varRef": {
|
||||
const binding = this.binding(e.localId);
|
||||
if (binding.kind === "global") return this.plainReference(binding.type);
|
||||
return (
|
||||
binding.kind === "local" &&
|
||||
binding.local !== undefined &&
|
||||
!binding.local.boxed &&
|
||||
!binding.local.tdz &&
|
||||
this.plainReference(binding.local.type) &&
|
||||
!this.stringSlices.has(e.localId) &&
|
||||
!this.storedSplits.has(e.localId) &&
|
||||
!this.localUnionStorage.has(e.localId)
|
||||
);
|
||||
}
|
||||
case "fieldGet":
|
||||
case "recordGet":
|
||||
return this.borrowsWithoutOwning(e) && this.unownedWalk(e.obj);
|
||||
case "unionNarrow":
|
||||
case "downcast":
|
||||
case "upcast":
|
||||
return this.borrowsWithoutOwning(e) && this.unownedWalk(e.value);
|
||||
case "unionWrap":
|
||||
if (!this.borrowsWithoutOwning(e)) return false;
|
||||
return e.value.kind === "unitLit" || this.unownedWalk(e.value);
|
||||
case "ternary": {
|
||||
const arm = (v: IrExpr): boolean =>
|
||||
(v.kind === "libCall" && v.fn === "error.nodeThrow") || this.unownedWalk(v);
|
||||
return this.borrowsWithoutOwning(e) && arm(e.then) && arm(e.else_);
|
||||
}
|
||||
case "call":
|
||||
return (
|
||||
this.borrowsWithoutOwning(e) &&
|
||||
e.args.every((arg) => !isRefCounted(arg.type) || this.unownedWalk(arg))
|
||||
);
|
||||
case "arrayGet":
|
||||
return (
|
||||
!this.isStackUnionSource(e) &&
|
||||
this.optionalArrayReads.get(e) === null &&
|
||||
this.unownedWalk(e.arr)
|
||||
);
|
||||
case "seqExpr": {
|
||||
const guarded = guardedValue(e);
|
||||
return guarded !== null && this.unownedWalk(guarded);
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Every location a borrowed-return callee's walks can read (its body's
|
||||
* field, record and element reads, through nested borrowed returns). */
|
||||
private borrowedReturnKeys(callee: string): string[] | null {
|
||||
if (!this.borrowedReturns.has(callee)) return null;
|
||||
const known = this.borrowedReturnKeyCache.get(callee);
|
||||
if (known) return known;
|
||||
const rw = this.referenceWrites!;
|
||||
const keys = new Set<string>();
|
||||
const seen = new Set([callee]);
|
||||
const pending = [callee];
|
||||
while (pending.length > 0) {
|
||||
const fn = this.fnByName.get(pending.pop()!);
|
||||
if (!fn) continue;
|
||||
everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (e) => {
|
||||
if (e.kind === "fieldGet") keys.add(rw.fieldKey(e.className, e.field));
|
||||
else if (e.kind === "recordGet") {
|
||||
keys.add(`r:${e.field}`);
|
||||
keys.add("r:*");
|
||||
} else if (e.kind === "arrayGet") keys.add("a");
|
||||
else if (e.kind === "call" && this.borrowedReturns.has(e.callee) && !seen.has(e.callee)) {
|
||||
seen.add(e.callee);
|
||||
pending.push(e.callee);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
const result = [...keys];
|
||||
this.borrowedReturnKeyCache.set(callee, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── statements ──────────────────────────────────────────────────────────
|
||||
|
||||
private emitStmts(stmts: IrStmt[]): void {
|
||||
for (const s of stmts) {
|
||||
// Statements after a terminator are unreachable (dead code after
|
||||
// return/break/continue). Skip them so no dropped SSA definition
|
||||
// can leak forward.
|
||||
if (this.B.isTerminated()) return;
|
||||
this.emitStmt(s);
|
||||
private emitStmts(stmts: IrStmt[], scoped = false): void {
|
||||
const saved = this.stmtCursor;
|
||||
const cursor = scoped ? { stmts, index: 0 } : null;
|
||||
try {
|
||||
for (let i = 0; i < stmts.length; i++) {
|
||||
// Statements after a terminator are unreachable (dead code after
|
||||
// return/break/continue). Skip them so no dropped SSA definition
|
||||
// can leak forward.
|
||||
if (this.B.isTerminated()) return;
|
||||
if (cursor) cursor.index = i;
|
||||
this.stmtCursor = cursor;
|
||||
this.emitStmt(stmts[i]!);
|
||||
}
|
||||
} finally {
|
||||
this.stmtCursor = saved;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5416,7 +5622,7 @@ export class LlEmitter {
|
||||
const scope: LlScopeEntry[] = [];
|
||||
this.scopes.push(scope);
|
||||
setup?.(scope);
|
||||
this.emitStmts(stmts);
|
||||
this.emitStmts(stmts, true);
|
||||
const ended = endsWithJump(stmts);
|
||||
this.scopes.pop();
|
||||
if (!ended) this.releaseScope(scope);
|
||||
@@ -5476,7 +5682,13 @@ export class LlEmitter {
|
||||
}
|
||||
const localRead = this.localArrayReads.get(s.localId);
|
||||
if (localRead) {
|
||||
const owner = emitLocalArrayRead(this, localRead, b.slot);
|
||||
const owner = emitLocalArrayRead(
|
||||
this,
|
||||
!localRead.borrow && this.unwrittenLocalArrayRead(s, localRead)
|
||||
? { ...localRead, borrow: true }
|
||||
: localRead,
|
||||
b.slot,
|
||||
);
|
||||
if (owner) this.scopes[this.scopes.length - 1]!.push(owner);
|
||||
break;
|
||||
}
|
||||
@@ -5565,7 +5777,7 @@ export class LlEmitter {
|
||||
b.local &&
|
||||
isRefCounted(b.type) &&
|
||||
this.stableCallBindings.has(s.localId) &&
|
||||
this.canBorrowCallArgument(s.init)
|
||||
(this.canBorrowCallArgument(s.init) || this.unwrittenAlias(s))
|
||||
) {
|
||||
const v = this.emitReadReceiver(s.init);
|
||||
B.line(`store ptr ${v.name}, ptr ${b.slot}`);
|
||||
@@ -6466,10 +6678,29 @@ export class LlEmitter {
|
||||
// the actual ret. The parked value owns a synthetic slot-backed
|
||||
// scope entry during each copy so a throwing finally releases it.
|
||||
let v: LlValue | null = null;
|
||||
if (s.value !== null && this.currentBorrowedReturn) {
|
||||
const moved = this.finallyStack.length === 0 ? this.movableReturn(s.value) : null;
|
||||
if (moved !== null) {
|
||||
// Returning an owned local hands its reference to the caller: no
|
||||
// retain now and no release of the local below.
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ptr, ptr ${moved.entry.slot}`);
|
||||
v = { name: t, type: s.value!.type };
|
||||
moved.scope.splice(moved.scope.indexOf(moved.entry), 1);
|
||||
this.releaseForJump(0, 0);
|
||||
moved.scope.splice(moved.index, 0, moved.entry);
|
||||
} else if (s.value !== null && this.currentBorrowedReturn) {
|
||||
// A borrowed-return body hands back its walk without a reference
|
||||
// (it has no try statements, so no finally can intervene).
|
||||
v = this.emitReadReceiver(s.value);
|
||||
// (it has no try statements, so no finally can intervene). An
|
||||
// optional element read lends the element its array keeps alive.
|
||||
const read = this.optionalArrayReads.get(s.value);
|
||||
if (read) {
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
emitLocalArrayRead(this, { ...read, borrow: true }, slot);
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ptr, ptr ${slot}`);
|
||||
v = { name: t, type: s.value.type };
|
||||
} else v = this.emitReadReceiver(s.value);
|
||||
} else if (s.value !== null) {
|
||||
v = this.emitExpr(s.value);
|
||||
this.moveTemp(v);
|
||||
@@ -6501,7 +6732,7 @@ export class LlEmitter {
|
||||
this.scopes = savedScopes;
|
||||
this.finallyStack = savedFinally;
|
||||
this.tryStack = savedTry;
|
||||
} else {
|
||||
} else if (moved === null) {
|
||||
this.releaseForJump(0, 0);
|
||||
}
|
||||
if (!B.isTerminated()) {
|
||||
@@ -7375,6 +7606,10 @@ export class LlEmitter {
|
||||
if (result.owner) this.ownSlot(result.owner.slot, result.owner.type);
|
||||
return result.value;
|
||||
}
|
||||
// A borrowed-return helper (such as an optional element read) lends its
|
||||
// result like any borrowed-return call.
|
||||
if (e.kind === "call" && this.borrowedReturns.has(e.callee) && this.canBorrowReceiver(e))
|
||||
return emitBorrowedResultCall(this, e);
|
||||
const arrayRead = this.optionalArrayReads.get(e);
|
||||
if (arrayRead) return emitProjectedArrayRead(this, arrayRead);
|
||||
const narrow = this.checkedNarrows.get(e);
|
||||
@@ -7698,17 +7933,20 @@ export class LlEmitter {
|
||||
const indexes = this.callLifetimes.borrowed.get(fn.name);
|
||||
return new Set(fn.params.filter((_, i) => indexes?.has(i)).map((p) => p.localId));
|
||||
};
|
||||
const hostFor = (fn: IrFunction) => {
|
||||
const hostFor = (fn: IrFunction): WalkBorrowHost => {
|
||||
const allowed = borrowedIds(fn);
|
||||
return {
|
||||
const host = {
|
||||
pointerLocal: (local: IrLocal) => pointer(local.type),
|
||||
borrowsWithoutOwning: (e: IrExpr) => this.borrowsWithoutOwning(e),
|
||||
borrowedReturn: (callee: string) => this.borrowedReturns.has(callee),
|
||||
parameterAllowed: (id: string) => allowed.has(id),
|
||||
};
|
||||
return this.referenceEffects.functions.has(fn.name) ? host : this.unwrittenWalkHost(fn, host);
|
||||
};
|
||||
const preserving = [...this.fnByName.values()].filter((fn) =>
|
||||
this.referenceEffects.functions.has(fn.name),
|
||||
// With ReferenceWrites, a body that writes none of its walks' locations
|
||||
// qualifies like one that preserves every edge.
|
||||
const preserving = [...this.fnByName.values()].filter(
|
||||
(fn) => this.referenceEffects.functions.has(fn.name) || this.referenceWrites !== null,
|
||||
);
|
||||
const candidates = preserving.filter((fn) => {
|
||||
if (!pointer(fn.returnType) || fn.async || fn.generator || fn.captures) return false;
|
||||
@@ -7728,6 +7966,8 @@ export class LlEmitter {
|
||||
if (this.borrowedReturns.has(fn.name)) continue;
|
||||
if (!analyzeWalks(fn, hostFor(fn)).returnsWalk) continue;
|
||||
this.borrowedReturns.add(fn.name);
|
||||
// Nested borrowed returns extend the locations a caller depends on.
|
||||
this.borrowedReturnKeyCache.clear();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
@@ -7752,6 +7992,14 @@ export class LlEmitter {
|
||||
return this.nullableFieldGet(e) === null;
|
||||
case "recordGet":
|
||||
return true;
|
||||
// emitReadReceiver lends a required element of a borrowed array when
|
||||
// computing the index removes no edge.
|
||||
case "arrayGet":
|
||||
return (
|
||||
e.arr.type.kind === "array" &&
|
||||
this.optionalArrayReads.get(e) === null &&
|
||||
this.referenceEffects.preserves(e.index)
|
||||
);
|
||||
case "unionNarrow":
|
||||
return this.nullableUnions.has(e.unionId);
|
||||
case "downcast":
|
||||
|
||||
@@ -25,7 +25,13 @@ import { f64Lit, ffiNativeTypeLl, ffiNativeParamLl, ffiNativeReturnLl } from "./
|
||||
import { canStackUnion, emitStackUnion } from "./stack-unions.js";
|
||||
import { emitCallArrayRead } from "./local-array-reads.js";
|
||||
import { emitStackMapRead } from "./map-read-lifetimes.js";
|
||||
import { borrowableInputs, emitBorrowedInput, emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
import {
|
||||
borrowableInputs,
|
||||
emitBorrowedInput,
|
||||
emitBorrowedInputs,
|
||||
inputSuffixWrites,
|
||||
} from "./borrowed-inputs.js";
|
||||
import type { Writes } from "./reference-writes.js";
|
||||
|
||||
export function emitCallExpr(
|
||||
host: LlvmEmitterContext,
|
||||
@@ -58,12 +64,18 @@ export function emitCallExpr(
|
||||
const borrowed = host.callLifetimes.borrowed.get(e.callee);
|
||||
const projected = host.callLifetimes.parameters.get(e.callee);
|
||||
const inputs = borrowed
|
||||
? borrowableInputs(host, e.args, host.referenceEffects.functions.has(e.callee))
|
||||
? borrowableInputs(
|
||||
host,
|
||||
e.args,
|
||||
host.referenceEffects.functions.has(e.callee),
|
||||
host.referenceWrites?.summary(e.callee),
|
||||
)
|
||||
: [];
|
||||
// Borrowed snapshots live through this call, not through the whole
|
||||
// enclosing expression. Otherwise a large initializer accumulates
|
||||
// owners and repeats their cleanup at every later throwing call.
|
||||
if (borrowed) host.frames.push([]);
|
||||
let suffixes: Writes[] | undefined;
|
||||
const args = e.args.map((a, index) => {
|
||||
if (borrowed?.has(index)) {
|
||||
// A heap-borrowing helper may retain or return its argument.
|
||||
@@ -73,7 +85,18 @@ export function emitCallExpr(
|
||||
const callback = host.stackCallbacks.emit(host, a);
|
||||
if (callback) return callback;
|
||||
const read = host.callArrayReads.get(a);
|
||||
if (read) return emitCallArrayRead(host, read);
|
||||
if (read) {
|
||||
suffixes ??= inputSuffixWrites(
|
||||
host,
|
||||
e.args,
|
||||
host.referenceWrites?.summary(e.callee) ?? null,
|
||||
);
|
||||
const lend =
|
||||
!read.borrow &&
|
||||
index < suffixes.length &&
|
||||
host.unwrittenCallArrayRead(read, suffixes[index]!);
|
||||
return emitCallArrayRead(host, lend ? { ...read, borrow: true } : read);
|
||||
}
|
||||
const mapRead = host.mapReadLifetimes.arguments.get(a);
|
||||
if (mapRead) {
|
||||
const result = emitStackMapRead(host, mapRead);
|
||||
@@ -739,6 +762,7 @@ export function emitCallExpr(
|
||||
host,
|
||||
e.args,
|
||||
host.referenceEffects.functions.has(ctorName),
|
||||
host.referenceWrites?.constructWrites(e.className),
|
||||
);
|
||||
host.frames.push([]);
|
||||
const args = e.args.map((a, i) => {
|
||||
@@ -951,7 +975,15 @@ export function emitCallExpr(
|
||||
// (LlEmitter.computeVirtualBorrowing): those are passed like borrowed
|
||||
// arguments of a direct call, owned snapshots living in this frame.
|
||||
const borrowed = host.virtualSlotBorrowed(meta.root.def.name, slotIdx);
|
||||
const inputs = borrowed.size > 0 ? borrowableInputs(host, e.args, false) : [];
|
||||
const inputs =
|
||||
borrowed.size > 0
|
||||
? borrowableInputs(
|
||||
host,
|
||||
e.args,
|
||||
false,
|
||||
host.referenceWrites?.virtualWrites(e.className, e.method),
|
||||
)
|
||||
: [];
|
||||
if (borrowed.size > 0) host.frames.push([]);
|
||||
const args = e.args.map((a, index) =>
|
||||
borrowed.has(index) && inputs[index] ? host.emitReadReceiver(a) : host.emitExpr(a),
|
||||
|
||||
@@ -3,18 +3,22 @@ import { everyStmtList } from "../../ir/traverse.js";
|
||||
import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
import type { ReferenceEffects } from "./reference-effects.js";
|
||||
import type { OptionalArrayReads } from "./local-array-reads.js";
|
||||
import type { ReferenceWrites } from "./reference-writes.js";
|
||||
|
||||
/** Limit a borrow to a loop whose complete iteration preserves heap edges.
|
||||
* Initialization and unrelated work elsewhere in the function may mutate.
|
||||
* The array remains owned by its unchanged local or global binding; the
|
||||
* iteration binding can only project it or forward it to proven consumers.
|
||||
* Callbacks, suspension, capture, reference writes, and escaping uses keep
|
||||
* the ordinary owned read. Missing elements retain the runtime's checks. */
|
||||
* the ordinary owned read. Missing elements retain the runtime's checks.
|
||||
* With ReferenceWrites, an iteration that writes no array element (nor the
|
||||
* binding holding the array) suffices, whatever else it writes. */
|
||||
export function findLoopArrayBorrows(
|
||||
fn: IrFunction,
|
||||
lifetimes: CallLifetimes,
|
||||
effects: ReferenceEffects,
|
||||
optionalReads?: OptionalArrayReads,
|
||||
writes?: ReferenceWrites | null,
|
||||
): ReadonlySet<IrStmt> {
|
||||
const result = new Set<IrStmt>();
|
||||
if (fn.async || fn.generator) return result;
|
||||
@@ -45,19 +49,28 @@ export function findLoopArrayBorrows(
|
||||
!local.tdz &&
|
||||
projected?.has(loop.localId) &&
|
||||
bindings?.has(loop.localId) &&
|
||||
effects.preservesScope(loop.body) &&
|
||||
effects.preserves(loop.iterable)
|
||||
((effects.preservesScope(loop.body) && effects.preserves(loop.iterable)) ||
|
||||
// The loop owns the evaluated array; only element removals can
|
||||
// release an element it lends.
|
||||
(writes && !writes.conflicts(["a"], writes.stmts(loop.body))))
|
||||
)
|
||||
result.add(loop);
|
||||
return true;
|
||||
}
|
||||
if (loop.kind !== "for" && loop.kind !== "while" && loop.kind !== "doWhile") return true;
|
||||
if (
|
||||
!effects.preservesScope(loop.body) ||
|
||||
(loop.cond && !effects.preserves(loop.cond)) ||
|
||||
(loop.kind === "for" && loop.update && !effects.preservesScope([loop.update]))
|
||||
)
|
||||
return true;
|
||||
const preserving =
|
||||
effects.preservesScope(loop.body) &&
|
||||
(!loop.cond || effects.preserves(loop.cond)) &&
|
||||
(loop.kind !== "for" || !loop.update || effects.preservesScope([loop.update]));
|
||||
const loopWrites =
|
||||
preserving || !writes
|
||||
? null
|
||||
: writes.union([
|
||||
writes.stmts(loop.body),
|
||||
loop.cond ? writes.expr(loop.cond) : null,
|
||||
loop.kind === "for" && loop.update ? writes.stmt(loop.update) : null,
|
||||
]);
|
||||
if (!preserving && !writes) return true;
|
||||
everyStmtList(loop.body, {
|
||||
expr: () => true,
|
||||
stmt: (stmt) => {
|
||||
@@ -71,7 +84,12 @@ export function findLoopArrayBorrows(
|
||||
return true;
|
||||
const source =
|
||||
stmt.init.kind === "arrayGet" ? stmt.init.arr : optionalReads?.get(stmt.init)?.array;
|
||||
if (source && stableSource(source)) result.add(stmt);
|
||||
if (!source || !stableSource(source)) return true;
|
||||
if (preserving) result.add(stmt);
|
||||
else {
|
||||
const keys = writes!.readKeys(source);
|
||||
if (keys && !writes!.conflicts(["a", ...keys], loopWrites)) result.add(stmt);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -167,18 +167,23 @@ test("string operands preserve their left-to-right snapshot when a later assignm
|
||||
expect(facts(f).parameters.has("replace")).toBe(false);
|
||||
});
|
||||
|
||||
test("a later global write keeps mutable string arguments owned", () => {
|
||||
test("a callee's global write cannot replace the caller's mutable string locals", () => {
|
||||
const source = fn("source", [], str("next"));
|
||||
source.body.unshift({ kind: "assign", localId: "%g.changed", value: str("effect"), loc });
|
||||
const compare = fn("compare", ["text"], equal(ref("text"), call("source", [], STRING)));
|
||||
// A later write anywhere in this function rules out parameter borrowing.
|
||||
// A write anywhere in this function rules out parameter borrowing, but
|
||||
// the rebound local itself owns its value across the later operand.
|
||||
compare.body.unshift({ kind: "assign", localId: "text", value: str("first"), loc });
|
||||
const module = mod(source, compare);
|
||||
module.globals = [{ id: "%g.changed", name: "changed", type: STRING, mutable: true }];
|
||||
const ir = body(module, "sc_f_compare");
|
||||
const sourceCall = ir.indexOf("@sc_f_source");
|
||||
expect(sourceCall).toBeGreaterThan(0);
|
||||
expect(ir.slice(0, sourceCall)).toContain("@scr_str_retain_v");
|
||||
expect(ir.slice(0, sourceCall)).not.toContain("@scr_str_retain_v");
|
||||
// Reading the global the later operand replaces still takes a snapshot.
|
||||
compare.body = [ret(equal(ref("%g.changed"), call("source", [], STRING)))];
|
||||
const global = body(module, "sc_bf_compare");
|
||||
expect(global.slice(0, global.indexOf("@sc_f_source"))).toContain("@scr_str_retain_v");
|
||||
});
|
||||
|
||||
test("immutable lexical owners survive arbitrary later operands", () => {
|
||||
@@ -214,24 +219,22 @@ test("reference fields borrow across preserving calls but snapshot across writes
|
||||
source.locals.push({ id: "changed", name: "changed", type: STRING, mutable: true });
|
||||
source.body.unshift({ kind: "assign", localId: "changed", value: str("effect"), loc });
|
||||
expect(body(module, "sc_bf_compare")).not.toContain("@scr_str_retain_v");
|
||||
// A global write replaces no record field: the projection still borrows.
|
||||
source.body.unshift({ kind: "assign", localId: "%g.changed", value: str("effect"), loc });
|
||||
module.globals = [{ id: "%g.changed", name: "changed", type: STRING, mutable: true }];
|
||||
// The field is never written after its record literal, so no call can
|
||||
// replace (and release) it while the holder lives.
|
||||
module.globals = [
|
||||
{ id: "%g.changed", name: "changed", type: STRING, mutable: true },
|
||||
{ id: "%g.record", name: "record", type: record, mutable: true },
|
||||
];
|
||||
expect(body(module, "sc_bf_compare")).not.toContain("@scr_str_retain_v");
|
||||
// Once the program writes the field, a reference-writing call snapshots it.
|
||||
const writer = fn("writer", [], str("written"));
|
||||
writer.params = [{ localId: "target", name: "target", type: record }];
|
||||
writer.locals = [{ id: "target", name: "target", type: record, mutable: false }];
|
||||
writer.body.unshift({
|
||||
// Writing the projected field of any record does replace it.
|
||||
source.body.unshift({
|
||||
kind: "recordSet",
|
||||
obj: ref("target", record),
|
||||
obj: ref("%g.record", record),
|
||||
shapeId: "text",
|
||||
field: "value",
|
||||
value: str("written"),
|
||||
value: str("effect"),
|
||||
loc,
|
||||
});
|
||||
module.functions.push(writer);
|
||||
expect(body(module, "sc_bf_compare")).toContain("@scr_str_retain_v");
|
||||
f.body = [ret(equal(str("constant"), projection))];
|
||||
expect(body(module, "sc_bf_compare")).not.toContain("@scr_str_retain_v");
|
||||
@@ -358,7 +361,7 @@ test.each(methodCases)(
|
||||
},
|
||||
);
|
||||
|
||||
test("string search snapshots both receiver and needle before a numeric argument writes globally", () => {
|
||||
test("string search borrows rebound locals but snapshots a global a later argument writes", () => {
|
||||
const position = fn("position", [], num(0));
|
||||
position.body.unshift({ kind: "assign", localId: "%g.changed", value: str("effect"), loc });
|
||||
const search = fn(
|
||||
@@ -377,9 +380,16 @@ test("string search snapshots both receiver and needle before a numeric argument
|
||||
const searchCall = ir.indexOf("@scr_str_index_of");
|
||||
expect(start).toBeGreaterThan(0);
|
||||
expect(searchCall).toBeGreaterThan(start);
|
||||
// Literals need no owner, but both reads take independent snapshots.
|
||||
expect(ir.slice(0, start).match(/@scr_str_retain_v/g)).toHaveLength(2);
|
||||
expect(ir.slice(searchCall)).toContain("@scr_str_release");
|
||||
// The rebound locals own their values across the call: no snapshots.
|
||||
expect(ir.slice(0, start)).not.toContain("@scr_str_retain_v");
|
||||
// A receiver read from the global the numeric argument replaces does.
|
||||
search.body = [
|
||||
ret(intrinsic(ref("%g.changed"), "indexOf", [ref("needle"), call("position", [], F64)], F64)),
|
||||
];
|
||||
const global = body(module, "sc_bf_search");
|
||||
const call2 = global.indexOf("@sc_f_position");
|
||||
expect(global.slice(0, call2).match(/@scr_str_retain_v/g)).toHaveLength(1);
|
||||
expect(global.slice(global.indexOf("@scr_str_index_of"))).toContain("@scr_str_release");
|
||||
});
|
||||
|
||||
test("global string ownership cannot be inferred from an immutable-looking reference", () => {
|
||||
|
||||
@@ -17,6 +17,11 @@ export interface WalkBorrowHost {
|
||||
/** Parameters that may become candidates; once calling conventions are
|
||||
* fixed, only borrowed parameters qualify. Defaults to every parameter. */
|
||||
parameterAllowed?(localId: string): boolean;
|
||||
/** For bodies that may write the heap: whether a walk only reads
|
||||
* locations the whole body never writes, given the current candidates
|
||||
* (whose own rebinding releases nothing). Absent: the body preserves
|
||||
* every edge. */
|
||||
unwritten?(value: IrExpr, candidates: ReadonlySet<string>): boolean;
|
||||
}
|
||||
|
||||
export interface WalkFacts {
|
||||
@@ -34,7 +39,9 @@ export interface WalkFacts {
|
||||
* calling convention.
|
||||
*
|
||||
* Soundness rests on the whole body preserving heap edges (the caller
|
||||
* passes only functions in ReferenceEffects.functions): neither the body
|
||||
* passes only functions in ReferenceEffects.functions, or supplies
|
||||
* `unwritten` to admit only walks over locations the body never writes;
|
||||
* the argument below then applies to those locations): neither the body
|
||||
* nor anything it calls removes a field, element or global reference, so
|
||||
* every object reachable from a parameter at entry stays reachable (and
|
||||
* alive) until the function returns. Such a local therefore needs no
|
||||
@@ -133,6 +140,10 @@ export function analyzeWalks(fn: IrFunction, host: WalkBorrowHost): WalkFacts {
|
||||
case "fieldGet":
|
||||
case "recordGet":
|
||||
return host.borrowsWithoutOwning(e) && walk(e.obj);
|
||||
// A required element of a walked array (the read still traps when the
|
||||
// element is missing).
|
||||
case "arrayGet":
|
||||
return host.borrowsWithoutOwning(e) && walk(e.arr);
|
||||
case "unionNarrow":
|
||||
case "downcast":
|
||||
case "upcast":
|
||||
@@ -155,12 +166,20 @@ export function analyzeWalks(fn: IrFunction, host: WalkBorrowHost): WalkFacts {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const walkOrThrow = (e: IrExpr): boolean =>
|
||||
(e.kind === "libCall" && e.fn === "error.nodeThrow") || walk(e);
|
||||
const isThrow = (e: IrExpr): boolean => e.kind === "libCall" && e.fn === "error.nodeThrow";
|
||||
const walkOrThrow = (e: IrExpr): boolean => isThrow(e) || walk(e);
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const id of candidates) {
|
||||
if (definitions.get(id)!.every((value) => value === null || walk(value))) continue;
|
||||
if (
|
||||
definitions
|
||||
.get(id)!
|
||||
.every(
|
||||
(value) =>
|
||||
value === null || (walk(value) && host.unwritten?.(value, candidates) !== false),
|
||||
)
|
||||
)
|
||||
continue;
|
||||
candidates.delete(id);
|
||||
changed = true;
|
||||
}
|
||||
@@ -169,6 +188,11 @@ export function analyzeWalks(fn: IrFunction, host: WalkBorrowHost): WalkFacts {
|
||||
const returnsWalk =
|
||||
!guarded &&
|
||||
returns.length > 0 &&
|
||||
returns.every((value) => value !== null && walkOrThrow(value));
|
||||
returns.every(
|
||||
(value) =>
|
||||
value !== null &&
|
||||
walkOrThrow(value) &&
|
||||
(isThrow(value) || host.unwritten?.(value, candidates) !== false),
|
||||
);
|
||||
return { locals: result, returnsWalk };
|
||||
}
|
||||
|
||||
@@ -329,11 +329,15 @@ console.log(alias(new Item()).value, snapshot(new Item()).value);
|
||||
// The rebound parameter keeps borrowing (its new value lives in an owner
|
||||
// slot), but the alias saved before the rebinding must own its value.
|
||||
// `new Item()` calls the borrowed constructor body, so no retain transfers
|
||||
// the fresh instance into it: the saved alias and the returned value.
|
||||
// the fresh instance into it. The saved alias retains once, and `return
|
||||
// saved` moves that owned scope entry into the result instead of retaining
|
||||
// it again and releasing the local at scope exit: the only releases are the
|
||||
// rebound parameter's owner slot (before the store and at exit).
|
||||
const snapshot = body(llvm, "sc_bf_snapshot");
|
||||
expect(snapshot.match(/@sc_retain_Item/g)).toHaveLength(2);
|
||||
expect(snapshot.match(/@sc_retain_Item/g)).toHaveLength(1);
|
||||
expect(snapshot).toContain("@sc_bf__x25_Item_constructor");
|
||||
expect(snapshot).toContain("@sc_release_Item");
|
||||
expect(snapshot.match(/@sc_release_Item/g)).toHaveLength(2);
|
||||
expect(snapshot).toMatch(/(%t\d+) = load ptr, ptr %sc_l_saved_0\n[^]*ret ptr \1\n/);
|
||||
});
|
||||
|
||||
test("unchanged captured parameters are boxed when a closure first needs them", async () => {
|
||||
|
||||
@@ -217,15 +217,39 @@ test("a throwing later source argument releases the retained snapshot on its exc
|
||||
const module = await lower(`
|
||||
class Item { value = 7; }
|
||||
function read(item: Item, ignored: number): number { return item.value + ignored; }
|
||||
function fail(items: Item[]): number { items.pop(); throw new Error("failed"); }
|
||||
function work(items: Item[]): number { return read(items[0], fail(items)); }
|
||||
try { console.log(work([new Item()])); } catch {}
|
||||
`);
|
||||
// The later argument removes the element, so the snapshot must own it.
|
||||
const work = body(emitLlvmModule(module), "sc_bf_work");
|
||||
const failure = work.indexOf("@sc_bf_fail");
|
||||
expect(failure).toBeGreaterThan(0);
|
||||
expect(work.slice(0, failure)).toContain("@sc_retain_Item");
|
||||
expect(work.slice(failure)).toMatch(PENDING_CHECK);
|
||||
const exceptionEdge = work.slice(failure, work.indexOf("@sc_bf_read"));
|
||||
expect(exceptionEdge).toContain("@sc_release_Item");
|
||||
expect(work).not.toContain("@scr_union_release");
|
||||
});
|
||||
|
||||
test("a throwing later source argument that writes no element leaves the snapshot borrowed", async () => {
|
||||
const module = await lower(`
|
||||
class Item { value = 7; }
|
||||
function read(item: Item, ignored: number): number { return item.value + ignored; }
|
||||
function fail(): number { throw new Error("failed"); }
|
||||
function work(items: Item[]): number { return read(items[0], fail()); }
|
||||
try { console.log(work([new Item()])); } catch {}
|
||||
`);
|
||||
// Neither the callee nor the later argument writes an array element (the
|
||||
// Error constructor's stores into its own fresh object do not count), so
|
||||
// the borrowed parameter's array keeps the element alive and no edge,
|
||||
// normal or exceptional, owes a release.
|
||||
const work = body(emitLlvmModule(module), "sc_bf_work");
|
||||
const failure = work.indexOf("@sc_f_fail");
|
||||
expect(failure).toBeGreaterThan(0);
|
||||
expect(work.slice(failure)).toMatch(PENDING_CHECK);
|
||||
expect(work.slice(failure)).toContain("@sc_release_Item");
|
||||
expect(work).not.toContain("@sc_retain_Item");
|
||||
expect(work).not.toContain("@sc_release_Item");
|
||||
expect(work).not.toContain("@scr_union_release");
|
||||
});
|
||||
|
||||
|
||||
@@ -10493,24 +10493,6 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/missing-read-lambda-results.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/missing-read-lambda-results.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/missing-read-narrowed-storage.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/missing-read-narrowed-storage.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/missing-read-number-conversions.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/missing-read-number-conversions.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/module-fallback-functions/main.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/module-fallback-functions/platform.js",
|
||||
@@ -15797,6 +15779,54 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4630-reference-write-call-arguments.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4630-reference-write-call-arguments.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4631-reference-write-aliases.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4631-reference-write-aliases.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4632-reference-write-loops-and-walks.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4632-reference-write-loops-and-walks.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4633-reference-write-callbacks-returns.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4633-reference-write-callbacks-returns.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/4634-optional-element-borrowed-returns.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4634-optional-element-borrowed-returns.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/missing-read-lambda-results.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/missing-read-lambda-results.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/missing-read-narrowed-storage.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/missing-read-narrowed-storage.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/missing-read-number-conversions.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/missing-read-number-conversions.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/native-worker-arena-reuse.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/native-worker-arena-reuse.ts"
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
// Field reads and rebound locals passed to borrowing callees: borrowed only
|
||||
// when neither the callee nor a later argument can replace them. Each
|
||||
// "replaces" case makes the field the value's last owner, frees it inside
|
||||
// the call, churns the allocator and then reads the argument again.
|
||||
|
||||
class Name {
|
||||
text: string;
|
||||
parts: string[];
|
||||
constructor(text: string) {
|
||||
this.text = text;
|
||||
this.parts = text.split("");
|
||||
}
|
||||
}
|
||||
|
||||
class Holder {
|
||||
name: Name | undefined;
|
||||
other: Name | undefined = undefined;
|
||||
items: Name[] = [];
|
||||
constructor(name: Name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
function churn(): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < 200; i++) total += new Name("churn-" + i).parts.length;
|
||||
return total;
|
||||
}
|
||||
|
||||
function describe(n: Name | undefined): string {
|
||||
if (n === undefined) return "none";
|
||||
return n.text + ":" + n.parts.length;
|
||||
}
|
||||
|
||||
// Writes an unrelated field: the caller may lend `h.name`.
|
||||
function touchOther(h: Holder, n: Name | undefined): string {
|
||||
h.other = new Name("other");
|
||||
churn();
|
||||
return describe(n);
|
||||
}
|
||||
|
||||
// Replaces the very field the caller passed: the argument must be owned.
|
||||
function replaceName(h: Holder, n: Name | undefined): string {
|
||||
h.name = new Name("replacement");
|
||||
churn();
|
||||
return describe(n);
|
||||
}
|
||||
|
||||
// Removes array elements: an element argument must be owned.
|
||||
function dropItems(h: Holder, n: Name): string {
|
||||
h.items.length = 0;
|
||||
churn();
|
||||
return describe(n);
|
||||
}
|
||||
|
||||
function compose(a: string, b: string): string {
|
||||
return a + "|" + b;
|
||||
}
|
||||
|
||||
function later(h: Holder): string {
|
||||
h.name = new Name("later");
|
||||
churn();
|
||||
return "later";
|
||||
}
|
||||
|
||||
// A later argument replaces the field an earlier argument read.
|
||||
function laterArgument(h: Holder): string {
|
||||
return compose(describe2(h.name), later(h));
|
||||
}
|
||||
function describe2(n: Name | undefined): string {
|
||||
churn();
|
||||
return describe(n);
|
||||
}
|
||||
|
||||
// A rebound parameter passed along: no callee can rebind the caller's local.
|
||||
function rebound(n: Name, h: Holder): string {
|
||||
if (n.text.length > 3) n = h.name ?? n;
|
||||
return replaceAndDescribe(n, h) + " " + describe(n);
|
||||
}
|
||||
function replaceAndDescribe(n: Name, h: Holder): string {
|
||||
h.name = undefined;
|
||||
churn();
|
||||
return describe(n);
|
||||
}
|
||||
|
||||
// A constructor's stores into its own fresh object replace nothing the
|
||||
// caller lends; a constructor storing into another object does.
|
||||
class Attach {
|
||||
label: Name;
|
||||
constructor(h: Holder, label: Name) {
|
||||
h.name = undefined;
|
||||
churn();
|
||||
this.label = new Name("own-" + label.text);
|
||||
}
|
||||
}
|
||||
function construct(h: Holder): string {
|
||||
const a = new Attach(h, h.name!);
|
||||
return a.label.text;
|
||||
}
|
||||
|
||||
const h1 = new Holder(new Name("first"));
|
||||
console.log(touchOther(h1, h1.name));
|
||||
const h2 = new Holder(new Name("second"));
|
||||
console.log(replaceName(h2, h2.name), describe(h2.name));
|
||||
const h3 = new Holder(new Name("third"));
|
||||
h3.items.push(new Name("item0"), new Name("item1"));
|
||||
console.log(dropItems(h3, h3.items[1]!));
|
||||
const h4 = new Holder(new Name("fourth"));
|
||||
console.log(laterArgument(h4), describe(h4.name));
|
||||
const h5 = new Holder(new Name("fifth-name"));
|
||||
console.log(rebound(new Name("sixth"), h5));
|
||||
const h6 = new Holder(new Name("seventh"));
|
||||
console.log(construct(h6), describe(h6.name));
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const h = new Holder(new Name("loop" + i));
|
||||
sum += touchOther(h, h.name).length + replaceName(h, h.name).length;
|
||||
}
|
||||
console.log("sum", sum);
|
||||
@@ -0,0 +1,116 @@
|
||||
// `const t = o.f` borrows the field only when the rest of its block writes
|
||||
// none of the locations keeping the value alive; returning or storing the
|
||||
// alias still takes its own reference.
|
||||
|
||||
class Leaf {
|
||||
text: string;
|
||||
constructor(text: string) {
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
class Box {
|
||||
leaf: Leaf;
|
||||
spare: Leaf | undefined = undefined;
|
||||
list: Leaf[] = [];
|
||||
constructor(leaf: Leaf) {
|
||||
this.leaf = leaf;
|
||||
}
|
||||
}
|
||||
|
||||
function churn(): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < 200; i++) total += new Leaf("x" + i).text.length;
|
||||
return total;
|
||||
}
|
||||
|
||||
// No write to `leaf` in the block: borrowed.
|
||||
function readOnly(b: Box): string {
|
||||
const t = b.leaf;
|
||||
b.spare = new Leaf("spare");
|
||||
churn();
|
||||
return t.text + "/" + b.spare.text;
|
||||
}
|
||||
|
||||
// The block replaces the field: the alias must own its value.
|
||||
function replaced(b: Box): string {
|
||||
const t = b.leaf;
|
||||
b.leaf = new Leaf("new");
|
||||
churn();
|
||||
return t.text + "->" + b.leaf.text;
|
||||
}
|
||||
|
||||
// The replacement happens in a callee.
|
||||
function swap(b: Box): void {
|
||||
b.leaf = new Leaf("swapped");
|
||||
churn();
|
||||
}
|
||||
function replacedInCallee(b: Box): string {
|
||||
const t = b.leaf;
|
||||
swap(b);
|
||||
return t.text;
|
||||
}
|
||||
|
||||
// Element aliases and removals.
|
||||
function element(b: Box): string {
|
||||
const first = b.list[0]!;
|
||||
b.list.pop();
|
||||
b.list.length = 0;
|
||||
churn();
|
||||
return first.text;
|
||||
}
|
||||
|
||||
// The alias escapes into a store and a return value.
|
||||
function escape(b: Box, other: Box): Leaf {
|
||||
const t = b.leaf;
|
||||
other.spare = t;
|
||||
b.leaf = new Leaf("gone");
|
||||
churn();
|
||||
return t;
|
||||
}
|
||||
|
||||
// Aliases in loop bodies are fresh per iteration; `continue` ends one.
|
||||
function loop(boxes: Box[]): string {
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < boxes.length; i++) {
|
||||
const t = boxes[i]!.leaf;
|
||||
if (i % 2 === 0) {
|
||||
out.push(t.text);
|
||||
continue;
|
||||
}
|
||||
boxes[i]!.leaf = new Leaf("odd" + i);
|
||||
churn();
|
||||
out.push(t.text);
|
||||
}
|
||||
return out.join(",");
|
||||
}
|
||||
|
||||
// A switch case's bindings live for the whole switch.
|
||||
function sw(b: Box, k: number): string {
|
||||
let s = "";
|
||||
switch (k) {
|
||||
case 0: {
|
||||
const t = b.leaf;
|
||||
b.leaf = new Leaf("case0");
|
||||
churn();
|
||||
s = t.text;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
s = b.leaf.text;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
console.log(readOnly(new Box(new Leaf("ro"))));
|
||||
console.log(replaced(new Box(new Leaf("old"))));
|
||||
console.log(replacedInCallee(new Box(new Leaf("callee"))));
|
||||
const withList = new Box(new Leaf("l"));
|
||||
withList.list.push(new Leaf("e0"), new Leaf("e1"));
|
||||
console.log(element(withList));
|
||||
const other = new Box(new Leaf("other"));
|
||||
console.log(escape(new Box(new Leaf("escaped")), other).text, other.spare?.text);
|
||||
const boxes: Box[] = [];
|
||||
for (let i = 0; i < 6; i++) boxes.push(new Box(new Leaf("b" + i)));
|
||||
console.log(loop(boxes), boxes.map((b) => b.leaf.text).join(" "));
|
||||
console.log(sw(new Box(new Leaf("sw0")), 0), sw(new Box(new Leaf("sw1")), 1));
|
||||
@@ -0,0 +1,116 @@
|
||||
// Loop element borrows, optional element reads, parent walks and borrowed
|
||||
// returns in bodies that write the heap: borrowed only while nothing the
|
||||
// body (or a callee) writes can release the lent value.
|
||||
|
||||
class Item {
|
||||
name: string;
|
||||
parent: Item | undefined;
|
||||
note: string | undefined = undefined;
|
||||
constructor(name: string, parent: Item | undefined) {
|
||||
this.name = name;
|
||||
this.parent = parent;
|
||||
}
|
||||
}
|
||||
|
||||
function churn(): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < 100; i++) total += new Item("c" + i, undefined).name.length;
|
||||
return total;
|
||||
}
|
||||
|
||||
// The body writes another field of each element: elements stay lent.
|
||||
function annotate(items: Item[]): string {
|
||||
const names: string[] = [];
|
||||
for (const it of items) {
|
||||
it.note = "seen:" + it.name;
|
||||
names.push(it.name);
|
||||
}
|
||||
return names.join(",");
|
||||
}
|
||||
|
||||
// The body removes elements from the iterated array: elements are owned.
|
||||
function drain(items: Item[]): string {
|
||||
const names: string[] = [];
|
||||
for (const it of items) {
|
||||
items.length = 0;
|
||||
churn();
|
||||
names.push(it.name);
|
||||
}
|
||||
return names.join(",");
|
||||
}
|
||||
|
||||
// Indexed loop with an optional element read and an unrelated write.
|
||||
function indexed(items: Item[], log: string[]): string {
|
||||
let s = "";
|
||||
for (let i = 0; i < items.length + 1; i++) {
|
||||
const it = items[i];
|
||||
log.push("i" + i);
|
||||
s += it === undefined ? "-" : it.name;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// Indexed loop replacing the element it just read.
|
||||
function replaceEach(items: Item[]): string {
|
||||
let s = "";
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const it = items[i];
|
||||
items[i] = new Item("r" + i, undefined);
|
||||
churn();
|
||||
s += it === undefined ? "-" : it.name;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// A parent walk in a body that writes notes: the walk borrows.
|
||||
function rootWithNotes(it: Item): string {
|
||||
let p = it;
|
||||
while (p.parent !== undefined) {
|
||||
p.note = "walked";
|
||||
p = p.parent;
|
||||
}
|
||||
return p.name;
|
||||
}
|
||||
|
||||
// A walk whose body detaches parents: the walk owns its steps.
|
||||
function detach(it: Item): string {
|
||||
let p = it;
|
||||
let s = "";
|
||||
while (p.parent !== undefined) {
|
||||
const next = p.parent;
|
||||
p.parent = undefined;
|
||||
churn();
|
||||
s += next.name;
|
||||
p = next;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// A borrowed return from a body that writes elsewhere, consumed after a
|
||||
// call that writes notes.
|
||||
function topOf(it: Item, log: string[]): Item {
|
||||
log.push("top");
|
||||
let p = it;
|
||||
while (p.parent !== undefined) p = p.parent;
|
||||
return p;
|
||||
}
|
||||
function useTop(it: Item, log: string[]): string {
|
||||
const top = topOf(it, log);
|
||||
annotate([it]);
|
||||
return top.name + ":" + (it.note ?? "");
|
||||
}
|
||||
|
||||
const chain: Item[] = [];
|
||||
let prev: Item | undefined = undefined;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
prev = new Item("n" + i, prev);
|
||||
chain.push(prev);
|
||||
}
|
||||
const log: string[] = [];
|
||||
console.log(annotate(chain.slice()), chain[3]!.note);
|
||||
console.log(drain(chain.slice(0, 4)));
|
||||
console.log(indexed(chain.slice(0, 3), log), log.length);
|
||||
console.log(replaceEach(chain.slice(0, 3)));
|
||||
console.log(rootWithNotes(chain[7]!), chain[4]!.note);
|
||||
console.log(useTop(chain[6]!, log), log.join(""));
|
||||
console.log(detach(chain[7]!), chain[7]!.parent, chain[2]!.parent);
|
||||
@@ -0,0 +1,98 @@
|
||||
// Closure calls resolved through callback parameters, adapters and
|
||||
// captures, and owned locals moved into return values.
|
||||
|
||||
class Cell {
|
||||
value: string;
|
||||
next: Cell | undefined = undefined;
|
||||
constructor(value: string) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
class Store {
|
||||
cell: Cell;
|
||||
constructor(cell: Cell) {
|
||||
this.cell = cell;
|
||||
}
|
||||
}
|
||||
|
||||
function churn(): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < 100; i++) n += new Cell("k" + i).value.length;
|
||||
return n;
|
||||
}
|
||||
|
||||
function show(c: Cell | undefined): string {
|
||||
return c === undefined ? "none" : c.value;
|
||||
}
|
||||
|
||||
// The callback a caller passes replaces the field the caller lends.
|
||||
function withCallback(s: Store, c: Cell, f: (s: Store) => void): string {
|
||||
f(s);
|
||||
churn();
|
||||
return show(c);
|
||||
}
|
||||
function each(cells: Cell[], f: (c: Cell) => string): string {
|
||||
return cells.map(f).join(",");
|
||||
}
|
||||
|
||||
// Captured state written by the callback.
|
||||
function capture(s: Store): string {
|
||||
let seen = s.cell;
|
||||
const record = (c: Cell): string => {
|
||||
seen = c;
|
||||
return c.value;
|
||||
};
|
||||
const listed = each([new Cell("a"), new Cell("b")], record);
|
||||
return listed + " " + seen.value;
|
||||
}
|
||||
|
||||
// Returning owned locals: plain, from a loop, and through a finally.
|
||||
function build(n: number): Cell {
|
||||
const head = new Cell("head" + n);
|
||||
let cur = head;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const c = new Cell("c" + i);
|
||||
cur.next = c;
|
||||
cur = c;
|
||||
}
|
||||
return head;
|
||||
}
|
||||
function pick(cells: Cell[], want: string): Cell | undefined {
|
||||
for (const c of cells) {
|
||||
const found = c;
|
||||
if (found.value === want) return found;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
function guarded(n: number): Cell {
|
||||
const c = new Cell("guarded" + n);
|
||||
try {
|
||||
if (n > 1) return c;
|
||||
return new Cell("small");
|
||||
} finally {
|
||||
c.value += "!";
|
||||
}
|
||||
}
|
||||
|
||||
const s1 = new Store(new Cell("one"));
|
||||
console.log(
|
||||
withCallback(s1, s1.cell, (s) => {
|
||||
s.cell = new Cell("replaced");
|
||||
}),
|
||||
s1.cell.value,
|
||||
);
|
||||
const s2 = new Store(new Cell("two"));
|
||||
console.log(withCallback(s2, s2.cell, () => {}), s2.cell.value);
|
||||
console.log(capture(new Store(new Cell("start"))));
|
||||
let walk: Cell | undefined = build(3);
|
||||
const parts: string[] = [];
|
||||
while (walk !== undefined) {
|
||||
parts.push(walk.value);
|
||||
walk = walk.next;
|
||||
}
|
||||
console.log(parts.join(">"));
|
||||
const cells = [new Cell("x"), new Cell("y"), new Cell("z")];
|
||||
console.log(show(pick(cells, "y")), show(pick(cells, "w")));
|
||||
console.log(guarded(2).value, guarded(0).value);
|
||||
console.log(each(cells, (c) => c.value.toUpperCase()));
|
||||
@@ -0,0 +1,47 @@
|
||||
// Optional element reads (`arr[i]` of `T | undefined` elements) lower to
|
||||
// helpers that now lend the element their array keeps alive: paged lookups
|
||||
// borrow, and reads followed by removals still own what they keep.
|
||||
|
||||
class Link {
|
||||
name: string;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
class Pages {
|
||||
pages: (Link[] | undefined)[] = [];
|
||||
get(id: number): Link {
|
||||
let link = this.pages[id >> 3]?.[id & 7];
|
||||
if (link === undefined) {
|
||||
let page = this.pages[id >> 3];
|
||||
if (page === undefined) {
|
||||
page = [];
|
||||
this.pages[id >> 3] = page;
|
||||
}
|
||||
link = new Link("l" + id);
|
||||
page[id & 7] = link;
|
||||
}
|
||||
return link;
|
||||
}
|
||||
}
|
||||
|
||||
function first(links: Link[]): string {
|
||||
const head = links[0];
|
||||
return head === undefined ? "none" : head.name;
|
||||
}
|
||||
|
||||
const shared: Link[] = [new Link("x"), new Link("y")];
|
||||
function readThenClear(): string {
|
||||
const kept = shared[1];
|
||||
shared.length = 0;
|
||||
let churn = 0;
|
||||
for (let i = 0; i < 100; i++) churn += new Link("c" + i).name.length;
|
||||
return (kept === undefined ? "-" : kept.name) + churn;
|
||||
}
|
||||
|
||||
const pages = new Pages();
|
||||
const seen: string[] = [];
|
||||
for (let i = 0; i < 40; i++) seen.push(pages.get((i * 7) % 19).name);
|
||||
console.log(seen.join(" "));
|
||||
console.log(first([]), first([new Link("a")]), readThenClear(), shared.length);
|
||||
Reference in New Issue
Block a user