Keep captured bindings and bound receivers in per-site clones

Two cases where a per-site clone lost writes, so later reads of the
written objects folded to their initial literals:

- A function walked in an enclosing clone's or receiver's context is
  no longer cloned per site. The clone was its own context root, so the
  variables it captures resolved to the default context's bindings, and
  its writes through them reached no object.
- A clone's receiver now receives what the function's own receiver
  does, such as the object `bind` supplies, and an arrow's clone keeps
  the arrow's lexical `this`. Before, a bound function called at module
  initialization wrote through a fresh, empty receiver.

The static-tables case writes at initialization through a helper's
captured binding and through a bound function. It prints Node's `1,1`
(it printed `0,0`), and the count of constant reads stays at 8. zod3's
report keeps its 180 constant objects and 360 foldable reads.
This commit is contained in:
Chris Tate
2026-10-25 10:23:48 -05:00
parent df02c72407
commit e87dfe9398
4 changed files with 47 additions and 5 deletions
@@ -1012,7 +1012,14 @@ class Analyzer {
// ---- functions ---------------------------------------------------------------
functionInfo(node: Node, ownerClass: Node | null, ctxArg = ""): FunctionInfo {
/** `lexicalThis`: an arrow's receiver node, for a clone walked away from
* the function that encloses the arrow. */
functionInfo(
node: Node,
ownerClass: Node | null,
ctxArg = "",
lexicalThis: number | null = null,
): FunctionInfo {
// A nested closure walked inside a clone shares the clone's context.
const inherit =
ctxArg === "" &&
@@ -1052,7 +1059,12 @@ class Analyzer {
label: ctx === "" ? label : `${label} [${this.ctxLabel(ctx)}]`,
params,
rest,
thisNode: isArrow && this.currentFn !== null ? this.currentFn.thisNode : this.newNode(),
thisNode:
isArrow && lexicalThis !== null
? lexicalThis
: isArrow && this.currentFn !== null
? this.currentFn.thisNode
: this.newNode(),
ret: this.newNode(),
isArrow,
isAsync: mods.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword),
@@ -1363,6 +1375,11 @@ class Analyzer {
private siteClone(info: FunctionInfo, site: Node, args: number[]): FunctionInfo | null {
if (SITE_CLONES === 0 || info.isGenerator || ts.isSourceFile(info.node)) return null;
if (ts.isClassLikeDeclaration(info.node)) return null;
// A function walked in an enclosing clone's or receiver's context reads
// and writes that context's bindings of the variables it captures. A
// clone is its own context root, so it would resolve them in the
// default context instead, and their writes would be lost.
if (info.root !== null && info.root !== info.node) return null;
const sc = this.argsFrom.get(args);
if (sc === undefined || sc.inLoop || this.forceLoop || this.repeatingSites.has(site))
return null;
@@ -1382,7 +1399,12 @@ class Analyzer {
const count = this.siteClones.get(info.node) ?? 0;
if (count >= SITE_CLONES) return null;
this.siteClones.set(info.node, count + 1);
return this.functionInfo(info.node, info.ownerClass, ctx);
// The clone keeps the function's receiver: an arrow's lexical `this`,
// and whatever reaches a function's own `this`, such as the receiver
// `bind` supplies.
const clone = this.functionInfo(info.node, info.ownerClass, ctx, info.thisNode);
if (!clone.isArrow) this.edge(info.thisNode, clone.thisNode);
return clone;
}
private readonly factories = new Map<Node, boolean>();
+1 -1
View File
@@ -14,7 +14,7 @@ lib.register("b", function () {
});
console.log(lib.dispatch("b"), lib.limit());
lib.bump();
console.log(lib.limit(), new lib.Pair(2, 3).sum());
console.log(lib.limit(), new lib.Pair(2, 3).sum(), lib.inits());
try {
lib.apply("missing", 1, 2);
} catch (e) {
+1 -1
View File
@@ -13,4 +13,4 @@ console.log(lib.read("get"), lib.dispatch("a"), lib.pick(1));
lib.register("b", () => "b!");
console.log(lib.dispatch("b"), lib.limit());
lib.bump();
console.log(lib.limit(), new lib.Pair(2, 3).sum());
console.log(lib.limit(), new lib.Pair(2, 3).sum(), lib.inits());
@@ -71,6 +71,26 @@ lib.limit = function () {
return lib.Limits.MAX;
};
// Objects written at init through a helper's captured binding, and through
// the receiver of a bound function.
function makeState() {
var state = { v: 0 };
function set() {
state.v = 1;
}
set();
return state;
}
lib.made = makeState();
lib.bound = { v: 0 };
var setBound = function () {
this.v = 1;
}.bind(lib.bound);
setBound();
lib.inits = function () {
return lib.made.v + "," + lib.bound.v;
};
// A constructor function with prototype methods.
lib.Pair = function (a, b) {
this.a = a;