diff --git a/compiler/forget/src/HIR/BuildHIR.ts b/compiler/forget/src/HIR/BuildHIR.ts index 1386175177..ac1b308f02 100644 --- a/compiler/forget/src/HIR/BuildHIR.ts +++ b/compiler/forget/src/HIR/BuildHIR.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import { NodePath } from "@babel/traverse"; +import { NodePath, Scope } from "@babel/traverse"; import * as t from "@babel/types"; import { invariant } from "../Utils/CompilerError"; import todo, { todoInvariant } from "../Utils/todo"; @@ -1151,6 +1151,8 @@ function lowerExpression( case "FunctionExpression": { const expr = exprPath as NodePath; const name: string | null = expr.get("id")?.node?.name ?? null; + const componentScope: Scope = expr.scope.parent.getFunctionParent()!; + const dependencies = gatherCapturedDeps(expr, componentScope); const body = expr.get("body").node; const params: Array = expr.get("params").map((p) => { todoInvariant(p.isIdentifier(), "handle non identifier params"); @@ -1161,6 +1163,7 @@ function lowerExpression( name, body, params, + dependencies, loc: exprLoc, }; } @@ -1540,3 +1543,53 @@ function lowerAssignment( } } } + +function capturePureScopes( + currentScope: Scope, + componentScope: Scope +): Set { + let pureScopes: Set = new Set(); + while (currentScope) { + pureScopes.add(currentScope); + + if (currentScope === componentScope) { + break; + } + + currentScope = currentScope.parent; + } + return pureScopes; +} + +function gatherCapturedDeps( + fn: NodePath, + componentScope: Scope +): Set { + const captured: Set = new Set(); + + // Capture all the scopes from the parent of this function up to and including + // the component scope. + const pureScopes: Set = capturePureScopes( + fn.scope.parent, + componentScope + ); + + fn.get("body").traverse({ + Expression(path) { + // TODO(gsn): Handle member expressions + if (!path.isIdentifier) { + return; + } + + const id = path as NodePath; + const binding = id.scope.getBinding(id.node.name); + if (binding === undefined || !pureScopes.has(binding.scope)) { + return; + } + + captured.add(binding.identifier); + }, + }); + + return captured; +} diff --git a/compiler/forget/src/HIR/HIR.ts b/compiler/forget/src/HIR/HIR.ts index 8043f44d99..bc2d4682f7 100644 --- a/compiler/forget/src/HIR/HIR.ts +++ b/compiler/forget/src/HIR/HIR.ts @@ -313,6 +313,7 @@ export type InstructionData = kind: "FunctionExpression"; name: string | null; params: Array; + dependencies: Set; body: t.BlockStatement; } diff --git a/compiler/forget/src/HIR/PrintHIR.ts b/compiler/forget/src/HIR/PrintHIR.ts index 67392651a3..19e5ac833e 100644 --- a/compiler/forget/src/HIR/PrintHIR.ts +++ b/compiler/forget/src/HIR/PrintHIR.ts @@ -293,7 +293,10 @@ export function printInstructionValue(instrValue: InstructionValue): string { case "FunctionExpression": { const params = instrValue.params.join(","); const body = generate(instrValue.body).code; - value = `Function ${instrValue.name}(${params}){${body}}`; + const deps = [...instrValue.dependencies].map((i) => i.name).join(","); + value = `Function ${instrValue.name ?? ""} @deps[${deps}] (${ + params ?? "" + }){${body}}`; break; } default: { diff --git a/compiler/forget/src/__tests__/fixtures/hir/capturing-function-1.expect.md b/compiler/forget/src/__tests__/fixtures/hir/capturing-function-1.expect.md new file mode 100644 index 0000000000..de7dd20aec --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/capturing-function-1.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +function component() { + let z = 100; + let x = function () { + z; + }; + return x; +} + +``` + +## Code + +```javascript +function component() { + const $ = React.useMemoCache(); + const z = 100; + let x; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + x = function () { + z; + }; + + $[0] = x; + } else { + x = $[0]; + } + + return x; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/capturing-function-1.js b/compiler/forget/src/__tests__/fixtures/hir/capturing-function-1.js new file mode 100644 index 0000000000..e3fc3c5452 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/capturing-function-1.js @@ -0,0 +1,7 @@ +function component() { + let z = 100; + let x = function () { + z; + }; + return x; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/capturing-function-within-block.expect.md b/compiler/forget/src/__tests__/fixtures/hir/capturing-function-within-block.expect.md new file mode 100644 index 0000000000..6f8bbc044d --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/capturing-function-within-block.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +function component() { + let z = 100; + let x; + { + x = function () { + z; + }; + } + return x; +} + +``` + +## Code + +```javascript +function component() { + const $ = React.useMemoCache(); + const z = 100; + const x = undefined; + let x$0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + x$0 = function () { + z; + }; + + $[0] = x$0; + } else { + x$0 = $[0]; + } + + return x$0; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/capturing-function-within-block.js b/compiler/forget/src/__tests__/fixtures/hir/capturing-function-within-block.js new file mode 100644 index 0000000000..5154c4c8fb --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/capturing-function-within-block.js @@ -0,0 +1,10 @@ +function component() { + let z = 100; + let x; + { + x = function () { + z; + }; + } + return x; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/capturing-variable-in-nested-block.expect.md b/compiler/forget/src/__tests__/fixtures/hir/capturing-variable-in-nested-block.expect.md new file mode 100644 index 0000000000..a9aaf58a15 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/capturing-variable-in-nested-block.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +function component() { + let z = 100; + let x = function () { + { + z; + } + }; + return x; +} + +``` + +## Code + +```javascript +function component() { + const $ = React.useMemoCache(); + const z = 100; + let x; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + x = function () { + { + z; + } + }; + + $[0] = x; + } else { + x = $[0]; + } + + return x; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/capturing-variable-in-nested-block.js b/compiler/forget/src/__tests__/fixtures/hir/capturing-variable-in-nested-block.js new file mode 100644 index 0000000000..26fd218e71 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/capturing-variable-in-nested-block.js @@ -0,0 +1,9 @@ +function component() { + let z = 100; + let x = function () { + { + z; + } + }; + return x; +} diff --git a/compiler/forget/src/__tests__/fixtures/hir/capturing-variable-in-nested-function.expect.md b/compiler/forget/src/__tests__/fixtures/hir/capturing-variable-in-nested-function.expect.md new file mode 100644 index 0000000000..5d90511ee5 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/capturing-variable-in-nested-function.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +function component() { + let z = 100; + let x = function () { + (function () { + z; + })(); + }; + return x; +} + +``` + +## Code + +```javascript +function component() { + const $ = React.useMemoCache(); + const z = 100; + let x; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + x = function () { + (function () { + z; + })(); + }; + + $[0] = x; + } else { + x = $[0]; + } + + return x; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/hir/capturing-variable-in-nested-function.js b/compiler/forget/src/__tests__/fixtures/hir/capturing-variable-in-nested-function.js new file mode 100644 index 0000000000..46eda93731 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/hir/capturing-variable-in-nested-function.js @@ -0,0 +1,9 @@ +function component() { + let z = 100; + let x = function () { + (function () { + z; + })(); + }; + return x; +}