From 54899bcc10742adebd05eeb00c55ee0fd7b138a3 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 11:38:58 -0500 Subject: [PATCH] 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. --- .../src/frontend/shape-inference/analyze.ts | 17 +++++++++++++++++ .../node_modules/static-tables/index.js | 15 ++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/shape-inference/analyze.ts b/packages/compiler/src/frontend/shape-inference/analyze.ts index 44d0f06a1..6722a532a 100644 --- a/packages/compiler/src/frontend/shape-inference/analyze.ts +++ b/packages/compiler/src/frontend/shape-inference/analyze.ts @@ -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(); + /** 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(); isFactory(node: Node): boolean { let r = this.factories.get(node); diff --git a/tests/fixtures/npm/cases/static-tables/node_modules/static-tables/index.js b/tests/fixtures/npm/cases/static-tables/node_modules/static-tables/index.js index 6bae9a947..1150aff5f 100644 --- a/tests/fixtures/npm/cases/static-tables/node_modules/static-tables/index.js +++ b/tests/fixtures/npm/cases/static-tables/node_modules/static-tables/index.js @@ -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.