-
Notifications
You must be signed in to change notification settings - Fork 47.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[compiler] HIR-based FlattenScopesWithHooksOrUse
Per title, implements an HIR-based version of FlattenScopesWithHooksOrUse as part of our push to use HIR everywhere. This is the last pass to migrate before PropagateScopeDeps, which is blocking the fix for `bug.invalid-hoisting-functionexpr`, ie where we can infer incorrect dependencies for function expressions if the dependencies are accessed conditionally. ghstack-source-id: 05c6e26b3b7a3b1c3e106a37053f88ac3c72caf5 Pull Request resolved: #29840
- Loading branch information
1 parent
43c17d1
commit b5b9daa
Showing
2 changed files
with
127 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
...packages/babel-plugin-react-compiler/src/ReactiveScopes/FlattenScopesWithHooksOrUseHIR.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
/** | ||
* Copyright (c) Meta Platforms, Inc. and affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
import { CompilerError } from ".."; | ||
import { | ||
BlockId, | ||
HIRFunction, | ||
LabelTerminal, | ||
PrunedScopeTerminal, | ||
ReactiveScope, | ||
getHookKind, | ||
isUseOperator, | ||
} from "../HIR"; | ||
import { retainWhere } from "../Utils/utils"; | ||
|
||
/** | ||
* For simplicity the majority of compiler passes do not treat hooks specially. However, hooks are different | ||
* from regular functions in two key ways: | ||
* - They can introduce reactivity even when their arguments are non-reactive (accounted for in InferReactivePlaces) | ||
* - They cannot be called conditionally | ||
* | ||
* The `use` operator is similar: | ||
* - It can access context, and therefore introduce reactivity | ||
* - It can be called conditionally, but _it must be called if the component needs the return value_. This is because | ||
* React uses the fact that use was called to remember that the component needs the value, and that changes to the | ||
* input should invalidate the component itself. | ||
* | ||
* This pass accounts for the "can't call conditionally" aspect of both hooks and use. Though the reasoning is slightly | ||
* different for reach, the result is that we can't memoize scopes that call hooks or use since this would make them | ||
* called conditionally in the output. | ||
* | ||
* The pass finds and removes any scopes that transitively contain a hook or use call. By running all | ||
* the reactive scope inference first, agnostic of hooks, we know that the reactive scopes accurately | ||
* describe the set of values which "construct together", and remove _all_ that memoization in order | ||
* to ensure the hook call does not inadvertently become conditional. | ||
*/ | ||
export function flattenScopesWithHooksOrUseHIR(fn: HIRFunction): void { | ||
const activeScopes: Array<{ block: BlockId; scope: ReactiveScope }> = []; | ||
const prune: Array<BlockId> = []; | ||
|
||
for (const [, block] of fn.body.blocks) { | ||
const firstId = block.instructions[0]?.id ?? block.terminal.id; | ||
retainWhere(activeScopes, (current) => current.scope.range.end > firstId); | ||
|
||
for (const instr of block.instructions) { | ||
const { value } = instr; | ||
switch (value.kind) { | ||
case "MethodCall": | ||
case "CallExpression": { | ||
const callee = | ||
value.kind === "MethodCall" ? value.property : value.callee; | ||
if ( | ||
getHookKind(fn.env, callee.identifier) != null || | ||
isUseOperator(callee.identifier) | ||
) { | ||
prune.push(...activeScopes.map((entry) => entry.block)); | ||
activeScopes.length = 0; | ||
} | ||
} | ||
} | ||
} | ||
if (block.terminal.kind === "scope") { | ||
activeScopes.push({ | ||
block: block.id, | ||
scope: block.terminal.scope, | ||
}); | ||
} | ||
} | ||
|
||
for (const id of prune) { | ||
const block = fn.body.blocks.get(id)!; | ||
const terminal = block.terminal; | ||
CompilerError.invariant(terminal.kind === "scope", { | ||
reason: `Expected block to have a scope terminal`, | ||
description: `Expected block bb${block.id} to end in a scope terminal`, | ||
loc: terminal.loc, | ||
}); | ||
const body = fn.body.blocks.get(terminal.block)!; | ||
if ( | ||
body.instructions.length === 1 && | ||
body.terminal.kind === "goto" && | ||
body.terminal.block === terminal.fallthrough | ||
) { | ||
/* | ||
* This was a scope just for a hook call, which doesn't need memoization. | ||
* flatten it away. We rely on the PrunedUnusedLabel step to do the actual | ||
* flattening | ||
*/ | ||
block.terminal = { | ||
kind: "label", | ||
block: terminal.block, | ||
fallthrough: terminal.fallthrough, | ||
id: terminal.id, | ||
loc: terminal.loc, | ||
} as LabelTerminal; | ||
continue; | ||
} | ||
|
||
block.terminal = { | ||
kind: "pruned-scope", | ||
block: terminal.block, | ||
fallthrough: terminal.fallthrough, | ||
id: terminal.id, | ||
loc: terminal.loc, | ||
scope: terminal.scope, | ||
} as PrunedScopeTerminal; | ||
} | ||
} |