diff --git a/packages/compiler/src/frontend/shape-inference/analyze.ts b/packages/compiler/src/frontend/shape-inference/analyze.ts index d783a2ad7..bedd39c77 100644 --- a/packages/compiler/src/frontend/shape-inference/analyze.ts +++ b/packages/compiler/src/frontend/shape-inference/analyze.ts @@ -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(); diff --git a/tests/fixtures/npm/cases/static-tables/js-main.cjs b/tests/fixtures/npm/cases/static-tables/js-main.cjs index a3e42ff98..8433deecd 100644 --- a/tests/fixtures/npm/cases/static-tables/js-main.cjs +++ b/tests/fixtures/npm/cases/static-tables/js-main.cjs @@ -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) { diff --git a/tests/fixtures/npm/cases/static-tables/main.ts b/tests/fixtures/npm/cases/static-tables/main.ts index a4929e831..d53e2fa8b 100644 --- a/tests/fixtures/npm/cases/static-tables/main.ts +++ b/tests/fixtures/npm/cases/static-tables/main.ts @@ -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()); 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 a8db112e6..6bae9a947 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 @@ -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;