Summary:
The fact that phis are identifiers rather than places is unfortunate in a few cases. In some later analyses, we might wish to know whether a phi is reactive, but we don't have an easy way to do that currently.
Most of the changes here is just replacing phi.id with phi.place.identifier and such. Interesting bits are EnterSSA (several functions now take places rather than identifiers, and InferReactivePlaces now needs to mark places as reactive explicitly.
ghstack-source-id: 5f4fb396cd
Pull Request resolved: https://github.com/facebook/react/pull/31171
Updates the compiler to always import from `react-compiler-runtime` by
default. The runtime then decides whether to use the official or
userspace implementation of useMemoCache.
When we added support for Reanimated, we didn't distinguish between true
globals (i.e. identifiers with no static resolutions), module types, and
imports #29188. For the past 3-4 months, Reanimated imports were not
being matched to the correct hook / function shape we match globals and
module imports against two different registries.
This PR fixes our support for Reanimated library functions imported
under `react-native-reanimated`. See test fixtures for details
Stack from [ghstack](https://github.com/ezyang/ghstack) (oldest at
bottom):
* #31066
* __->__ #31032
Prior to this PR, we check whether the property load source (e.g. the
evaluation of `<base>` in `<base>.property`) is mutable + scoped to
determine whether the property load itself is eligible for hoisting.
This changes to check the base identifier of the load.
- This is needed for the next PR #31066. We want to evaluate whether the
base identifier is mutable within the context of the *outermost
function*. This is because all LoadLocals and PropertyLoads within a
nested function declaration have mutable-ranges within the context of
the function, but the base identifier is a context variable.
- A side effect is that we no longer infer loads from props / other
function arguments as mutable in edge cases (e.g. props escaping out of
try-blocks or being assigned to context variables)
Adds HIR version of `PropagateScopeDeps` to handle optional chaining.
Internally, this improves memoization on ~4% of compiled files (internal links: [1](https://www.internalfb.com/intern/paste/P1610406497/))
Summarizing the changes in this PR.
1. `CollectOptionalChainDependencies` recursively traverses optional blocks down to the base. From the base, we build up a set of `baseIdentifier.propertyA?.propertyB` mappings.
The tricky bit here is that optional blocks sometimes reference other optional blocks that are *not* part of the same chain e.g. a(c?.d)?.d. See code + comments in `traverseOptionalBlock` for how we avoid concatenating unrelated blocks.
2. Adding optional chains into non-null object calculation.
(Note that marking `a?.b` as 'non-null' means that `a?.b.c` is safe to evaluate, *not* `(a?.b).c`. Happy to rename this / reword comments accordingly if there's a better term)
This pass is split into two stages. (1) collecting non-null objects by block and (2) propagating non-null objects across blocks. The only significant change here was to (2). We add an extra reduce step `X=Reduce(Union(X, Intersect(X_neighbors)))` to merge optional and non-optional nodes (e.g. nonNulls=`{a, a?.b}` reduces to `{a, a.b}`)
3. Adding optional chains into dependency calculation.
This was the trickiest. We need to take the "maximal" property chain as a dependency. Prior to this PR, we avoided taking subpaths e.g. `a.b` of `a.b.c` as dependencies by only visiting non-PropertyLoad/LoadLocal instructions. This effectively only recorded the property-path at site-of-use.
Unfortunately, this *quite* doesn't work for optional chains for a few reasons:
- We would need to skip relevant `StoreLocal`/`Branch terminal` instructions (but only those within optional blocks that have been successfully read).
- Given an optional chain, either (1) only a subpath or (2) the entire path can be represented as a PropertyLoad. We cannot directly add the last hoistable optional-block as a dependency as MethodCalls are an edge case e.g. given a?.b.c(), we should depend on `a?.b`, not `a?.b.c`
This means that we add its dependency at either the innermost unhoistable optional-block or when encountering it within its phi-join.
4. Handle optional chains in DeriveMinimalDependenciesHIR.
This was also a bit tricky to formulate. Ideally, we would avoid a 2^3 case join (cond | uncond cfg, optional | not optional load, access | dependency). This PR attempts to simplify by building two trees
1. First add each hoistable path into a tree containing `Optional | NonOptional` nodes.
2. Then add each dependency into another tree containing `Optional | NonOptional`, `Access | Dependency` nodes, truncating the dependency at the earliest non-hoistable node (i.e. non-matching pair when walking the hoistable tree)
ghstack-source-id: a2170f2628
Pull Request resolved: https://github.com/facebook/react/pull/31037
Rename for clarity:
- `CollectHoistablePropertyLoads:Tree` -> `CollectHoistablePropertyLoads:PropertyPathRegistry`
- `getPropertyLoadNode` -> `getOrCreateProperty`
- `getOrCreateRoot` -> `getOrCreateIdentifier`
- `PropertyLoadNode` -> `PropertyPathNode`
Refactor to CFG joining logic for `CollectHoistablePropertyLoads`. We now write to the same set of inferredNonNullObjects when traversing from entry and exit blocks. This is more correct, as non-nulls inferred from a forward traversal should be included when computing the backward traversal (and vice versa). This fix is needed by an edge case in #31036
Added invariant into fixed-point iteration to terminate (instead of infinite looping).
ghstack-source-id: 1e8eb2d566
Pull Request resolved: https://github.com/facebook/react/pull/31036
Found when writing #31037, summary copied from comments:
This is an extreme edge case and not code we'd expect any reasonable developer to write. In most cases e.g. `(a?.b != null ? a.b : DEFAULT)`, we do want to take a dependency on `a?.b`.
I found this trying to come up with edge cases that break the current dependency + CFG merging logic. I think it makes sense to error on the side of correctness. After all, we still take `a` as a dependency if users write `a != null ? a.b : DEFAULT`, and the same fix (understanding the `<hoistable> != null` test expression) works for both. Can be convinced otherwise though!
ghstack-source-id: cc06afda59
Pull Request resolved: https://github.com/facebook/react/pull/31035
Followup from #30894.
This adds a new flagged mode `enablePropagateScopeDepsInHIR: "enabled_with_optimizations"`, under which we infer more hoistable loads:
- it's always safe to evaluate loads from `props` (i.e. first parameter of a `component`)
- destructuring sources are safe to evaluate loads from (e.g. given `{x} = obj`, we infer that it's safe to evaluate obj.y)
- computed load sources are safe to evaluate loads from (e.g. given `arr[0]`, we can infer that it's safe to evaluate arr.length)
ghstack-source-id: 32f3bb72e9
Pull Request resolved: https://github.com/facebook/react/pull/31033
Followup from #30894 , not sure how these got missed. Note that this PR just copies the fixtures without adding `@enablePropagateDepsInHIR`. #31032 follows and actually enables the HIR-version of propagateScopeDeps to run. I split this out into two PRs to make snapshot differences easier to review, but also happy to merge
Fixtures found from locally setting snap test runner to default to `enablePropagateDepsInHIR: 'enabled_baseline'` and forking fixtures files with different output.
ghstack-source-id: 7d7cf41aa9
Pull Request resolved: https://github.com/facebook/react/pull/31030
Currently the playground is setup as a linked workspace for the
compiler which complicates our yarn workspace setup and means that snap
can sometimes pull in a different version of react than was otherwise
specified.
There's no real reason to have these workspaces combined so let's split
them up.
ghstack-source-id: 56ab064b2f
Pull Request resolved: https://github.com/facebook/react/pull/31081
Based on https://github.com/facebook/react/pull/30995 ([rendered
diff](https://github.com/jackpope/react/compare/inline-jsx-2...jackpope:react:inline-jsx-3?expand=1))
____
Some apps still use `react.element` symbols. Not only do we want to test
there but we also want to be able to upgrade those sites to
`react.transitional.element` without blocking on the compiler (we can
change the symbol feature flag and compiler config at the same time).
The compiler runtime uses `react.transitional.element`, so the snap
fixture will fail if we change the default here. However I confirmed
that commenting out the fixture entrypoint and running snap with
`react.element` will update the fixture symbols as expected.
If JSX receives a props spread without additional attributes (besides
`ref` and `key`), we can pass the spread object as a property directly
to avoid the extra object copy.
```
<Test {...propsToSpread} />
// {props: propsToSpread}
<Test {...propsToSpread} a="z" />
// {props: {...propsToSpread, a: "z"}}
```
This adds an `InlineJsxTransform` optimization pass, toggled by the
`enableInlineJsxTransform` flag. When enabled, JSX will be transformed
into React Element object literals, preventing runtime overhead during
element creation.
TODO:
- [ ] Add conditionals to make transform PROD-only
- [ ] Make the React element symbol configurable so this works with
runtimes that support `react.element` or `react.transitional.element`
- [ ] Look into additional optimization to pass props spread through
directly if none of the properties are mutated
Summary:
1. Minor refactor to provide a stable API for calling the compiler from the playground
2. Allows spaces in pass names without breaking the appearance of the playground by replacing spaces with in pass tabs
ghstack-source-id: 12a43ad86c
Pull Request resolved: https://github.com/facebook/react/pull/30988
Compiler bailout diagnostics should now highlight only the first line of
the source location span.
(Resubmission of #30423 which was reverted due to invalid column
number.)
Summary:
Introduces a new binding kind for functions that allows them to be hoisted. Also has the result of causing all nested function declarations to be outputted as function declarations, not as let bindings.
ghstack-source-id: fa40d4909f
Pull Request resolved: https://github.com/facebook/react/pull/30922
Summary:
This brings the behavior of ref mutation within hook callbacks into alignment with the behavior of global mutations--that is, we allow all hooks to take callbacks that may mutate a ref. This is potentially unsafe if the hook eagerly calls its callback, but the alternative is excessively limiting (and inconsistent with other enforcement).
This also bans *directly* passing a ref.current value to a hook, which was previously allowed.
ghstack-source-id: e66ce7123e
Pull Request resolved: https://github.com/facebook/react/pull/30917
Summary:
This change expands our handling of refs to build an understanding of nested refs within objects and functions that may return refs. It builds a special-purpose type system within the ref analysis that gives a very lightweight structural type to objects and array expressions (merging the types of all their members), and then propagating those types throughout the analysis (e.g., if `ref` has type `Ref`, then `{ x: ref }` and `[ref]` have type `Structural(value=Ref)` and `{x: ref}.anything` and `[ref][anything]` have type `Ref`).
This allows us to support structures that contain refs, and functions that operate over them, being created and passed around during rendering without at runtime accessing a ref value.
The analysis here uses a fixpoint to allow types to be fully propagated through the system, and we defend against diverging by widening the type of a variable if it could grow infinitely: so, in something like
```
let x = ref;
while (condition) {
x = [x]
}
```
we end up giving `x` the type `Structural(value=Ref)`.
ghstack-source-id: afb0b0cb01
Pull Request resolved: https://github.com/facebook/react/pull/30902
Summary:
This PR performs a major refactor of InferReferenceEffects to separate out the work on marking places with Effects from inferring FunctionEffects. The behavior should be identical after this change (see [internal sync](https://www.internalfb.com/intern/everpaste/?handle=GN74VxscnUaztTYDAL8q0CRWBIxibsIXAAAB)) but the FunctionEffect logic should be easier to work with.
These analyses are unfortunately still deeply linked--the FunctionEffect analysis needs to reason about the "current" value kind for each point in the program, while the InferReferenceEffects algorithm performs global updates on the state of the program (e.g. freezing). In the future, it might be possible to make these entirely separate passes if we store the ValueKind directly on places.
For the most part, the logic of reference effects and function effects can be cleanly separated: for each instruction and terminal, we visit its places and infer their effects, and then we visit its places and infer any function effects that they cause. The biggest wrinkle here is that when a transitive function freeze operation occurs, it has to happen *after* inferring the function effects on the place, because otherwise we may convert a value from Context to Frozen, which will cause the ContextualMutation function effect to be converted to a ReactMutation effect too early. This can be observed in a case like this:
```
export default component C() {
foo(() => {
const p = {};
return () => {
p['a'] = 1
};
});
}
```
Here when the outer function returns the inner function, it freezes the inner function which transitively freezes `p`. But before that freeze happens, we need to replay the ContextualMutation on the inner function to determine that the value is mutable in the outer context. If we froze `p` first, we would instead convert the ContextualMutation to a ReactMutation and error.
To handle this, InferReferenceEffects now delays the exection of the freezeValue action until after it's called the helper functions that generate function effects. So the order of operations on a given place is now
set effect --> generate function effects --> transitively freeze dependencies, if applicable
ghstack-source-id: 21cb50c140
Pull Request resolved: https://github.com/facebook/react/pull/30920
Resubmission of #30079 -- core logic unchanged, but needed to rebase past #30573
### Quick background
#### Temporaries
The compiler currently treats temporaries and named variables (e.g. `x`) differently in this pass.
- named variables may be reassigned (in fact, since we're running after LeaveSSA, a single named identifier's IdentifierId may map to multiple `Identifier` instances -- each with its own scope and mutable range)
- temporaries are replaced with their represented expressions during codegen. This is correct (mostly correct, see #29878) as we're careful to always lower the correct evaluation semantics. However, since we rewrite reactive scopes entirely (to if/else blocks), we need to track temporaries that a scope produces in `ReactiveScope.declarations` and later promote them to named variables.
In the same example, $4, $5, and $6 need to be promoted: $2 ->`t0`, $5 ->`t1`, and $6 ->`t2`.
```js
[1] $2 = LoadGlobal(global) foo
[2] $3 = LoadLocal bar$1
scope 0:
[3] $4 = Call $2(<unknown> $3)
scope 1:
[4] $5 = Object { }
scope 2:
[5] $6 = Object { a: $4, b: $5 }
[6] $8 = StoreLocal Const x$7 = $6
```
#### Dependencies
`ReactiveScope.dependencies` records the set of (read-only) values that a reactive scope is dependent on. This is currently limited to just variables (named variables from source and promoted temporaries) and property-loads.
All dependencies we record need to be hoistable -- i.e. reordered to just before the ReactiveScope begins. Not all PropertyLoads are hoistable.
In this example, we should not evaluate `obj.a.b` without before creating x and checking `objIsNull`.
```js
// reduce-reactive-deps/no-uncond.js
function useFoo({ obj, objIsNull }) {
const x = [];
if (isFalse(objIsNull)) {
x.push(obj.a.b);
}
return x;
}
```
While other memoization strategies with different constraints exist, the current compiler requires that `ReactiveScope.dependencies` be re-orderable to the beginning of the reactive scope. But.. `PropertyLoad`s from null values will throw `TypeError`. This means that evaluating hoisted dependencies should throw if and only if the source program throws. (It is also a bug if source throws and compiler output does not throw. See https://github.com/facebook/react-forget/pull/2709)
---
### Rough high level overview
1. Pass 1
Walk over instructions to gather every temporary used outside of its defining scope (same as ReactiveFunction version). These determine the sidemaps we produce, as temporaries used outside of their declaring scopes get promoted to named variables later (and are not considered hoistable rvals).
2. Pass 2 (collectTemporariesSidemap)
Walk over instructions to generate a sidemap of temporary identifier -> named variable and property path (e.g. `$3 -> {obj: props, path: ["a", "b"]}`)
2. Pass 2 (collectHoistablePropertyLoads)
a. Build a sidemap of block -> accessed variables and properties (e.g. `bb0 -> [ {obj: props, path: ["a", "b"]} ]`)
b. Propagate "non-nullness" i.e. variables and properties for which we can safely evaluate `PropertyLoad`.
A basic block can unconditionally read from identifier X if any of the following applies:
- the block itself reads from identifier X
- all predecessors of the block read from identifier X
- all successors of the block read from identifier X
4. Pass 3: (collectDependencies)
Walks over instructions again to record dependencies and declarations, using the previously produced sidemaps. We do not record any control-flow here
5. Merge every scope's recorded dependencies with the set of hoistable PropertyLoads
Tested by syncing internally and (1) checking compilation output differences ([internal link](https://www.internalfb.com/intern/everpaste/?handle=GPCfUBt_HCoy_S4EAJDVFJyJJMR0bsIXAAAB)), running internally e2e tests ([internal link](https://fburl.com/sandcastle/cs5mlkxq))
---
### Followups:
1. Rewrite function expression deps
This change produces much more optimal output as the compiler now uses the function CFG to understand which variables / paths are assumed to be non-null. However, it may exacerbate [this function-expr hoisting bug](https://github.com/facebook/react/blob/main/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-hoisting-functionexpr.tsx). A short term fix here is to simply call some form of `collectNonNullObjects` on every function expression to find hoistable variable / paths. In the longer term, we should refactor out `FunctionExpression.deps`.
2. Enable optional paths
(a) don't count optional load temporaries as dependencies (e.g. `collectOptionalLoadRValues(...)`).
(b) record optional paths in both collectHoistablePropertyLoads and dependency collection
ghstack-source-id: 2507f6ea75
Pull Request resolved: https://github.com/facebook/react/pull/30894
- flip `enablePropagateDepsInHIR` to off by default
- fork fixtures which produce compilation differences in #30894 to separate directory `propagate-scope-deps-hir-fork`, to be cleaned up when we remove this flag
ghstack-source-id: 7d5b8dc297
Pull Request resolved: https://github.com/facebook/react/pull/30949
Alternative to #30868. The goal is to ensure that the types coming out of moduleTypeProvider are valid wrt to hook typing. If something is named like a hook, then it must be typed as a hook (or don't type it).
ghstack-source-id: 3e8b5a0a70
Pull Request resolved: https://github.com/facebook/react/pull/30888
Reactive scopes in HIR has been stable for over 3 months now and is the future direction of react compiler, removing this flag to reduce implementation forks.
ghstack-source-id: 65cdf63cf7
Pull Request resolved: https://github.com/facebook/react/pull/30891