Files
react/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts
T
Mofei Zhang 0751fac747 [compiler] Optional chaining for dependencies (HIR rewrite)
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
2024-10-02 13:30:55 -04:00

172 lines
3.7 KiB
TypeScript

/**
* 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 {NodePath} from '@babel/traverse';
/*
* Trigger an exhaustivess check in TypeScript and throw at runtime.
*
* Example:
*
* ```ts
* enum ErrorCode = {
* E0001 = "E0001",
* E0002 = "E0002"
* }
*
* switch (code) {
* case ErrorCode.E0001:
* // ...
* default:
* assertExhaustive(code, "Unhandled error code");
* }
* ```
*/
export function assertExhaustive(_: never, errorMsg: string): never {
throw new Error(errorMsg);
}
// Modifies @param array in place, retaining only the items where the predicate returns true.
export function retainWhere<T>(
array: Array<T>,
predicate: (item: T) => boolean,
): void {
let writeIndex = 0;
for (let readIndex = 0; readIndex < array.length; readIndex++) {
const item = array[readIndex];
if (predicate(item) === true) {
array[writeIndex++] = item;
}
}
array.length = writeIndex;
}
export function retainWhere_Set<T>(
items: Set<T>,
predicate: (item: T) => boolean,
): void {
for (const item of items) {
if (!predicate(item)) {
items.delete(item);
}
}
}
export function getOrInsertWith<U, V>(
m: Map<U, V>,
key: U,
makeDefault: () => V,
): V {
if (m.has(key)) {
return m.get(key) as V;
} else {
const defaultValue = makeDefault();
m.set(key, defaultValue);
return defaultValue;
}
}
export function getOrInsertDefault<U, V>(
m: Map<U, V>,
key: U,
defaultValue: V,
): V {
if (m.has(key)) {
return m.get(key) as V;
} else {
m.set(key, defaultValue);
return defaultValue;
}
}
export function Set_equal<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean {
if (a.size !== b.size) {
return false;
}
for (const item of a) {
if (!b.has(item)) {
return false;
}
}
return true;
}
export function Set_union<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): Set<T> {
const union = new Set<T>(a);
for (const item of b) {
union.add(item);
}
return union;
}
export function Set_intersect<T>(sets: Array<ReadonlySet<T>>): Set<T> {
if (sets.length === 0 || sets.some(s => s.size === 0)) {
return new Set();
} else if (sets.length === 1) {
return new Set(sets[0]);
}
const result: Set<T> = new Set();
const first = sets[0];
outer: for (const e of first) {
for (let i = 1; i < sets.length; i++) {
if (!sets[i].has(e)) {
continue outer;
}
}
result.add(e);
}
return result;
}
export function Iterable_some<T>(
iter: Iterable<T>,
pred: (item: T) => boolean,
): boolean {
for (const item of iter) {
if (pred(item)) {
return true;
}
}
return false;
}
export function nonNull<T extends NonNullable<U>, U>(
value: T | null | undefined,
): value is T {
return value != null;
}
export function Set_filter<T>(
source: ReadonlySet<T>,
fn: (arg: T) => boolean,
): Set<T> {
const result = new Set<T>();
for (const entry of source) {
if (fn(entry)) {
result.add(entry);
}
}
return result;
}
export function hasNode<T>(
input: NodePath<T | null | undefined>,
): input is NodePath<NonNullable<T>> {
/*
* Internal babel is on an older version that does not have hasNode (v7.17)
* See https://github.com/babel/babel/pull/13940/files for impl
* https://github.com/babel/babel/blob/5ebab544af2f1c6fc6abdaae6f4e5426975c9a16/packages/babel-traverse/src/path/index.ts#L128-L130
*/
return input.node != null;
}
export function hasOwnProperty<T>(
obj: T,
key: string | number | symbol,
): key is keyof T {
return Object.prototype.hasOwnProperty.call(obj, key);
}