mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
21652a135b6bd1cb87dbcc2480aaf0886e056d63
## Problem
The previous version of LeaveSSA used a very simple approach in which
identifiers stored their pre-ssa id, and LeaveSSA restored this id back. The
upside of this approach is that it's very simple and trivially correct (assuming
no reordering of code). The downside is that after running LeaveSSA we lose all
information about which versions of variable declarations are distinct, and
which might merge together in a phi. That information is really useful for scope
analysis! Consider this input (variables are numbered as they would be in SSA
form):
```javascript
function foo(a, b, c) {
let x$1 = null;
if (a) {
x$2 = b;
} else {
x$3 = c;
}
x$4 = phi(x$2, x$3);
return x$4;
```
The current LeaveSSA assigns all 4 variables back to `x$1`, with a single let
declaration at `let x$1 = null`. However, from a reactive scopes perspective,
there are really just 2 versions of x: the initial x$1 (defined and never used)
and then x$2, x$3, and x$4, which have to be merged into a single scope because
they are part of a phi. In other words, we can't independently compute x$2, x$3,
or x$4 - if any of their inputs changes, we have to redo all the computation.
However, the existing structure makes it difficult to figure out the correct
starting point for this scope — there is no initial `let` declaration that we
can refer to.
Instead, we can represent the program as follows after LeaveSSA, and then use
this form for scope analysis:
```javascript
function foo(a, b, c) {
const x$1 = null; // NOTE: rewritten to const
let x$2; // synthesized declaration to allow later reassignment
if (a) {
x$2 = b;
} else {
x$2 = c;
}
return x$2;
```
Note that there are only 2 versions of x, and we have synthesized a variable
declaration for x$2 at the appropriate scope. Our scope analysis can then
determine that the range of x$2 is from the declaration to the end of the if.
## Approach
This pass does two main rewrites:
* For variables that do *not* appear as a phi id or operand, it rewrites the
declaration to be `const`. You can see this above for x$1.
* For variables that *do* appear as a phi or operand, it synthesizes a new `let`
binding at the appropriate scope (ie, in the appropriate block), and updates all
other operands from the phi to use the same id for the variable. You can see
this above for x$2, x$3, and x$4.
Note that the let binding is generated at the narrowest scope possible. In this
example, we generate distinct let bindings for the other if and else branches:
```javascript
function foo(a, b, c) {
let x = null;
if (a) {
// we generate a `let x$2` here
if (b) {
x = 0; // becomes x$2
} else {
x = 1; // becomes x$2
}
x // becomes x$2
} else {
// we generate a `let x$3` here
if (c) {
x = 2; // becomes x$3
} else {
x = 3; // becomes x$3
}
x; // becomes x$3
}
}
```
Because the different x values from the outer if/else can never join in a phi,
we can treat them as independent variables and (re)compute them independently.
The algorithm works by iterating in reverse-postorder, and looking ahead at
fallthrough blocks to find phi nodes that may need a let declaration (see above
example of where these are generated). It also tracks variables which _don't_
participate in a phi so that it can rewrite their declarations to `const`.
## TODO
This PR does *not* yet work for cases where there is unconditional assignment
within a `while` test condition. That would technically create a distinct
version of the variable that shadows the value for the loop, and you can't have
variable declarations in a while test condition.
That case already doesn't work, though, so i'm punting on it for now until we
figure out a bit more around
"value" blocks. We have some good options, like desugaring to a `for(;;)` and
manually implementing the while semantics in that case.
Description
Languages
JavaScript
67.1%
TypeScript
29.4%
HTML
1.5%
CSS
1.1%
C++
0.6%
Other
0.2%