diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index 1127e91029..690e54607b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -36,6 +36,7 @@ import { inferReactivePlaces, inferReferenceEffects, inlineImmediatelyInvokedFunctionExpressions, + inferEffectDependencies, } from '../Inference'; import { constantPropagation, @@ -353,6 +354,10 @@ function* runWithEnvironment( name: 'PropagateScopeDependenciesHIR', value: hir, }); + + if (env.config.EXPERIMENTAL_inferEffectDependencies) { + inferEffectDependencies(env, hir); + } if (env.config.inlineJsxTransform) { inlineJsxTransform(hir, env.config.inlineJsxTransform); diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts new file mode 100644 index 0000000000..45af5bb5ac --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts @@ -0,0 +1,60 @@ +import { ArrayExpression, Effect, Environment, FunctionExpression, GeneratedSource, HIRFunction, IdentifierId, Instruction, isUseEffectHookType, makeInstructionId } from "../HIR"; +import { createTemporaryPlace } from "../HIR/HIRBuilder"; + +export function inferEffectDependencies( + env: Environment, + fn: HIRFunction, + ): void { + const fnExpressions = new Map(); + for (const [, block] of fn.body.blocks) { + for (const instr of block.instructions) { + const {value, lvalue} = instr; + if ( + value.kind === 'FunctionExpression' + ) { + fnExpressions.set(lvalue.identifier.id, value) + } + } + } + + for (const [, block] of fn.body.blocks) { + let newInstructions = [...block.instructions]; + let addedInstrs = 0; + for (const [idx, instr] of block.instructions.entries()) { + const {value} = instr; + + /* + * This check is not final. Right now we only look for useEffects without a dependency array. + * This is likely not how we will ship this feature, but it is good enough for us to make progress + * on the implementation and test it. + */ + if ( + value.kind === 'CallExpression' && + isUseEffectHookType(value.callee.identifier) && + value.args[0].kind === 'Identifier' && + value.args.length === 1 + ) { + const fnExpr = fnExpressions.get(value.args[0].identifier.id); + if (fnExpr != null) { + const deps: ArrayExpression = { + kind: "ArrayExpression", + elements: [...fnExpr.loweredFunc.dependencies], + loc: GeneratedSource + }; + const depsPlace = createTemporaryPlace(env, GeneratedSource); + depsPlace.effect = Effect.Read; + const newInstruction: Instruction = { + id: makeInstructionId(0), + loc: GeneratedSource, + lvalue: depsPlace, + value: deps, + }; + newInstructions.splice(idx + addedInstrs, 0, newInstruction); + addedInstrs++; + value.args[1] = depsPlace; + } + } + } + block.instructions = newInstructions; + } + } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/index.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/index.ts index ee76a37bcb..93b99fb385 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/index.ts @@ -11,3 +11,4 @@ export {inferMutableRanges} from './InferMutableRanges'; export {inferReactivePlaces} from './InferReactivePlaces'; export {default as inferReferenceEffects} from './InferReferenceEffects'; export {inlineImmediatelyInvokedFunctionExpressions} from './InlineImmediatelyInvokedFunctionExpressions'; +export {inferEffectDependencies} from './InferEffectDependencies'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies.expect.md new file mode 100644 index 0000000000..30143e1cc0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies.expect.md @@ -0,0 +1,86 @@ + +## Input + +```javascript +// @inferEffectDependencies +const nonreactive = 0; + +function Component({foo, bar}) { + useEffect(() => { + console.log(foo); + console.log(bar); + console.log(nonreactive); + }); + + useEffect(() => { + console.log(foo); + console.log(bar?.baz); + console.log(bar.qux); + }); + + function f() { + console.log(foo); + } + + useEffect(f); + +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies +const nonreactive = 0; + +function Component(t0) { + const $ = _c(8); + const { foo, bar } = t0; + let t1; + if ($[0] !== foo || $[1] !== bar) { + t1 = () => { + console.log(foo); + console.log(bar); + console.log(nonreactive); + }; + $[0] = foo; + $[1] = bar; + $[2] = t1; + } else { + t1 = $[2]; + } + useEffect(t1, [foo, bar]); + let t2; + if ($[3] !== foo || $[4] !== bar) { + t2 = () => { + console.log(foo); + console.log(bar?.baz); + console.log(bar.qux); + }; + $[3] = foo; + $[4] = bar; + $[5] = t2; + } else { + t2 = $[5]; + } + useEffect(t2, [foo, bar, bar.qux]); + let t3; + if ($[6] !== foo) { + t3 = function f() { + console.log(foo); + }; + $[6] = foo; + $[7] = t3; + } else { + t3 = $[7]; + } + const f = t3; + + useEffect(f); +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies.js new file mode 100644 index 0000000000..86766641f0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies.js @@ -0,0 +1,24 @@ +// @inferEffectDependencies +const nonreactive = 0; + +function Component({foo, bar}) { + useEffect(() => { + console.log(foo); + console.log(bar); + console.log(nonreactive); + }); + + useEffect(() => { + console.log(foo); + console.log(bar?.baz); + console.log(bar.qux); + }); + + function f() { + console.log(foo); + } + + // No inferred dep array, the argument is not a lambda + useEffect(f); + +}