Leave factories that declare a class out of per-site clones

A class's constructor and methods are analyzed in receiver contexts
rooted at each member. Inside a per-site clone of the function that
declares the class, they resolved that function's bindings in the
default context: their writes through captured variables were lost,
and reads of the written objects folded to stale literals. A function
with a class declaration or expression anywhere inside is no longer
cloned per site.

The static-tables case adds a factory whose class method writes the
factory's object at initialization. It prints Node's `1,1,1` (it
printed `1,1,0`), and the count of constant reads stays at 8. zod3's
report is unchanged: 180 constant objects and 360 foldable reads.
This commit is contained in:
Chris Tate
2026-10-25 11:38:58 -05:00
parent 6aa70ff3a6
commit 54899bcc10
2 changed files with 31 additions and 1 deletions
@@ -1441,6 +1441,10 @@ class Analyzer {
// 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;
// A class's constructor and methods run in receiver contexts of their
// own, rooted at each member: inside a clone they too would resolve the
// function's bindings in the default context.
if (this.declaresClass(info.node)) return null;
const sc = this.argsFrom.get(args);
if (sc === undefined || sc.inLoop || this.forceLoop || this.repeatingSites.has(site))
return null;
@@ -1468,6 +1472,19 @@ class Analyzer {
return clone;
}
private readonly classDeclarers = new Map<Node, boolean>();
/** Whether a class declaration or expression is anywhere inside `node`. */
private declaresClass(node: Node): boolean {
let r = this.classDeclarers.get(node);
if (r === undefined) {
const visit = (n: Node): true | undefined =>
ts.isClassLikeDeclaration(n) ? true : ts.forEachChild(n, visit);
r = ts.forEachChild(node, visit) === true;
this.classDeclarers.set(node, r);
}
return r;
}
private readonly factories = new Map<Node, boolean>();
isFactory(node: Node): boolean {
let r = this.factories.get(node);
+14 -1
View File
@@ -87,8 +87,21 @@ var setBound = function () {
this.v = 1;
}.bind(lib.bound);
setBound();
// A factory that declares a class whose methods write the factory's
// bindings.
function makeTally() {
var tally = { n: 0 };
class Tally {
bump() {
tally.n = 1;
}
}
new Tally().bump();
return tally;
}
lib.tallied = makeTally();
lib.inits = function () {
return lib.made.v + "," + lib.bound.v;
return lib.made.v + "," + lib.bound.v + "," + lib.tallied.n;
};
// A constructor function with prototype methods.