diff --git a/compiler/forget/src/ReactiveScopes/MemoizeFbtOperandsInSameScope.ts b/compiler/forget/src/ReactiveScopes/MemoizeFbtOperandsInSameScope.ts index c91df8874d..cfb0d3ce46 100644 --- a/compiler/forget/src/ReactiveScopes/MemoizeFbtOperandsInSameScope.ts +++ b/compiler/forget/src/ReactiveScopes/MemoizeFbtOperandsInSameScope.ts @@ -15,21 +15,27 @@ import { eachInstructionValueOperand } from "../HIR/visitors"; import { ReactiveFunctionVisitor, visitReactiveFunction } from "./visitors"; /** - * This is a Meta-ism. We special-case the `` element for translation purposes, - * and have a transform that requires the children of this element to be a limited - * subset of nodes. Notably, any dynamic translation values must appear as - * `` children — we disallow identifiers as children of `` nodes. + * This pass supports the `fbt` translation system (https://facebook.github.io/fbt/). + * FBT provides the `` JSX element and `fbt()` calls (which take params in the + * form of `` children or `fbt.param()` arguments, respectively). These + * tags/functions have restrictions on what types of syntax may appear as props/children/ + * arguments, notably that variable references may not appear directly — variables + * must always be wrapped in a `` or `fbt.param()`. * - * This PR adds a new pass which finds `` nodes and ensures their immediate - * operands are not independently memoized. Note that this still allows the values - * of `` to be independently memoized + * To ensure that Forget doesn't rewrite code to violate this restriction, we force + * operands to fbt tags/calls have the same scope as the tag/call itself. + * + * Note that this still allows the props/arguments of ``/`fbt.param()` + * to be independently memoized */ export function memoizeFbtOperandsInSameScope(fn: ReactiveFunction): void { visitReactiveFunction(fn, new Transform(), undefined); } class Transform extends ReactiveFunctionVisitor { - fbtTags: Set = new Set(); + // Values that represent *potential* references of `fbt` as a JSX tag name + // or as a callee. + fbtValues: Set = new Set(); override visitInstruction( instruction: ReactiveInstruction, @@ -46,10 +52,15 @@ class Transform extends ReactiveFunctionVisitor { ) { // We don't distinguish between tag names and strings, so record // all `fbt` string literals in case they are used as a jsx tag. - this.fbtTags.add(lvalue.identifier.id); + this.fbtValues.add(lvalue.identifier.id); + } else if (value.kind === "LoadGlobal" && value.name === "fbt") { + // Record references to `fbt` as a global + this.fbtValues.add(lvalue.identifier.id); } else if ( - value.kind === "JsxExpression" && - this.fbtTags.has(value.tag.identifier.id) + (value.kind === "JsxExpression" && + this.fbtValues.has(value.tag.identifier.id)) || + (value.kind === "CallExpression" && + this.fbtValues.has(value.callee.identifier.id)) ) { // if the JSX element's tag was `fbt`, mark all its operands // to ensure that they end up in the same scope as the jsx element diff --git a/compiler/forget/src/__tests__/fixtures/compiler/fbt-call-complex-param-value.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/fbt-call-complex-param-value.expect.md new file mode 100644 index 0000000000..b945d9c1ba --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/fbt-call-complex-param-value.expect.md @@ -0,0 +1,46 @@ + +## Input + +```javascript +function Component(props) { + const text = fbt( + `Hello, ${fbt.param("(key) name", capitalize(props.name))}!`, + "(description) Greeting" + ); + return
{text}
; +} + +``` + +## Code + +```javascript +function Component(props) { + const $ = React.unstable_useMemoCache(4); + const c_0 = $[0] !== props.name; + let t0; + if (c_0) { + t0 = fbt( + `Hello, ${fbt.param("(key) name", capitalize(props.name))}!`, + "(description) Greeting" + ); + $[0] = props.name; + $[1] = t0; + } else { + t0 = $[1]; + } + const text = t0; + const c_2 = $[2] !== text; + let t1; + if (c_2) { + t1 =
{text}
; + $[2] = text; + $[3] = t1; + } else { + t1 = $[3]; + } + return t1; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/fbt-call-complex-param-value.js b/compiler/forget/src/__tests__/fixtures/compiler/fbt-call-complex-param-value.js new file mode 100644 index 0000000000..c7f8570d6f --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/fbt-call-complex-param-value.js @@ -0,0 +1,7 @@ +function Component(props) { + const text = fbt( + `Hello, ${fbt.param("(key) name", capitalize(props.name))}!`, + "(description) Greeting" + ); + return
{text}
; +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/fbt-call.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/fbt-call.expect.md new file mode 100644 index 0000000000..b0e3d49bc3 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/fbt-call.expect.md @@ -0,0 +1,46 @@ + +## Input + +```javascript +function Component(props) { + const text = fbt( + `${fbt.param("(key) count", props.count)} items`, + "(description) Number of items" + ); + return
{text}
; +} + +``` + +## Code + +```javascript +function Component(props) { + const $ = React.unstable_useMemoCache(4); + const c_0 = $[0] !== props.count; + let t0; + if (c_0) { + t0 = fbt( + `${fbt.param("(key) count", props.count)} items`, + "(description) Number of items" + ); + $[0] = props.count; + $[1] = t0; + } else { + t0 = $[1]; + } + const text = t0; + const c_2 = $[2] !== text; + let t1; + if (c_2) { + t1 =
{text}
; + $[2] = text; + $[3] = t1; + } else { + t1 = $[3]; + } + return t1; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/fbt-call.js b/compiler/forget/src/__tests__/fixtures/compiler/fbt-call.js new file mode 100644 index 0000000000..087fa47d99 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/fbt-call.js @@ -0,0 +1,7 @@ +function Component(props) { + const text = fbt( + `${fbt.param("(key) count", props.count)} items`, + "(description) Number of items" + ); + return
{text}
; +}