mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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
172 lines
3.7 KiB
TypeScript
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);
|
|
}
|