mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
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:
@@ -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
@@ -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
@@ -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());
|
||||
|
||||
+20
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user