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 7ae520a144c9b..103942aa94323 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, @@ -356,6 +357,10 @@ function* runWithEnvironment( 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/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index b85d9425cb7ac..a09827caa634d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -239,7 +239,12 @@ const EnvironmentConfigSchema = z.object({ * the dependency. */ enableOptionalDependencies: z.boolean().default(true), - + + /** + * Enables inference of effect dependencies. Still experimental. + */ + EXPERIMENTAL_inferEffectDependencies: z.boolean().default(false), + /** * Enables inlining ReactElement object literals in place of JSX * An alternative to the standard JSX transform which replaces JSX with React's jsxProd() runtime 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 0000000000000..45af5bb5ac5b7 --- /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 ee76a37bcb4b7..93b99fb385262 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 0000000000000..30143e1cc0781 --- /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 0000000000000..86766641f031a --- /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); + +} diff --git a/compiler/packages/snap/src/compiler.ts b/compiler/packages/snap/src/compiler.ts index cd907575fb797..d450c270c7231 100644 --- a/compiler/packages/snap/src/compiler.ts +++ b/compiler/packages/snap/src/compiler.ts @@ -142,6 +142,7 @@ function makePluginOptions( importSpecifierName: '$structuralCheck', }; } + const hookPatternMatch = /@hookPattern:"([^"]+)"/.exec(firstLine); if ( hookPatternMatch && @@ -209,6 +210,11 @@ function makePluginOptions( if (firstLine.includes('@enableInlineJsxTransform')) { inlineJsxTransform = {elementSymbol: 'react.transitional.element'}; } + + let inferEffectDependencies = false; + if (firstLine.includes('@inferEffectDependencies')) { + inferEffectDependencies = true; + } let logs: Array<{filename: string | null; event: LoggerEvent}> = []; let logger: Logger | null = null; @@ -240,6 +246,7 @@ function makePluginOptions( lowerContextAccess, validateBlocklistedImports, inlineJsxTransform, + EXPERIMENTAL_inferEffectDependencies: inferEffectDependencies, }, compilationMode, logger,