Compare commits

..

1 Commits

Author SHA1 Message Date
Mofei Zhang
6cfde32738 [compiler] Fallback for inferred effect dependencies
When effect dependencies cannot be inferred due to memoization-related bailouts or unexpected mutable ranges (which currently often have to do with writes to refs), fall back to traversing the effect lambda itself.

This fallback uses the same logic as PropagateScopeDependencies:
1. Collect a sidemap of loads and property loads
2. Find hoistable accesses from the control flow graph. Note that here, we currently take into account the mutable ranges of instructions (see `mutate-after-useeffect-granular-access` fixture)
3. Collect the set of property paths accessed by the effect
4. Merge to get the set of minimal dependencies
2025-04-25 15:36:31 -04:00
46 changed files with 219 additions and 1781 deletions

View File

@@ -129,7 +129,6 @@ function run(
mode,
config,
contextIdentifiers,
func,
logger,
filename,
code,

View File

@@ -70,14 +70,12 @@ import {BuiltInArrayId} from './ObjectShape';
export function lower(
func: NodePath<t.Function>,
env: Environment,
// Bindings captured from the outer function, in case lower() is called recursively (for lambdas)
bindings: Bindings | null = null,
capturedRefs: Array<t.Identifier> = [],
// the outermost function being compiled, in case lower() is called recursively (for lambdas)
parent: NodePath<t.Function> | null = null,
): Result<HIRFunction, CompilerError> {
const builder = new HIRBuilder(env, {
bindings,
context: capturedRefs,
});
const builder = new HIRBuilder(env, parent ?? func, bindings, capturedRefs);
const context: HIRFunction['context'] = [];
for (const ref of capturedRefs ?? []) {
@@ -217,7 +215,7 @@ export function lower(
return Ok({
id,
params,
fnType: bindings == null ? env.fnType : 'Other',
fnType: parent == null ? env.fnType : 'Other',
returnTypeAnnotation: null, // TODO: extract the actual return type node if present
returnType: makeType(),
body: builder.build(),
@@ -3419,7 +3417,7 @@ function lowerFunction(
| t.ObjectMethod
>,
): LoweredFunction | null {
const componentScope: Scope = builder.environment.parentFunction.scope;
const componentScope: Scope = builder.parentFunction.scope;
const capturedContext = gatherCapturedContext(expr, componentScope);
/*
@@ -3430,10 +3428,13 @@ function lowerFunction(
* This isn't a problem in practice because use Babel's scope analysis to
* identify the correct references.
*/
const lowering = lower(expr, builder.environment, builder.bindings, [
...builder.context,
...capturedContext,
]);
const lowering = lower(
expr,
builder.environment,
builder.bindings,
[...builder.context, ...capturedContext],
builder.parentFunction,
);
let loweredFunc: HIRFunction;
if (lowering.isErr()) {
lowering
@@ -3455,7 +3456,7 @@ function lowerExpressionToTemporary(
return lowerValueToTemporary(builder, value);
}
export function lowerValueToTemporary(
function lowerValueToTemporary(
builder: HIRBuilder,
value: InstructionValue,
): Place {

View File

@@ -191,7 +191,6 @@ type TerminalRewriteInfo =
| {
kind: 'StartScope';
blockId: BlockId;
dependencyId: BlockId;
fallthroughId: BlockId;
instrId: InstructionId;
scope: ReactiveScope;
@@ -216,14 +215,12 @@ function pushStartScopeTerminal(
scope: ReactiveScope,
context: ScopeTraversalContext,
): void {
const dependencyId = context.env.nextBlockId;
const blockId = context.env.nextBlockId;
const fallthroughId = context.env.nextBlockId;
context.rewrites.push({
kind: 'StartScope',
blockId,
fallthroughId,
dependencyId,
instrId: scope.range.start,
scope,
});
@@ -265,13 +262,10 @@ type RewriteContext = {
* instr1, instr2, instr3, instr4, [[ original terminal ]]
* Rewritten:
* bb0:
* instr1, [[ scope start dependencies=bb1 block=bb2]]
* instr1, [[ scope start block=bb1]]
* bb1:
* [[ empty, filled in in PropagateScopeDependenciesHIR ]]
* goto bb2
* instr2, instr3, [[ scope end goto=bb2 ]]
* bb2:
* instr2, instr3, [[ scope end goto=bb3 ]]
* bb3:
* instr4, [[ original terminal ]]
*/
function handleRewrite(
@@ -285,7 +279,6 @@ function handleRewrite(
? {
kind: 'scope',
fallthrough: terminalInfo.fallthroughId,
dependencies: terminalInfo.dependencyId,
block: terminalInfo.blockId,
scope: terminalInfo.scope,
id: terminalInfo.instrId,
@@ -312,28 +305,7 @@ function handleRewrite(
context.nextPreds = new Set([currBlockId]);
context.nextBlockId =
terminalInfo.kind === 'StartScope'
? terminalInfo.dependencyId
? terminalInfo.blockId
: terminalInfo.fallthroughId;
context.instrSliceIdx = idx;
if (terminalInfo.kind === 'StartScope') {
const currBlockId = context.nextBlockId;
context.rewrites.push({
kind: context.source.kind,
id: currBlockId,
instructions: [],
preds: context.nextPreds,
// Only the first rewrite should reuse source block phis
phis: new Set(),
terminal: {
kind: 'goto',
variant: GotoVariant.Break,
block: terminal.block,
id: terminalInfo.instrId,
loc: GeneratedSource,
},
});
context.nextPreds = new Set([currBlockId]);
context.nextBlockId = terminalInfo.blockId;
}
}

View File

@@ -241,10 +241,7 @@ type PropertyPathNode =
class PropertyPathRegistry {
roots: Map<IdentifierId, RootNode> = new Map();
getOrCreateIdentifier(
identifier: Identifier,
reactive: boolean,
): PropertyPathNode {
getOrCreateIdentifier(identifier: Identifier): PropertyPathNode {
/**
* Reads from a statically scoped variable are always safe in JS,
* with the exception of TDZ (not addressed by this pass).
@@ -258,19 +255,12 @@ class PropertyPathRegistry {
optionalProperties: new Map(),
fullPath: {
identifier,
reactive,
path: [],
},
hasOptional: false,
parent: null,
};
this.roots.set(identifier.id, rootNode);
} else {
CompilerError.invariant(reactive === rootNode.fullPath.reactive, {
reason:
'[HoistablePropertyLoads] Found inconsistencies in `reactive` flag when deduping identifier reads within the same scope',
loc: identifier.loc,
});
}
return rootNode;
}
@@ -288,7 +278,6 @@ class PropertyPathRegistry {
parent: parent,
fullPath: {
identifier: parent.fullPath.identifier,
reactive: parent.fullPath.reactive,
path: parent.fullPath.path.concat(entry),
},
hasOptional: parent.hasOptional || entry.optional,
@@ -304,7 +293,7 @@ class PropertyPathRegistry {
* so all subpaths of a PropertyLoad should already exist
* (e.g. a.b is added before a.b.c),
*/
let currNode = this.getOrCreateIdentifier(n.identifier, n.reactive);
let currNode = this.getOrCreateIdentifier(n.identifier);
if (n.path.length === 0) {
return currNode;
}
@@ -326,11 +315,10 @@ function getMaybeNonNullInInstruction(
instr: InstructionValue,
context: CollectHoistablePropertyLoadsContext,
): PropertyPathNode | null {
let path: ReactiveScopeDependency | null = null;
let path = null;
if (instr.kind === 'PropertyLoad') {
path = context.temporaries.get(instr.object.identifier.id) ?? {
identifier: instr.object.identifier,
reactive: instr.object.reactive,
path: [],
};
} else if (instr.kind === 'Destructure') {
@@ -393,7 +381,7 @@ function collectNonNullsInBlocks(
) {
const identifier = fn.params[0].identifier;
knownNonNullIdentifiers.add(
context.registry.getOrCreateIdentifier(identifier, true),
context.registry.getOrCreateIdentifier(identifier),
);
}
const nodes = new Map<
@@ -628,11 +616,9 @@ function reduceMaybeOptionalChains(
changed = false;
for (const original of optionalChainNodes) {
let {identifier, path: origPath, reactive} = original.fullPath;
let currNode: PropertyPathNode = registry.getOrCreateIdentifier(
identifier,
reactive,
);
let {identifier, path: origPath} = original.fullPath;
let currNode: PropertyPathNode =
registry.getOrCreateIdentifier(identifier);
for (let i = 0; i < origPath.length; i++) {
const entry = origPath[i];
// If the base is known to be non-null, replace with a non-optional load

View File

@@ -114,7 +114,7 @@ export type OptionalChainSidemap = {
hoistableObjects: ReadonlyMap<BlockId, ReactiveScopeDependency>;
};
export type OptionalTraversalContext = {
type OptionalTraversalContext = {
currFn: HIRFunction;
blocks: ReadonlyMap<BlockId, BasicBlock>;
@@ -235,7 +235,7 @@ function matchOptionalTestBlock(
* property loads. If any part of the optional chain is not hoistable, returns
* null.
*/
export function traverseOptionalBlock(
function traverseOptionalBlock(
optional: TBasicBlock<OptionalTerminal>,
context: OptionalTraversalContext,
outerAlternate: BlockId | null,
@@ -290,7 +290,6 @@ export function traverseOptionalBlock(
);
baseObject = {
identifier: maybeTest.instructions[0].value.place.identifier,
reactive: maybeTest.instructions[0].value.place.reactive,
path,
};
test = maybeTest.terminal;
@@ -392,7 +391,6 @@ export function traverseOptionalBlock(
);
const load = {
identifier: baseObject.identifier,
reactive: baseObject.reactive,
path: [
...baseObject.path,
{

View File

@@ -25,9 +25,8 @@ export class ReactiveScopeDependencyTreeHIR {
* `identifier.path`, or `identifier?.path` is in this map, it is safe to
* evaluate (non-optional) PropertyLoads from.
*/
#hoistableObjects: Map<Identifier, HoistableNode & {reactive: boolean}> =
new Map();
#deps: Map<Identifier, DependencyNode & {reactive: boolean}> = new Map();
#hoistableObjects: Map<Identifier, HoistableNode> = new Map();
#deps: Map<Identifier, DependencyNode> = new Map();
/**
* @param hoistableObjects a set of paths from which we can safely evaluate
@@ -36,10 +35,9 @@ export class ReactiveScopeDependencyTreeHIR {
* duplicates when traversing the CFG.
*/
constructor(hoistableObjects: Iterable<ReactiveScopeDependency>) {
for (const {path, identifier, reactive} of hoistableObjects) {
for (const {path, identifier} of hoistableObjects) {
let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
identifier,
reactive,
this.#hoistableObjects,
path.length > 0 && path[0].optional ? 'Optional' : 'NonNull',
);
@@ -72,8 +70,7 @@ export class ReactiveScopeDependencyTreeHIR {
static #getOrCreateRoot<T extends string>(
identifier: Identifier,
reactive: boolean,
roots: Map<Identifier, TreeNode<T> & {reactive: boolean}>,
roots: Map<Identifier, TreeNode<T>>,
defaultAccessType: T,
): TreeNode<T> {
// roots can always be accessed unconditionally in JS
@@ -82,16 +79,9 @@ export class ReactiveScopeDependencyTreeHIR {
if (rootNode === undefined) {
rootNode = {
properties: new Map(),
reactive,
accessType: defaultAccessType,
};
roots.set(identifier, rootNode);
} else {
CompilerError.invariant(reactive === rootNode.reactive, {
reason: '[DeriveMinimalDependenciesHIR] Conflicting reactive root flag',
description: `Identifier ${printIdentifier(identifier)}`,
loc: GeneratedSource,
});
}
return rootNode;
}
@@ -102,10 +92,9 @@ export class ReactiveScopeDependencyTreeHIR {
* safe-to-evaluate subpath
*/
addDependency(dep: ReactiveScopeDependency): void {
const {identifier, reactive, path} = dep;
const {identifier, path} = dep;
let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot(
identifier,
reactive,
this.#deps,
PropertyAccessType.UnconditionalAccess,
);
@@ -183,13 +172,7 @@ export class ReactiveScopeDependencyTreeHIR {
deriveMinimalDependencies(): Set<ReactiveScopeDependency> {
const results = new Set<ReactiveScopeDependency>();
for (const [rootId, rootNode] of this.#deps.entries()) {
collectMinimalDependenciesInSubtree(
rootNode,
rootNode.reactive,
rootId,
[],
results,
);
collectMinimalDependenciesInSubtree(rootNode, rootId, [], results);
}
return results;
@@ -311,24 +294,25 @@ type HoistableNode = TreeNode<'Optional' | 'NonNull'>;
type DependencyNode = TreeNode<PropertyAccessType>;
/**
* TODO: this is directly pasted from DeriveMinimalDependencies. Since we no
* longer have conditionally accessed nodes, we can simplify
*
* Recursively calculates minimal dependencies in a subtree.
* @param node DependencyNode representing a dependency subtree.
* @returns a minimal list of dependencies in this subtree.
*/
function collectMinimalDependenciesInSubtree(
node: DependencyNode,
reactive: boolean,
rootIdentifier: Identifier,
path: Array<DependencyPathEntry>,
results: Set<ReactiveScopeDependency>,
): void {
if (isDependency(node.accessType)) {
results.add({identifier: rootIdentifier, reactive, path});
results.add({identifier: rootIdentifier, path});
} else {
for (const [childName, childNode] of node.properties) {
collectMinimalDependenciesInSubtree(
childNode,
reactive,
rootIdentifier,
[
...path,

View File

@@ -55,7 +55,7 @@ import {
ShapeRegistry,
addHook,
} from './ObjectShape';
import {Scope as BabelScope, NodePath} from '@babel/traverse';
import {Scope as BabelScope} from '@babel/traverse';
import {TypeSchema} from './TypeSchema';
export const ReactElementSymbolSchema = z.object({
@@ -863,7 +863,6 @@ export class Environment {
#contextIdentifiers: Set<t.Identifier>;
#hoistedIdentifiers: Set<t.Identifier>;
parentFunction: NodePath<t.Function>;
constructor(
scope: BabelScope,
@@ -871,7 +870,6 @@ export class Environment {
compilerMode: CompilerMode,
config: EnvironmentConfig,
contextIdentifiers: Set<t.Identifier>,
parentFunction: NodePath<t.Function>, // the outermost function being compiled
logger: Logger | null,
filename: string | null,
code: string | null,
@@ -930,7 +928,6 @@ export class Environment {
this.#moduleTypes.set(REANIMATED_MODULE_NAME, reanimatedModuleType);
}
this.parentFunction = parentFunction;
this.#contextIdentifiers = contextIdentifiers;
this.#hoistedIdentifiers = new Set();
}

View File

@@ -62,14 +62,12 @@ export type ReactiveFunction = {
export type ReactiveScopeBlock = {
kind: 'scope';
dependencyInstructions: Array<ReactiveInstructionStatement>;
scope: ReactiveScope;
instructions: ReactiveBlock;
};
export type PrunedReactiveScopeBlock = {
kind: 'pruned-scope';
dependencyInstructions: Array<ReactiveInstructionStatement>;
scope: ReactiveScope;
instructions: ReactiveBlock;
};
@@ -616,7 +614,6 @@ export type MaybeThrowTerminal = {
export type ReactiveScopeTerminal = {
kind: 'scope';
fallthrough: BlockId;
dependencies: BlockId;
block: BlockId;
scope: ReactiveScope;
id: InstructionId;
@@ -626,7 +623,6 @@ export type ReactiveScopeTerminal = {
export type PrunedScopeTerminal = {
kind: 'pruned-scope';
fallthrough: BlockId;
dependencies: BlockId;
block: BlockId;
scope: ReactiveScope;
id: InstructionId;
@@ -1476,10 +1472,9 @@ export type ReactiveScope = {
range: MutableRange;
/**
* Note the dependencies of a reactive scope are tracked in HIR and
* ReactiveFunction
* The inputs to this reactive scope
*/
dependencies: Array<Place>;
dependencies: ReactiveScopeDependencies;
/**
* The set of values produced by this scope. This may be empty
@@ -1540,18 +1535,6 @@ export type DependencyPathEntry = {
export type DependencyPath = Array<DependencyPathEntry>;
export type ReactiveScopeDependency = {
identifier: Identifier;
/**
* Reflects whether the base identifier is reactive. Note that some reactive
* objects may have non-reactive properties, but we do not currently track
* this.
*
* ```js
* // Technically, result[0] is reactive and result[1] is not.
* // Currently, both dependencies would be marked as reactive.
* const result = useState();
* ```
*/
reactive: boolean;
path: DependencyPath;
};
@@ -1755,40 +1738,6 @@ export function isStableType(id: Identifier): boolean {
);
}
export function isStableTypeContainer(id: Identifier): boolean {
const type_ = id.type;
if (type_.kind !== 'Object') {
return false;
}
return (
isUseStateType(id) || // setState
type_.shapeId === 'BuiltInUseActionState' || // setActionState
isUseReducerType(id) || // dispatcher
type_.shapeId === 'BuiltInUseTransition' // startTransition
);
}
export function evaluatesToStableTypeOrContainer(
env: Environment,
{value}: Instruction,
): boolean {
if (value.kind === 'CallExpression' || value.kind === 'MethodCall') {
const callee =
value.kind === 'CallExpression' ? value.callee : value.property;
const calleeHookKind = getHookKind(env, callee.identifier);
switch (calleeHookKind) {
case 'useState':
case 'useReducer':
case 'useActionState':
case 'useRef':
case 'useTransition':
return true;
}
}
return false;
}
export function isUseEffectHookType(id: Identifier): boolean {
return (
id.type.kind === 'Function' && id.type.shapeId === 'BuiltInUseEffectHook'

View File

@@ -110,6 +110,7 @@ export default class HIRBuilder {
#bindings: Bindings;
#env: Environment;
#exceptionHandlerStack: Array<BlockId> = [];
parentFunction: NodePath<t.Function>;
errors: CompilerError = new CompilerError();
/**
* Traversal context: counts the number of `fbt` tag parents
@@ -135,17 +136,16 @@ export default class HIRBuilder {
constructor(
env: Environment,
options?: {
bindings?: Bindings | null;
context?: Array<t.Identifier>;
entryBlockKind?: BlockKind;
},
parentFunction: NodePath<t.Function>, // the outermost function being compiled
bindings: Bindings | null = null,
context: Array<t.Identifier> | null = null,
) {
this.#env = env;
this.#bindings = options?.bindings ?? new Map();
this.#context = options?.context ?? [];
this.#bindings = bindings ?? new Map();
this.parentFunction = parentFunction;
this.#context = context ?? [];
this.#entry = makeBlockId(env.nextBlockId);
this.#current = newBlock(this.#entry, options?.entryBlockKind ?? 'block');
this.#current = newBlock(this.#entry, 'block');
}
currentBlockKind(): BlockKind {
@@ -239,7 +239,7 @@ export default class HIRBuilder {
// Check if the binding is from module scope
const outerBinding =
this.#env.parentFunction.scope.parent.getBinding(originalName);
this.parentFunction.scope.parent.getBinding(originalName);
if (babelBinding === outerBinding) {
const path = babelBinding.path;
if (path.isImportDefaultSpecifier()) {
@@ -293,7 +293,7 @@ export default class HIRBuilder {
const binding = this.#resolveBabelBinding(path);
if (binding) {
// Check if the binding is from module scope, if so return null
const outerBinding = this.#env.parentFunction.scope.parent.getBinding(
const outerBinding = this.parentFunction.scope.parent.getBinding(
path.node.name,
);
if (binding === outerBinding) {
@@ -376,7 +376,7 @@ export default class HIRBuilder {
}
// Terminate the current block w the given terminal, and start a new block
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): BlockId {
terminate(terminal: Terminal, nextBlockKind: BlockKind | null): void {
const {id: blockId, kind, instructions} = this.#current;
this.#completed.set(blockId, {
kind,
@@ -390,7 +390,6 @@ export default class HIRBuilder {
const nextId = this.#env.nextBlockId;
this.#current = newBlock(nextId, nextBlockKind);
}
return blockId;
}
/*
@@ -747,11 +746,6 @@ function getReversePostorderedBlocks(func: HIR): HIR['blocks'] {
* (eg bb2 then bb1), we ensure that they get reversed back to the correct order.
*/
const block = func.blocks.get(blockId)!;
CompilerError.invariant(block != null, {
reason: '[HIRBuilder] Unexpected null block',
description: `expected block ${blockId} to exist`,
loc: GeneratedSource,
});
const successors = [...eachTerminalSuccessor(block.terminal)].reverse();
const fallthrough = terminalFallthrough(block.terminal);

View File

@@ -286,13 +286,13 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
case 'scope': {
value = `[${terminal.id}] Scope ${printReactiveScopeSummary(
terminal.scope,
)} dependencies=bb${terminal.dependencies} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
)} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
break;
}
case 'pruned-scope': {
value = `[${terminal.id}] <pruned> Scope ${printReactiveScopeSummary(
terminal.scope,
)} dependencies=bb${terminal.dependencies} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
)} block=bb${terminal.block} fallthrough=bb${terminal.fallthrough}`;
break;
}
case 'try': {

View File

@@ -47,17 +47,6 @@ import {CompilerError} from '../CompilerError';
import {Iterable_some} from '../Utils/utils';
import {ReactiveScopeDependencyTreeHIR} from './DeriveMinimalDependenciesHIR';
import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies';
import {
fixScopeAndIdentifierRanges,
markInstructionIds,
markPredecessors,
reversePostorderBlocks,
} from './HIRBuilder';
import {printDependency} from '../ReactiveScopes/PrintReactiveFunction';
import {
readScopeDependencies,
writeScopeDependencies,
} from './ScopeDependencyUtils';
export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
const usedOutsideDeclaringScope =
@@ -84,10 +73,8 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
/**
* Derive the minimal set of hoistable dependencies for each scope.
*/
const minimalDeps = new Map<ReactiveScope, Set<ReactiveScopeDependency>>();
for (const [scope, deps] of scopeDeps) {
if (deps.length === 0) {
minimalDeps.set(scope, new Set());
continue;
}
@@ -114,79 +101,17 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void {
* Step 3: Reduce dependencies to a minimal set.
*/
const candidates = tree.deriveMinimalDependencies();
const dependencies = new Set<ReactiveScopeDependency>();
for (const candidateDep of candidates) {
if (
!Iterable_some(
dependencies,
scope.dependencies,
existingDep =>
existingDep.identifier.declarationId ===
candidateDep.identifier.declarationId &&
areEqualPaths(existingDep.path, candidateDep.path),
)
)
dependencies.add(candidateDep);
}
minimalDeps.set(scope, dependencies);
}
let changed = false;
/**
* Step 4: inject dependencies
*/
for (const [_, {terminal}] of fn.body.blocks) {
if (terminal.kind !== 'scope' && terminal.kind !== 'pruned-scope') {
continue;
}
const scope = terminal.scope;
const deps = minimalDeps.get(scope);
if (deps == null || deps.size === 0) {
continue;
}
writeScopeDependencies(terminal, deps, fn);
changed = true;
}
/**
* Step 5: fix scope and identifier ranges to account for renumbered
* instructions
*/
if (changed) {
reversePostorderBlocks(fn.body);
markPredecessors(fn.body);
markInstructionIds(fn.body);
fixScopeAndIdentifierRanges(fn.body);
}
// Sanity check
{
for (const [scope, deps] of minimalDeps) {
const checkedDeps = [...readScopeDependencies(fn, scope.id)];
CompilerError.invariant(checkedDeps != null, {
reason: '[Rewrite] Cannot find scope dep when reading',
loc: scope.loc,
});
CompilerError.invariant(checkedDeps.length === deps.size, {
reason: '[Rewrite] non matching sizes when reading',
description: `scopeId=${scope.id} deps=${[...deps].map(printDependency)} checkedDeps=${[...checkedDeps].map(printDependency)}`,
loc: scope.loc,
});
for (const dep of deps) {
CompilerError.invariant(
checkedDeps.some(
checkedDep =>
dep.identifier === checkedDep.identifier &&
areEqualPaths(dep.path, checkedDep.path),
),
{
reason:
'[Rewrite] could not find match for dependency when re-reading',
description: `${printDependency(dep)}`,
loc: scope.loc,
},
);
}
scope.dependencies.add(candidateDep);
}
}
}
@@ -384,7 +309,6 @@ function collectTemporariesSidemapImpl(
) {
temporaries.set(lvalue.identifier.id, {
identifier: value.place.identifier,
reactive: value.place.reactive,
path: [],
});
}
@@ -438,13 +362,11 @@ function getProperty(
if (resolvedDependency == null) {
property = {
identifier: object.identifier,
reactive: object.reactive,
path: [{property: propertyName, optional}],
};
} else {
property = {
identifier: resolvedDependency.identifier,
reactive: resolvedDependency.reactive,
path: [...resolvedDependency.path, {property: propertyName, optional}],
};
}
@@ -600,7 +522,6 @@ export class DependencyCollectionContext {
this.visitDependency(
this.#temporaries.get(place.identifier.id) ?? {
identifier: place.identifier,
reactive: place.reactive,
path: [],
},
);
@@ -665,7 +586,6 @@ export class DependencyCollectionContext {
) {
maybeDependency = {
identifier: maybeDependency.identifier,
reactive: maybeDependency.reactive,
path: [],
};
}
@@ -687,11 +607,7 @@ export class DependencyCollectionContext {
identifier =>
identifier.declarationId === place.identifier.declarationId,
) &&
this.#checkValidDependency({
identifier: place.identifier,
reactive: place.reactive,
path: [],
})
this.#checkValidDependency({identifier: place.identifier, path: []})
) {
currentScope.reassignments.add(place.identifier);
}

View File

@@ -1,569 +0,0 @@
import {
ScopeId,
HIRFunction,
Place,
ReactiveScopeDependency,
Identifier,
makeInstructionId,
InstructionKind,
GeneratedSource,
IdentifierId,
BlockId,
makeTemporaryIdentifier,
Effect,
OptionalTerminal,
TBasicBlock,
ReactiveScopeTerminal,
GotoVariant,
PrunedScopeTerminal,
ReactiveInstruction,
ReactiveValue,
ReactiveScopeBlock,
PrunedReactiveScopeBlock,
} from './HIR';
import {CompilerError} from '../CompilerError';
import {
OptionalTraversalContext,
traverseOptionalBlock,
} from './CollectOptionalChainDependencies';
import {Environment} from './Environment';
import HIRBuilder from './HIRBuilder';
import {lowerValueToTemporary} from './BuildHIR';
import {printDependency} from '../ReactiveScopes/PrintReactiveFunction';
import {printPlace} from './PrintHIR';
function writeNonOptionalDependency(
dep: ReactiveScopeDependency,
env: Environment,
builder: HIRBuilder,
): Identifier {
const loc = dep.identifier.loc;
let last: Identifier = makeTemporaryIdentifier(env.nextIdentifierId, loc);
builder.push({
lvalue: {
identifier: last,
kind: 'Identifier',
effect: Effect.Mutate,
reactive: dep.reactive,
loc,
},
value: {
kind: 'LoadLocal',
place: {
identifier: dep.identifier,
kind: 'Identifier',
effect: Effect.Freeze,
reactive: dep.reactive,
loc,
},
loc,
},
id: makeInstructionId(1),
loc: loc,
});
for (const path of dep.path) {
const next = makeTemporaryIdentifier(env.nextIdentifierId, loc);
builder.push({
lvalue: {
identifier: next,
kind: 'Identifier',
effect: Effect.Mutate,
reactive: dep.reactive,
loc,
},
value: {
kind: 'PropertyLoad',
object: {
identifier: last,
kind: 'Identifier',
effect: Effect.Freeze,
reactive: dep.reactive,
loc,
},
property: path.property,
loc,
},
id: makeInstructionId(1),
loc: loc,
});
last = next;
}
return last;
}
export function writeScopeDependencies(
terminal: ReactiveScopeTerminal | PrunedScopeTerminal,
deps: Set<ReactiveScopeDependency>,
fn: HIRFunction,
): void {
const scopeDepBlock = fn.body.blocks.get(terminal.dependencies);
CompilerError.invariant(scopeDepBlock != null, {
reason: 'Expected to find scope dependency block',
loc: terminal.loc,
});
CompilerError.invariant(
scopeDepBlock.instructions.length === 0 &&
scopeDepBlock.terminal.kind === 'goto' &&
scopeDepBlock.terminal.block === terminal.block,
{
reason: 'Expected scope.dependencies to be a goto block (invalid cfg)',
loc: terminal.loc,
},
);
const builder = new HIRBuilder(fn.env, {
entryBlockKind: 'value',
});
for (const dep of deps) {
if (dep.path.every(path => !path.optional)) {
const last = writeNonOptionalDependency(dep, fn.env, builder);
terminal.scope.dependencies.push({
kind: 'Identifier',
identifier: last,
effect: Effect.Freeze,
reactive: dep.reactive,
loc: GeneratedSource,
});
}
}
// Write all optional chaining deps
for (const dep of deps) {
if (!dep.path.every(path => !path.optional)) {
const last = writeOptional(
dep.path.length - 1,
dep,
builder,
terminal,
null,
);
terminal.scope.dependencies.push({
kind: 'Identifier',
identifier: last,
effect: Effect.Freeze,
reactive: dep.reactive,
loc: GeneratedSource,
});
}
}
// Placeholder terminal for HIRBuilder, to be later replaced by a `goto` to an outer block
const lastBlockId = builder.terminate(
{
kind: 'return',
value: {
kind: 'Identifier',
identifier: makeTemporaryIdentifier(
fn.env.nextIdentifierId,
GeneratedSource,
),
effect: Effect.Freeze,
loc: GeneratedSource,
reactive: true,
},
loc: GeneratedSource,
id: makeInstructionId(0),
},
null,
);
const dependenciesHIR = builder.build();
for (const [id, block] of dependenciesHIR.blocks) {
fn.body.blocks.set(id, block);
}
fn.body.blocks.delete(terminal.dependencies);
/**
* Connect the newly constructed inner HIR to the outer HIR
*/
terminal.dependencies = dependenciesHIR.entry;
// Rewire the replaceholder terminal to the correct goto
fn.body.blocks.get(lastBlockId)!.terminal = scopeDepBlock.terminal;
}
function writeOptional(
idx: number,
dep: ReactiveScopeDependency,
builder: HIRBuilder,
terminal: ReactiveScopeTerminal | PrunedScopeTerminal,
parentAlternate: BlockId | null,
): Identifier {
const env = builder.environment;
CompilerError.invariant(
idx >= 0 && !dep.path.slice(0, idx + 1).every(path => !path.optional),
{
reason: '[WriteOptional] Expected optional path',
description: `${idx} ${printDependency(dep)}`,
loc: GeneratedSource,
},
);
const continuationBlock = builder.reserve(builder.currentBlockKind());
const consequent = builder.reserve('value');
const returnPlace: Place = {
kind: 'Identifier',
identifier: makeTemporaryIdentifier(env.nextIdentifierId, GeneratedSource),
effect: Effect.Mutate,
reactive: dep.reactive,
loc: GeneratedSource,
};
let alternate;
if (parentAlternate != null) {
alternate = parentAlternate;
} else {
/**
* Make outermost alternate block
* $N = Primitive undefined
* $M = StoreLocal $OptionalResult = $N
* goto fallthrough
*/
alternate = builder.enter('value', () => {
const temp = lowerValueToTemporary(builder, {
kind: 'Primitive',
value: undefined,
loc: GeneratedSource,
});
lowerValueToTemporary(builder, {
kind: 'StoreLocal',
lvalue: {kind: InstructionKind.Const, place: {...returnPlace}},
value: {...temp},
type: null,
loc: GeneratedSource,
});
return {
kind: 'goto',
variant: GotoVariant.Break,
block: continuationBlock.id,
id: makeInstructionId(0),
loc: GeneratedSource,
};
});
}
let testIdentifier: Identifier | null = null;
const testBlock = builder.enter('value', () => {
const firstOptional = dep.path.findIndex(path => path.optional);
if (idx === firstOptional) {
// Lower test block
testIdentifier = writeNonOptionalDependency(
{
identifier: dep.identifier,
reactive: dep.reactive,
path: dep.path.slice(0, idx),
},
env,
builder,
);
} else {
testIdentifier = writeOptional(
idx - 1,
dep,
builder,
terminal,
alternate,
);
}
return {
kind: 'branch',
test: {
identifier: testIdentifier,
effect: Effect.Freeze,
kind: 'Identifier',
loc: GeneratedSource,
reactive: dep.reactive,
},
consequent: consequent.id,
alternate,
id: makeInstructionId(0),
loc: GeneratedSource,
fallthrough: continuationBlock.id,
};
});
builder.enterReserved(consequent, () => {
CompilerError.invariant(testIdentifier !== null, {
reason: 'Satisfy type checker',
description: null,
loc: null,
suggestions: null,
});
const tmpConsequent = lowerValueToTemporary(builder, {
kind: 'PropertyLoad',
object: {
identifier: testIdentifier,
kind: 'Identifier',
effect: Effect.Freeze,
reactive: dep.reactive,
loc: GeneratedSource,
},
property: dep.path[idx].property,
loc: GeneratedSource,
});
lowerValueToTemporary(builder, {
kind: 'StoreLocal',
lvalue: {kind: InstructionKind.Const, place: {...returnPlace}},
value: {...tmpConsequent},
type: null,
loc: GeneratedSource,
});
return {
kind: 'goto',
variant: GotoVariant.Break,
block: continuationBlock.id,
id: makeInstructionId(0),
loc: GeneratedSource,
};
});
builder.terminateWithContinuation(
{
kind: 'optional',
optional: dep.path[idx].optional,
test: testBlock,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc: GeneratedSource,
},
continuationBlock,
);
return returnPlace.identifier;
}
export function readScopeDependencies(
fn: HIRFunction,
scope: ScopeId,
): Set<ReactiveScopeDependency> {
for (const [_, {terminal}] of fn.body.blocks) {
if (terminal.kind !== 'scope' && terminal.kind !== 'pruned-scope') {
continue;
}
if (terminal.scope.id !== scope) {
continue;
}
const temporaries = new Map<IdentifierId, ReactiveScopeDependency>();
const context: OptionalTraversalContext = {
currFn: fn,
blocks: fn.body.blocks,
seenOptionals: new Set(),
processedInstrsInOptional: new Set(),
temporariesReadInOptional: temporaries,
hoistableObjects: new Map(),
};
/**
* Step 1: read all instructions between within scope dependencies block(s)
*/
let work = terminal.dependencies;
while (true) {
const block = fn.body.blocks.get(work)!;
for (const {lvalue, value} of block.instructions) {
if (value.kind === 'LoadLocal') {
temporaries.set(lvalue.identifier.id, {
identifier: value.place.identifier,
reactive: value.place.reactive,
path: [],
});
} else if (value.kind === 'PropertyLoad') {
const source = temporaries.get(value.object.identifier.id)!;
temporaries.set(lvalue.identifier.id, {
identifier: source.identifier,
reactive: source.reactive,
path: [...source.path, {property: value.property, optional: false}],
});
}
}
if (block.terminal.kind === 'optional') {
traverseOptionalBlock(
block as TBasicBlock<OptionalTerminal>,
context,
null,
);
work = block.terminal.fallthrough;
} else {
CompilerError.invariant(
block.terminal.kind === 'goto' &&
block.terminal.block === terminal.block,
{
reason: 'unexpected terminal',
description: `kind: ${block.terminal.kind}`,
loc: block.terminal.loc,
},
);
break;
}
}
/**
* Step 2: look up scope dependencies from the temporaries sidemap
*/
const scopeOwnDependencies = new Set<ReactiveScopeDependency>();
for (const dep of terminal.scope.dependencies) {
const reactiveScopeDependency = temporaries.get(dep.identifier.id)!;
CompilerError.invariant(reactiveScopeDependency != null, {
reason: 'Expected dependency to be found',
description: `${printPlace(dep)}`,
loc: terminal.scope.loc,
});
scopeOwnDependencies.add(reactiveScopeDependency);
}
return scopeOwnDependencies;
}
CompilerError.invariant(false, {
reason: 'Expected scope to be found',
loc: GeneratedSource,
});
}
function assertNonNull<T>(value: T | null | undefined): T {
if (value == null) {
throw new Error('Expected nonnull value');
}
return value;
}
function readScopeDependenciesRHIRInstr(
instr: ReactiveInstruction,
sidemap: Map<IdentifierId, ReactiveScopeDependency>,
): void {
const value = reacScopeDependenciesRHIRValue(instr.value, sidemap);
if (instr.lvalue != null) {
sidemap.set(instr.lvalue.identifier.id, value);
}
}
function reacScopeDependenciesRHIRValue(
instr: ReactiveValue,
sidemap: Map<IdentifierId, ReactiveScopeDependency>,
): ReactiveScopeDependency {
if (instr.kind === 'LoadLocal') {
const base = sidemap.get(instr.place.identifier.id);
if (base != null) {
return base;
} else {
return {
identifier: instr.place.identifier,
reactive: instr.place.reactive,
path: [],
};
}
} else if (instr.kind === 'PropertyLoad') {
const base = assertNonNull(sidemap.get(instr.object.identifier.id));
return {
identifier: base.identifier,
reactive: base.reactive,
path: [...base.path, {property: instr.property, optional: false}],
};
} else if (instr.kind === 'SequenceExpression') {
for (const inner of instr.instructions) {
readScopeDependenciesRHIRInstr(inner, sidemap);
}
return reacScopeDependenciesRHIRValue(instr.value, sidemap);
} else if (instr.kind === 'OptionalExpression') {
const value = reacScopeDependenciesRHIRValue(instr.value, sidemap);
CompilerError.invariant(
value.path.length > 0 && !value.path.at(-1)!.optional,
{
reason: 'Expected optional chain to be nonempty',
loc: instr.loc,
},
);
return {
...value,
path: [
...value.path.slice(0, -1),
{property: value.path.at(-1)!.property, optional: instr.optional},
],
};
}
CompilerError.invariant(false, {
reason: 'Unexpected value kind',
description: instr.kind,
loc: instr.loc,
});
}
export function readScopeDependenciesRHIR(
scopeBlock: ReactiveScopeBlock | PrunedReactiveScopeBlock,
): Map<Place, ReactiveScopeDependency> {
const sidemap = new Map<IdentifierId, ReactiveScopeDependency>();
for (const instr of scopeBlock.dependencyInstructions) {
readScopeDependenciesRHIRInstr(instr.instruction, sidemap);
}
return new Map<Place, ReactiveScopeDependency>(
scopeBlock.scope.dependencies.map(place => {
return [place, assertNonNull(sidemap.get(place.identifier.id))];
}),
);
}
/**
* Run DCE to delete instructions which are not used by any scope dependencies
*
* Note: this only handles simple pruning i.e. deleted dependency entries,
* not more complex rewrites such as pruned or edited entries.
*/
export function scopeDependenciesDCE(
scopeBlock: ReactiveScopeBlock | PrunedReactiveScopeBlock,
): void {
const usage = new Map<IdentifierId, IdentifierId | null>();
for (const {
instruction: {lvalue, value},
} of scopeBlock.dependencyInstructions) {
if (lvalue == null) {
continue;
}
switch (value.kind) {
case 'LoadLocal': {
usage.set(lvalue.identifier.id, null);
break;
}
case 'PropertyLoad': {
usage.set(lvalue.identifier.id, value.object.identifier.id);
break;
}
case 'OptionalExpression': {
usage.set(lvalue.identifier.id, null);
break;
}
default: {
CompilerError.invariant(false, {
reason: 'Unexpected value kind',
description: value.kind,
loc: value.loc,
});
}
}
}
const notUsed = new Set(usage.keys());
const seen = new Set();
for (const {identifier} of scopeBlock.scope.dependencies) {
let curr: IdentifierId | undefined | null = identifier.id;
while (curr != null) {
CompilerError.invariant(!seen.has(curr), {
reason: 'infinite loop',
loc: GeneratedSource,
});
notUsed.delete(curr);
seen.add(curr);
curr = usage.get(curr);
}
}
/**
* Remove unused instructions in place
*/
let j = 0;
for (let i = 0; i < scopeBlock.dependencyInstructions.length; i++) {
const instr = scopeBlock.dependencyInstructions[i].instruction;
if (instr.lvalue != null && !notUsed.has(instr.lvalue.identifier.id)) {
scopeBlock.dependencyInstructions[j] =
scopeBlock.dependencyInstructions[i];
j++;
}
}
scopeBlock.dependencyInstructions.length = j;
}

View File

@@ -860,13 +860,11 @@ export function mapTerminalSuccessors(
}
case 'scope':
case 'pruned-scope': {
const dependencies = fn(terminal.dependencies);
const block = fn(terminal.block);
const fallthrough = fn(terminal.fallthrough);
return {
kind: terminal.kind,
scope: terminal.scope,
dependencies,
block,
fallthrough,
id: makeInstructionId(0),
@@ -1019,7 +1017,7 @@ export function* eachTerminalSuccessor(terminal: Terminal): Iterable<BlockId> {
}
case 'scope':
case 'pruned-scope': {
yield terminal.dependencies;
yield terminal.block;
break;
}
case 'unreachable':
@@ -1070,13 +1068,6 @@ export function mapTerminalOperands(
}
break;
}
case 'scope':
case 'pruned-scope': {
for (let i = 0; i < terminal.scope.dependencies.length; i++) {
terminal.scope.dependencies[i] = fn(terminal.scope.dependencies[i]);
}
break;
}
case 'maybe-throw':
case 'sequence':
case 'label':
@@ -1090,7 +1081,9 @@ export function mapTerminalOperands(
case 'for-in':
case 'goto':
case 'unreachable':
case 'unsupported': {
case 'unsupported':
case 'scope':
case 'pruned-scope': {
// no-op
break;
}
@@ -1134,13 +1127,6 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
}
break;
}
case 'scope':
case 'pruned-scope': {
for (const dep of terminal.scope.dependencies) {
yield dep;
}
break;
}
case 'maybe-throw':
case 'sequence':
case 'label':
@@ -1154,7 +1140,9 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable<Place> {
case 'for-in':
case 'goto':
case 'unreachable':
case 'unsupported': {
case 'unsupported':
case 'scope':
case 'pruned-scope': {
// no-op
break;
}

View File

@@ -44,7 +44,6 @@ import {
DependencyCollectionContext,
handleInstruction,
} from '../HIR/PropagateScopeDependenciesHIR';
import {readScopeDependencies} from '../HIR/ScopeDependencyUtils';
import {eachInstructionOperand, eachTerminalOperand} from '../HIR/visitors';
import {empty} from '../Utils/Stack';
import {getOrInsertWith} from '../Utils/utils';
@@ -98,7 +97,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
) {
scopeInfos.set(
block.terminal.scope.id,
readScopeDependencies(fn, block.terminal.scope.id),
block.terminal.scope.dependencies,
);
}
}
@@ -486,7 +485,7 @@ function inferDependencies(
start: fnInstr.id,
end: makeInstructionId(fnInstr.id + 1),
},
dependencies: [],
dependencies: new Set(),
reassignments: new Set(),
declarations: new Map(),
earlyReturnValue: null,

View File

@@ -9,24 +9,18 @@ import {CompilerError} from '..';
import {
BlockId,
Effect,
Environment,
HIRFunction,
Identifier,
IdentifierId,
Instruction,
Place,
computePostDominatorTree,
evaluatesToStableTypeOrContainer,
getHookKind,
isStableType,
isStableTypeContainer,
isUseOperator,
isUseRefType,
} from '../HIR';
import {PostDominator} from '../HIR/Dominator';
import {
eachInstructionLValue,
eachInstructionOperand,
eachInstructionValueOperand,
eachTerminalOperand,
} from '../HIR/visitors';
@@ -37,103 +31,6 @@ import {
import DisjointSet from '../Utils/DisjointSet';
import {assertExhaustive} from '../Utils/utils';
/**
* Side map to track and propagate sources of stability (i.e. hook calls such as
* `useRef()` and property reads such as `useState()[1]). Note that this
* requires forward data flow analysis since stability is not part of React
* Compiler's type system.
*/
class StableSidemap {
map: Map<IdentifierId, {isStable: boolean}> = new Map();
env: Environment;
constructor(env: Environment) {
this.env = env;
}
handleInstruction(instr: Instruction): void {
const {value, lvalue} = instr;
switch (value.kind) {
case 'CallExpression':
case 'MethodCall': {
/**
* Sources of stability are known hook calls
*/
if (evaluatesToStableTypeOrContainer(this.env, instr)) {
if (isStableType(lvalue.identifier)) {
this.map.set(lvalue.identifier.id, {
isStable: true,
});
} else {
this.map.set(lvalue.identifier.id, {
isStable: false,
});
}
} else if (
this.env.config.enableTreatRefLikeIdentifiersAsRefs &&
isUseRefType(lvalue.identifier)
) {
this.map.set(lvalue.identifier.id, {
isStable: true,
});
}
break;
}
case 'Destructure':
case 'PropertyLoad': {
/**
* PropertyLoads may from stable containers may also produce stable
* values. ComputedLoads are technically safe for now (as all stable
* containers have differently-typed elements), but are not handled as
* they should be rare anyways.
*/
const source =
value.kind === 'Destructure'
? value.value.identifier.id
: value.object.identifier.id;
const entry = this.map.get(source);
if (entry) {
for (const lvalue of eachInstructionLValue(instr)) {
if (isStableTypeContainer(lvalue.identifier)) {
this.map.set(lvalue.identifier.id, {
isStable: false,
});
} else if (isStableType(lvalue.identifier)) {
this.map.set(lvalue.identifier.id, {
isStable: true,
});
}
}
}
break;
}
case 'StoreLocal': {
const entry = this.map.get(value.value.identifier.id);
if (entry) {
this.map.set(lvalue.identifier.id, entry);
this.map.set(value.lvalue.place.identifier.id, entry);
}
break;
}
case 'LoadLocal': {
const entry = this.map.get(value.place.identifier.id);
if (entry) {
this.map.set(lvalue.identifier.id, entry);
}
break;
}
}
}
isStable(id: IdentifierId): boolean {
const entry = this.map.get(id);
return entry != null ? entry.isStable : false;
}
}
/*
* Infers which `Place`s are reactive, ie may *semantically* change
* over the course of the component/hook's lifetime. Places are reactive
@@ -214,7 +111,6 @@ class StableSidemap {
*/
export function inferReactivePlaces(fn: HIRFunction): void {
const reactiveIdentifiers = new ReactivityMap(findDisjointMutableValues(fn));
const stableIdentifierSources = new StableSidemap(fn.env);
for (const param of fn.params) {
const place = param.kind === 'Identifier' ? param : param.place;
reactiveIdentifiers.markReactive(place);
@@ -288,12 +184,11 @@ export function inferReactivePlaces(fn: HIRFunction): void {
}
}
for (const instruction of block.instructions) {
stableIdentifierSources.handleInstruction(instruction);
const {value} = instruction;
let hasReactiveInput = false;
/*
* NOTE: we want to mark all operands as reactive or not, so we
* avoid short-circuiting here
* avoid short-circuting here
*/
for (const operand of eachInstructionValueOperand(value)) {
const reactive = reactiveIdentifiers.isReactive(operand);
@@ -323,13 +218,7 @@ export function inferReactivePlaces(fn: HIRFunction): void {
if (hasReactiveInput) {
for (const lvalue of eachInstructionLValue(instruction)) {
/**
* Note that it's not correct to mark all stable-typed identifiers
* as non-reactive, since ternaries and other value blocks can
* produce reactive identifiers typed as these.
* (e.g. `props.cond ? setState1 : setState2`)
*/
if (stableIdentifierSources.isStable(lvalue.identifier.id)) {
if (isStableType(lvalue.identifier)) {
continue;
}
reactiveIdentifiers.markReactive(lvalue);
@@ -376,41 +265,6 @@ export function inferReactivePlaces(fn: HIRFunction): void {
}
}
} while (reactiveIdentifiers.snapshot());
function propagateReactivityToInnerFunctions(
fn: HIRFunction,
isOutermost: boolean,
): void {
for (const [, block] of fn.body.blocks) {
for (const instr of block.instructions) {
if (!isOutermost) {
for (const operand of eachInstructionOperand(instr)) {
reactiveIdentifiers.isReactive(operand);
}
}
if (
instr.value.kind === 'ObjectMethod' ||
instr.value.kind === 'FunctionExpression'
) {
propagateReactivityToInnerFunctions(
instr.value.loweredFunc.func,
false,
);
}
}
if (!isOutermost) {
for (const operand of eachTerminalOperand(block.terminal)) {
reactiveIdentifiers.isReactive(operand);
}
}
}
}
/**
* Propagate reactivity for inner functions, as we eventually hoist and dedupe
* dependency instructions for scopes.
*/
propagateReactivityToInnerFunctions(fn, true);
}
/*

View File

@@ -387,10 +387,12 @@ export function inlineJsxTransform(
if (block.terminal.kind === 'scope') {
const scope = block.terminal.scope;
/**
* Note that scope dependencies don't need to be renamed explicitly
* as they will be visited when traversing scope terminal successors.
*/
for (const dep of scope.dependencies) {
dep.identifier = handleIdentifier(
dep.identifier,
inlinedJsxDeclarations,
);
}
for (const [origId, decl] of [...scope.declarations]) {
const newDecl = handleIdentifier(

View File

@@ -17,16 +17,11 @@ import {
SourceLocation,
} from '../HIR';
import {
areEqualPaths,
GeneratedSource,
HIRFunction,
PrunedReactiveScopeBlock,
ReactiveBreakTerminal,
ReactiveContinueTerminal,
ReactiveFunction,
ReactiveInstructionStatement,
ReactiveLogicalValue,
ReactiveScopeBlock,
ReactiveSequenceValue,
ReactiveTerminalStatement,
ReactiveTerminalTargetKind,
@@ -34,12 +29,7 @@ import {
ReactiveValue,
Terminal,
} from '../HIR/HIR';
import {
readScopeDependencies,
readScopeDependenciesRHIR,
} from '../HIR/ScopeDependencyUtils';
import {assertExhaustive} from '../Utils/utils';
import {printDependency} from './PrintReactiveFunction';
/*
* Converts from HIR (lower-level CFG) to ReactiveFunction, a tree representation
@@ -48,7 +38,7 @@ import {printDependency} from './PrintReactiveFunction';
* labels for *all* terminals: see PruneUnusedLabels which removes unnecessary labels.
*/
export function buildReactiveFunction(fn: HIRFunction): ReactiveFunction {
const cx = new Context(fn);
const cx = new Context(fn.body);
const driver = new Driver(cx);
const body = driver.traverseBlock(cx.block(fn.body.entry));
return {
@@ -826,64 +816,17 @@ class Driver {
} else {
block = this.traverseBlock(this.cx.ir.blocks.get(terminal.block)!);
}
{
const scheduleId = this.cx.schedule(terminal.block, 'if');
scheduleIds.push(scheduleId);
this.cx.scopeFallthroughs.add(terminal.block);
}
CompilerError.invariant(!this.cx.isScheduled(terminal.dependencies), {
reason: `Unexpected 'scope' where the dependencies block is already scheduled`,
loc: terminal.loc,
});
const dependencies: Array<ReactiveInstructionStatement> =
this.traverseBlock(this.cx.ir.blocks.get(terminal.dependencies)!).map(
dep => {
CompilerError.invariant(dep.kind === 'instruction', {
reason: '[BuildReactiveFunction] Expected reactive instruction',
loc: GeneratedSource,
});
return dep;
},
);
this.cx.unscheduleAll(scheduleIds);
const scopeBlock: ReactiveScopeBlock | PrunedReactiveScopeBlock = {
blockValue.push({
kind: terminal.kind,
dependencyInstructions: dependencies,
instructions: block,
scope: terminal.scope,
};
blockValue.push(scopeBlock);
});
if (fallthroughId !== null) {
this.visitBlock(this.cx.ir.blocks.get(fallthroughId)!, blockValue);
}
/**
* Sanity check: check that dependencies stay the same after converting
* to ReactiveFunction.
*/
const hirDeps = readScopeDependencies(this.cx.fn, terminal.scope.id);
const rhirDeps = [...readScopeDependenciesRHIR(scopeBlock)];
CompilerError.invariant(hirDeps.size === rhirDeps.length, {
reason: `[BuildReactiveFunction] Sanity check failed: mismatch in dependencies count`,
loc: terminal.loc,
});
for (const hirDep of hirDeps) {
CompilerError.invariant(
rhirDeps.some(
([, rhirDep]) =>
hirDep.identifier === rhirDep.identifier &&
areEqualPaths(hirDep.path, rhirDep.path),
),
{
reason:
'[BuildReactiveFunction] Sanity check failed: mismatching scope dependencies',
description: `No match found for ${printDependency(hirDep)}. Candidates are ${rhirDeps.map(dep => printDependency(dep[1]))}`,
loc: GeneratedSource,
},
);
}
break;
}
case 'unreachable': {
@@ -1300,7 +1243,6 @@ class Driver {
}
class Context {
fn: HIRFunction;
ir: HIR;
#nextScheduleId: number = 0;
@@ -1331,9 +1273,8 @@ class Context {
*/
#controlFlowStack: Array<ControlFlowTarget> = [];
constructor(fn: HIRFunction) {
this.fn = fn;
this.ir = fn.body;
constructor(ir: HIR) {
this.ir = ir;
}
block(id: BlockId): BasicBlock {

View File

@@ -32,10 +32,10 @@ import {
ReactiveBlock,
ReactiveFunction,
ReactiveInstruction,
ReactiveInstructionStatement,
ReactiveScope,
ReactiveScopeBlock,
ReactiveScopeDeclaration,
ReactiveScopeDependency,
ReactiveTerminal,
ReactiveValue,
SourceLocation,
@@ -54,7 +54,6 @@ import {SINGLE_CHILD_FBT_TAGS} from './MemoizeFbtAndMacroOperandsInSameScope';
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
import {EMIT_FREEZE_GLOBAL_GATING, ReactFunctionType} from '../HIR/Environment';
import {ProgramContext} from '../Entrypoint';
import generate from '@babel/generator';
export const MEMO_CACHE_SENTINEL = 'react.memo_cache_sentinel';
export const EARLY_RETURN_SENTINEL = 'react.early_return_sentinel';
@@ -540,13 +539,7 @@ function codegenBlockNoReset(
}
case 'scope': {
const temp = new Map(cx.temp);
codegenReactiveScope(
cx,
statements,
item.scope,
item.instructions,
item.dependencyInstructions,
);
codegenReactiveScope(cx, statements, item.scope, item.instructions);
cx.temp = temp;
break;
}
@@ -610,7 +603,6 @@ function codegenReactiveScope(
statements: Array<t.Statement>,
scope: ReactiveScope,
block: ReactiveBlock,
dependencyInstructions: Array<ReactiveInstructionStatement>,
): void {
const cacheStoreStatements: Array<t.Statement> = [];
const cacheLoadStatements: Array<t.Statement> = [];
@@ -623,39 +615,9 @@ function codegenReactiveScope(
const changeExpressionComments: Array<string> = [];
const outputComments: Array<string> = [];
/**
* Step 1: Dependency instructions should codegen their expressions into
* the `Context.Temporaries` map.
*/
for (const instr of dependencyInstructions) {
/**
* codegenInstructionNullable should never return a statement, as all
* dependency instructions should be inlined into `Context.Temporaries`.
*/
const result = codegenInstructionNullable(cx, instr.instruction);
CompilerError.invariant(result == null, {
reason: 'Expected dependency instructions to be inlined',
loc: instr.instruction.loc,
});
}
const sortedDependencies: Array<[string, t.Expression]> = scope.dependencies
.map<[string, t.Expression]>(dep => {
const dependency = codegenPlace(cx, dep);
CompilerError.invariant(dependency.type !== 'JSXText', {
reason: 'Expected dependency to not be JSXText',
loc: GeneratedSource,
});
return [generate(dependency).code, dependency];
})
.sort(([aName], [bName]) => {
if (aName < bName) return -1;
else if (aName > bName) return 1;
else return 0;
});
for (const [code, dep] of sortedDependencies) {
for (const dep of [...scope.dependencies].sort(compareScopeDependency)) {
const index = cx.nextCacheIndex;
changeExpressionComments.push(code);
changeExpressionComments.push(printDependencyComment(dep));
const comparison = t.binaryExpression(
'!==',
t.memberExpression(
@@ -663,7 +625,7 @@ function codegenReactiveScope(
t.numericLiteral(index),
true,
),
dep,
codegenDependency(cx, dep),
);
if (cx.env.config.enableChangeVariableCodegen) {
@@ -690,7 +652,7 @@ function codegenReactiveScope(
t.numericLiteral(index),
true,
),
t.cloneNode(dep, true),
codegenDependency(cx, dep),
),
),
);
@@ -1481,6 +1443,17 @@ function codegenForInit(
}
}
function printDependencyComment(dependency: ReactiveScopeDependency): string {
const identifier = convertIdentifier(dependency.identifier);
let name = identifier.name;
if (dependency.path !== null) {
for (const path of dependency.path) {
name += `.${path.property}`;
}
}
return name;
}
function printDelimitedCommentList(
items: Array<string>,
finalCompletion: string,
@@ -1505,6 +1478,34 @@ function printDelimitedCommentList(
return output.join('');
}
function codegenDependency(
cx: Context,
dependency: ReactiveScopeDependency,
): t.Expression {
let object: t.Expression = convertIdentifier(dependency.identifier);
if (dependency.path.length !== 0) {
const hasOptional = dependency.path.some(path => path.optional);
for (const path of dependency.path) {
const property =
typeof path.property === 'string'
? t.identifier(path.property)
: t.numericLiteral(path.property);
const isComputed = typeof path.property !== 'string';
if (hasOptional) {
object = t.optionalMemberExpression(
object,
property,
isComputed,
path.optional,
);
} else {
object = t.memberExpression(object, property, isComputed);
}
}
}
return object;
}
function withLoc<T extends (...args: Array<any>) => t.Node>(
fn: T,
): (
@@ -2623,6 +2624,30 @@ function convertIdentifier(identifier: Identifier): t.Identifier {
return t.identifier(identifier.name.value);
}
function compareScopeDependency(
a: ReactiveScopeDependency,
b: ReactiveScopeDependency,
): number {
CompilerError.invariant(
a.identifier.name?.kind === 'named' && b.identifier.name?.kind === 'named',
{
reason: '[Codegen] Expected named identifier for dependency',
loc: a.identifier.loc,
},
);
const aName = [
a.identifier.name.value,
...a.path.map(entry => `${entry.optional ? '?' : ''}${entry.property}`),
].join('.');
const bName = [
b.identifier.name.value,
...b.path.map(entry => `${entry.optional ? '?' : ''}${entry.property}`),
].join('.');
if (aName < bName) return -1;
else if (aName > bName) return 1;
else return 0;
}
function compareScopeDeclaration(
a: ReactiveScopeDeclaration,
b: ReactiveScopeDeclaration,

View File

@@ -31,16 +31,14 @@ export function flattenReactiveLoopsHIR(fn: HIRFunction): void {
}
case 'scope': {
if (activeLoops.length !== 0) {
const newTerminal: PrunedScopeTerminal = {
block.terminal = {
kind: 'pruned-scope',
block: terminal.block,
dependencies: terminal.dependencies,
fallthrough: terminal.fallthrough,
id: terminal.id,
loc: terminal.loc,
scope: terminal.scope,
};
block.terminal = newTerminal;
} as PrunedScopeTerminal;
}
break;
}

View File

@@ -83,50 +83,28 @@ export function flattenScopesWithHooksOrUseHIR(fn: HIRFunction): void {
body.terminal.kind === 'goto' &&
body.terminal.block === terminal.fallthrough
) {
/**
* Note that scope blocks are unique in that they represent two nested
* goto-labels. We entirely remove dependency blocks here to simplify
* rewrite logic
*/
const dependencyBlock = fn.body.blocks.get(terminal.dependencies);
CompilerError.invariant(
dependencyBlock != null &&
dependencyBlock.instructions.length === 0 &&
dependencyBlock.terminal.kind === 'goto' &&
dependencyBlock.terminal.block === terminal.block,
{
reason: `Expected scope dependency block to have no instructions and goto scope block`,
loc: terminal.loc,
},
);
fn.body.blocks.delete(terminal.dependencies);
const scopeBlock = fn.body.blocks.get(terminal.block)!;
scopeBlock.preds = new Set([block.id]);
/*
* 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
*/
const newTerminal: LabelTerminal = {
block.terminal = {
kind: 'label',
block: terminal.block,
fallthrough: terminal.fallthrough,
id: terminal.id,
loc: terminal.loc,
};
block.terminal = newTerminal;
} as LabelTerminal;
continue;
}
const newTerminal: PrunedScopeTerminal = {
block.terminal = {
kind: 'pruned-scope',
block: terminal.block,
dependencies: terminal.dependencies,
fallthrough: terminal.fallthrough,
id: terminal.id,
loc: terminal.loc,
scope: terminal.scope,
};
block.terminal = newTerminal;
} as PrunedScopeTerminal;
}
}

View File

@@ -107,7 +107,7 @@ export function inferReactiveScopeVariables(fn: HIRFunction): void {
scope = {
id: fn.env.nextScopeId,
range: identifier.mutableRange,
dependencies: [],
dependencies: new Set(),
declarations: new Map(),
reassignments: new Set(),
earlyReturnValue: null,

View File

@@ -28,7 +28,6 @@ import {
BuiltInJsxId,
BuiltInObjectId,
} from '../HIR/ObjectShape';
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
import {eachInstructionLValue} from '../HIR/visitors';
import {assertExhaustive, Iterable_some} from '../Utils/utils';
import {printReactiveScopeSummary} from './PrintReactiveFunction';
@@ -120,29 +119,21 @@ class FindLastUsageVisitor extends ReactiveFunctionVisitor<void> {
class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | null> {
lastUsage: Map<DeclarationId, InstructionId>;
cache: Map<ReactiveScopeBlock, ReactiveScopeDependencies> = new Map();
constructor(lastUsage: Map<DeclarationId, InstructionId>) {
super();
this.lastUsage = lastUsage;
}
dependency(scopeBlock: ReactiveScopeBlock): ReactiveScopeDependencies {
let dependencies = this.cache.get(scopeBlock);
if (dependencies == null) {
dependencies = new Set(readScopeDependenciesRHIR(scopeBlock).values());
this.cache.set(scopeBlock, dependencies);
}
return dependencies;
}
override transformScope(
scopeBlock: ReactiveScopeBlock,
state: ReactiveScopeDependencies | null,
): Transformed<ReactiveStatement> {
const dependencies = this.dependency(scopeBlock);
this.visitScope(scopeBlock, dependencies);
if (state !== null && areEqualDependencies(state, dependencies)) {
this.visitScope(scopeBlock, scopeBlock.scope.dependencies);
if (
state !== null &&
areEqualDependencies(state, scopeBlock.scope.dependencies)
) {
return {kind: 'replace-many', value: scopeBlock.instructions};
} else {
return {kind: 'keep'};
@@ -269,7 +260,7 @@ class Transform extends ReactiveFunctionTransform<ReactiveScopeDependencies | nu
case 'scope': {
if (
current !== null &&
canMergeScopes(this, current.block, instr) &&
canMergeScopes(current.block, instr) &&
areLValuesLastUsedByScope(
instr.scope,
current.lvalues,
@@ -433,7 +424,6 @@ function areLValuesLastUsedByScope(
}
function canMergeScopes(
transform: Transform,
current: ReactiveScopeBlock,
next: ReactiveScopeBlock,
): boolean {
@@ -445,10 +435,10 @@ function canMergeScopes(
log(` cannot merge, has reassignments`);
return false;
}
const currDeps = transform.dependency(current);
const nextDeps = transform.dependency(next);
// Merge scopes whose dependencies are identical
if (areEqualDependencies(currDeps, nextDeps)) {
if (
areEqualDependencies(current.scope.dependencies, next.scope.dependencies)
) {
log(` canMergeScopes: dependencies are equal`);
return true;
}
@@ -466,14 +456,13 @@ function canMergeScopes(
new Set(
[...current.scope.declarations.values()].map(declaration => ({
identifier: declaration.identifier,
reactive: false,
path: [],
})),
),
nextDeps,
next.scope.dependencies,
) ||
(nextDeps.size !== 0 &&
[...nextDeps].every(
(next.scope.dependencies.size !== 0 &&
[...next.scope.dependencies].every(
dep =>
isAlwaysInvalidatingType(dep.identifier.type) &&
Iterable_some(
@@ -548,7 +537,7 @@ function areEqualDependencies(
* *never* change and it's also eligible for merging.
*/
function scopeIsEligibleForMerging(scopeBlock: ReactiveScopeBlock): boolean {
if (scopeBlock.scope.dependencies.length === 0) {
if (scopeBlock.scope.dependencies.size === 0) {
/*
* Regardless of the type of value produced, if the scope has no dependencies
* then its value will never change.

View File

@@ -64,7 +64,11 @@ export function printReactiveScopeSummary(scope: ReactiveScope): string {
items.push('scope');
items.push(`@${scope.id}`);
items.push(`[${scope.range.start}:${scope.range.end}]`);
items.push(`dependencies=[${scope.dependencies.map(printPlace).join(',')}]`);
items.push(
`dependencies=[${Array.from(scope.dependencies)
.map(dep => printDependency(dep))
.join(', ')}]`,
);
items.push(
`declarations=[${Array.from(scope.declarations)
.map(([, decl]) =>
@@ -92,8 +96,6 @@ export function writeReactiveBlock(
block: ReactiveScopeBlock,
): void {
writer.writeLine(`${printReactiveScopeSummary(block.scope)} {`);
writeReactiveInstructions(writer, block.dependencyInstructions);
writer.writeLine('} /* - end dependencies - */ {');
writeReactiveInstructions(writer, block.instructions);
writer.writeLine('}');
}

View File

@@ -26,13 +26,18 @@ import {
} from '../HIR/HIR';
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
import {eachInstructionValueLValue, eachPatternOperand} from '../HIR/visitors';
import {scopeDependenciesDCE} from '../HIR/ScopeDependencyUtils';
/**
* Phase 2: Promote identifiers which are used in a place that requires a named variable.
*/
class PromoteTemporaries extends ReactiveFunctionVisitor<State> {
override visitScope(scopeBlock: ReactiveScopeBlock, state: State): void {
for (const dep of scopeBlock.scope.dependencies) {
const {identifier} = dep;
if (identifier.name == null) {
promoteIdentifier(identifier, state);
}
}
/*
* This is technically optional. We could prune ReactiveScopes
* whose outputs are not used in another computation or return
@@ -45,29 +50,7 @@ class PromoteTemporaries extends ReactiveFunctionVisitor<State> {
promoteIdentifier(declaration.identifier, state);
}
}
/**
* Run DCE to remove dependency instructions that are no longer used due to
* pruning passes.
*/
scopeDependenciesDCE(scopeBlock);
/**
* Traverse into scope dependency instructions to promote references to
* unnamed outer identifiers.
*/
CompilerError.invariant(state.scopeDepContext == null, {
reason: 'PromoteTemporaries: unexpected nested scopeDepContext',
loc: GeneratedSource,
});
this.visitBlock(scopeBlock.dependencyInstructions, {
...state,
scopeDepContext: {
declarations: new Set(),
dependencies: new Set(
[...scopeBlock.scope.dependencies].map(dep => dep.identifier),
),
},
});
this.visitBlock(scopeBlock.instructions, state);
this.traverseScope(scopeBlock, state);
}
override visitPrunedScope(
@@ -92,33 +75,11 @@ class PromoteTemporaries extends ReactiveFunctionVisitor<State> {
}
}
override visitLValue(_id: InstructionId, lvalue: Place, state: State): void {
// Track lvalues from within scope dependency blocks to avoid promoting them.
state.scopeDepContext?.declarations.add(lvalue.identifier.declarationId);
}
override visitValue(
id: InstructionId,
value: ReactiveValue,
state: State,
): void {
if (
state.scopeDepContext &&
(value.kind === 'LoadLocal' || value.kind === 'LoadContext')
) {
/**
* Scope dependency LoadLocal sources (defined by instructions external to
* that scope) should be promoted, as scope boundaries represent
* re-ordering barriers
*/
const identifier = value.place.identifier;
if (
!state.scopeDepContext.declarations.has(identifier.declarationId) &&
identifier.name === null
) {
promoteIdentifier(identifier, state);
}
}
this.traverseValue(id, value, state);
if (value.kind === 'FunctionExpression' || value.kind === 'ObjectMethod') {
this.visitHirFunction(value.loweredFunc.func, state);
@@ -216,10 +177,6 @@ type State = {
DeclarationId,
{activeScopes: Array<ScopeId>; usedOutsideScope: boolean}
>; // true if referenced within another scope, false if only accessed outside of scopes
scopeDepContext: {
declarations: Set<DeclarationId>;
dependencies: ReadonlySet<Identifier>;
} | null;
};
/**
@@ -470,7 +427,6 @@ export function promoteUsedTemporaries(fn: ReactiveFunction): void {
tags: new Set(),
promoted: new Set(),
pruned: new Map(),
scopeDepContext: null,
};
visitReactiveFunction(fn, new CollectPromotableTemporaries(), state);
for (const operand of fn.params) {

View File

@@ -13,7 +13,6 @@ import {
ReactiveScopeBlock,
ReactiveStatement,
} from '../HIR';
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
/**
* Some instructions will *always* produce a new value, and unless memoized will *always*
@@ -88,8 +87,7 @@ class Transform extends ReactiveFunctionTransform<boolean> {
): Transformed<ReactiveStatement> {
this.visitScope(scopeBlock, true);
const scopeDeps = readScopeDependenciesRHIR(scopeBlock);
for (const [, dep] of scopeDeps) {
for (const dep of scopeBlock.scope.dependencies) {
if (this.unmemoizedValues.has(dep.identifier)) {
/*
* This scope depends on an always-invalidating value so the scope will always invalidate:
@@ -109,7 +107,6 @@ class Transform extends ReactiveFunctionTransform<boolean> {
kind: 'replace',
value: {
kind: 'pruned-scope',
dependencyInstructions: scopeBlock.dependencyInstructions,
scope: scopeBlock.scope,
instructions: scopeBlock.instructions,
},

View File

@@ -22,7 +22,6 @@ import {
isUseRefType,
isUseStateType,
} from '../HIR';
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
import {eachCallArgument, eachInstructionLValue} from '../HIR/visitors';
import DisjointSet from '../Utils/DisjointSet';
import {assertExhaustive} from '../Utils/utils';
@@ -179,22 +178,14 @@ class Visitor extends ReactiveFunctionVisitor<CreateUpdate> {
].map(id => this.map.get(id) ?? 'Unknown'),
);
super.visitScope(scope, state);
// TODO
const scopeDeps = readScopeDependenciesRHIR(scope);
[...scopeDeps].forEach(([place, dep]) => {
[...scope.scope.dependencies].forEach(ident => {
let target: undefined | IdentifierId =
this.aliases.find(dep.identifier.id) ?? dep.identifier.id;
dep.path.forEach(token => {
this.aliases.find(ident.identifier.id) ?? ident.identifier.id;
ident.path.forEach(token => {
target &&= this.paths.get(target)?.get(token.property);
});
if (target && this.map.get(target) === 'Create') {
const idx = scope.scope.dependencies.indexOf(place);
CompilerError.invariant(idx !== -1, {
reason: 'Expected dependency to be found',
loc: place.loc,
});
scope.scope.dependencies.splice(idx, 1);
scope.scope.dependencies.delete(ident);
}
});
}

View File

@@ -95,19 +95,13 @@ class Visitor extends ReactiveFunctionVisitor<ReactiveIdentifiers> {
state: ReactiveIdentifiers,
): void {
this.traverseScope(scopeBlock, state);
let j = 0;
for (let i = 0; i < scopeBlock.scope.dependencies.length; i++) {
const isReactive = state.has(
scopeBlock.scope.dependencies[i].identifier.id,
);
if (isReactive) {
scopeBlock.scope.dependencies[j] = scopeBlock.scope.dependencies[i];
j++;
for (const dep of scopeBlock.scope.dependencies) {
const isReactive = state.has(dep.identifier.id);
if (!isReactive) {
scopeBlock.scope.dependencies.delete(dep);
}
}
scopeBlock.scope.dependencies.length = j;
if (scopeBlock.scope.dependencies.length !== 0) {
if (scopeBlock.scope.dependencies.size !== 0) {
/**
* If any of a scope's dependencies are reactive, then all of its
* outputs will re-evaluate whenever those dependencies change.

View File

@@ -56,7 +56,6 @@ class Transform extends ReactiveFunctionTransform<State> {
value: {
kind: 'pruned-scope',
scope: scopeBlock.scope,
dependencyInstructions: scopeBlock.dependencyInstructions,
instructions: scopeBlock.instructions,
},
};

View File

@@ -5,7 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
import {CompilerError} from '..';
import {
HIRFunction,
InstructionId,
@@ -186,20 +185,6 @@ export class ReactiveFunctionVisitor<TState = void> {
this.traverseScope(scope, state);
}
traverseScope(scope: ReactiveScopeBlock, state: TState): void {
this.visitBlock(scope.dependencyInstructions, state);
const lastDependencyInstruction = scope.dependencyInstructions.at(-1);
let lastInstructionId: InstructionId | null = null;
if (lastDependencyInstruction !== undefined) {
lastInstructionId = lastDependencyInstruction.instruction.id;
}
for (const dep of scope.scope.dependencies) {
CompilerError.invariant(lastInstructionId !== null, {
reason:
'[ReactiveFunction] Expected at least one dependency instruction.',
loc: scope.scope.loc,
});
this.visitPlace(lastInstructionId, dep, state);
}
this.visitBlock(scope.instructions, state);
}

View File

@@ -17,7 +17,6 @@ import {
isUseInsertionEffectHookType,
isUseLayoutEffectHookType,
} from '../HIR';
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
import {isMutable} from '../ReactiveScopes/InferReactiveScopeVariables';
import {
ReactiveFunctionVisitor,
@@ -74,7 +73,7 @@ class Visitor extends ReactiveFunctionVisitor<CompilerError> {
* memoized, allowing a transitive memoization check.
*/
let areDependenciesMemoized = true;
for (const [, dep] of readScopeDependenciesRHIR(scopeBlock)) {
for (const dep of scopeBlock.scope.dependencies) {
if (isUnmemoized(dep.identifier, this.scopes)) {
areDependenciesMemoized = false;
break;

View File

@@ -25,7 +25,6 @@ import {
SourceLocation,
} from '../HIR';
import {printIdentifier, printManualMemoDependency} from '../HIR/PrintHIR';
import {readScopeDependenciesRHIR} from '../HIR/ScopeDependencyUtils';
import {eachInstructionValueOperand} from '../HIR/visitors';
import {collectMaybeMemoDependencies} from '../Inference/DropManualMemoization';
import {
@@ -407,8 +406,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
state.manualMemoState != null &&
state.manualMemoState.depsFromSource != null
) {
const deps = readScopeDependenciesRHIR(scopeBlock).values();
for (const dep of deps) {
for (const dep of scopeBlock.scope.dependencies) {
validateInferredDep(
dep,
this.temporaries,

View File

@@ -1,69 +0,0 @@
## Input
```javascript
// @inferEffectDependencies
import {useRef, useEffect} from 'react';
import {print, mutate} from 'shared-runtime';
function Component({cond}) {
const arr = useRef([]);
const other = useRef([]);
// Although arr and other are both stable, derived is not
const derived = cond ? arr : other;
useEffect(() => {
mutate(derived.current);
print(derived.current);
});
return arr;
}
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
import { useRef, useEffect } from "react";
import { print, mutate } from "shared-runtime";
function Component(t0) {
const $ = _c(4);
const { cond } = t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [];
$[0] = t1;
} else {
t1 = $[0];
}
const arr = useRef(t1);
let t2;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t2 = [];
$[1] = t2;
} else {
t2 = $[1];
}
const other = useRef(t2);
const derived = cond ? arr : other;
let t3;
if ($[2] !== derived) {
t3 = () => {
mutate(derived.current);
print(derived.current);
};
$[2] = derived;
$[3] = t3;
} else {
t3 = $[3];
}
useEffect(t3, [derived]);
return arr;
}
```
### Eval output
(kind: exception) Fixture not implemented

View File

@@ -1,15 +0,0 @@
// @inferEffectDependencies
import {useRef, useEffect} from 'react';
import {print, mutate} from 'shared-runtime';
function Component({cond}) {
const arr = useRef([]);
const other = useRef([]);
// Although arr and other are both stable, derived is not
const derived = cond ? arr : other;
useEffect(() => {
mutate(derived.current);
print(derived.current);
});
return arr;
}

View File

@@ -83,10 +83,10 @@ export const FIXTURE_ENTRYPOINT = {
import { c as _c2 } from "react/compiler-runtime"; // @inlineJsxTransform
function Parent(t0) {
const $ = _c2(3);
const $ = _c2(2);
const { children, ref } = t0;
let t1;
if ($[0] !== children || $[1] !== ref) {
if ($[0] !== children) {
if (DEV) {
t1 = <div ref={ref}>{children}</div>;
} else {
@@ -99,10 +99,9 @@ function Parent(t0) {
};
}
$[0] = children;
$[1] = ref;
$[2] = t1;
$[1] = t1;
} else {
t1 = $[2];
t1 = $[1];
}
return t1;
}

View File

@@ -4,8 +4,6 @@ import Theme, {ThemeToggleButton} from './Theme';
import './Chrome.css';
import LargeContent from './LargeContent';
export default class Chrome extends Component {
state = {theme: 'light'};
render() {
@@ -27,6 +25,7 @@ export default class Chrome extends Component {
/>
<Suspense fallback="Loading...">
<Theme.Provider value={this.state.theme}>
{this.props.children}
<div>
<ThemeToggleButton
onChange={theme => {
@@ -36,14 +35,9 @@ export default class Chrome extends Component {
}}
/>
</div>
{this.props.children}
</Theme.Provider>
</Suspense>
<p>This should appear in the first paint.</p>
<Suspense fallback="Loading...">
<p>This content should not block paint.</p>
<LargeContent />
</Suspense>
<script
dangerouslySetInnerHTML={{
__html: `assetManifest = ${JSON.stringify(assets)};`,

View File

@@ -1,243 +0,0 @@
import React, {Fragment} from 'react';
export default function LargeContent() {
return (
<Fragment>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris
porttitor tortor ac lectus faucibus, eget eleifend elit hendrerit.
Integer porttitor nisi in leo congue rutrum. Morbi sed ante posuere,
aliquam lorem ac, imperdiet orci. Duis malesuada gravida pharetra. Cras
facilisis arcu diam, id dictum lorem imperdiet a. Suspendisse aliquet
tempus tortor et ultricies. Aliquam libero velit, posuere tempus ante
sed, pellentesque tincidunt lorem. Nullam iaculis, eros a varius
aliquet, tortor felis tempor metus, nec cursus felis eros aliquam nulla.
Vivamus ut orci sed mauris congue lacinia. Cras eget blandit neque.
Pellentesque a massa in turpis ullamcorper volutpat vel at massa. Sed
ante est, auctor non diam non, vulputate ultrices metus. Maecenas dictum
fermentum quam id aliquam. Donec porta risus vitae pretium posuere.
Fusce facilisis eros in lacus tincidunt congue.
</p>
<p>
Pellentesque habitant morbi tristique senectus et netus et malesuada
fames ac turpis egestas. Phasellus dolor ante, iaculis vel nisl vitae,
ornare ornare orci. Praesent sit amet lobortis sapien. Suspendisse
pharetra posuere libero ut dapibus. Donec condimentum ante urna. Aliquam
laoreet tincidunt lacus, sed interdum tortor dapibus elementum. Nam sed
faucibus lorem. Suspendisse finibus, velit sed molestie finibus, risus
purus mollis ante, sit amet aliquet sapien nulla ut nibh. In eget ligula
metus. Duis in purus mattis, blandit magna nec, dictum nunc.
</p>
<p>
Sed convallis magna id tortor blandit dictum. Suspendisse in porttitor
neque. Integer quis metus consequat, rutrum est sit amet, finibus justo.
In hac habitasse platea dictumst. Nullam sagittis, risus sed vehicula
porta, sapien elit ultrices nibh, vel luctus odio tortor et ante. Sed
porta enim in hendrerit tristique. Pellentesque id feugiat libero, sit
amet tempor enim. Proin gravida nisl justo, vel ornare dolor bibendum
ac. Mauris scelerisque mattis facilisis. Praesent sodales augue mollis
orci vulputate aliquet. Mauris molestie luctus neque, sed congue elit
congue ut. Cras quis tortor augue. In auctor nulla vel turpis dapibus
egestas. Phasellus consequat rhoncus nisi sed dignissim. Quisque varius
justo non ex lobortis finibus cursus nec justo. Nulla erat neque,
commodo et sem convallis, tristique faucibus odio.
</p>
<p>
Ut condimentum volutpat sem, id accumsan augue placerat vel. Donec ac
efficitur turpis. Suspendisse pretium odio euismod sapien bibendum, sed
tempus est condimentum. Etiam nisl magna, consequat at ullamcorper at,
sollicitudin eu eros. In mattis ligula arcu. Sed eu consectetur turpis,
id molestie ligula. Vestibulum et venenatis enim. Donec condimentum
vitae nisi et placerat. Sed fringilla vehicula egestas. Proin
consectetur, nibh non ornare scelerisque, diam lorem cursus lectus, ut
mattis mauris purus id mi. Curabitur non ligula sit amet augue molestie
vulputate. Donec maximus magna at volutpat aliquet. Pellentesque
dignissim nulla eget odio eleifend tincidunt. Etiam diam lorem, ornare
vel scelerisque vel, iaculis id risus. Donec aliquet aliquam felis, ac
vehicula lacus suscipit vitae. Morbi eu ligula elit.
</p>
<p>
Praesent pellentesque, libero ut faucibus tempor, purus elit consequat
metus, in ornare nulla lectus at erat. Duis quis blandit turpis. Fusce
at ligula rutrum metus molestie tempor sit amet eu justo. Maecenas
tincidunt nisl nunc. Morbi ac metus tempor, pretium arcu vel, dapibus
velit. Nulla convallis ligula at porta mollis. Duis magna ante, mollis
eget nibh in, congue tempor dolor. Sed tincidunt sagittis arcu, in
ultricies neque tempor non. Suspendisse eget nunc neque. Nulla sit amet
odio volutpat, maximus purus id, dictum metus. Integer consequat, orci
nec ullamcorper porta, mauris libero vestibulum ipsum, nec tempor tellus
enim non nunc. Quisque nisl risus, dapibus sit amet purus nec, aliquam
finibus metus. Nullam condimentum urna viverra finibus cursus. Proin et
sollicitudin tellus, porta fermentum felis. Maecenas ac turpis sed dui
condimentum interdum sed sed erat. Mauris ut dignissim erat.
</p>
<p>
Proin varius porta dui, id fringilla elit lobortis eget. Integer at
metus elementum, efficitur eros id, euismod est. Morbi vestibulum nibh
ac leo luctus sagittis. Praesent rhoncus, risus sit amet mattis dictum,
diam sapien tempor neque, vel dignissim nulla neque eget ex. Nam
sollicitudin metus quis ullamcorper dapibus. Nam tristique euismod
efficitur. Pellentesque rhoncus vel sem eget lacinia. Pellentesque
volutpat velit ac dignissim luctus. Vivamus euismod tortor at ligula
mattis porta. Vestibulum ante ipsum primis in faucibus orci luctus et
ultrices posuere cubilia curae;
</p>
<p>
Proin blandit vulputate efficitur. Pellentesque sit amet porta odio.
Nunc pulvinar varius rhoncus. Mauris fermentum leo a imperdiet pretium.
Mauris scelerisque justo vel ante egestas, eget tempus neque malesuada.
Sed dictum ex vel justo dignissim, aliquam commodo diam rutrum. Integer
dignissim est ullamcorper augue laoreet consectetur id at diam. Vivamus
molestie blandit urna, eget pulvinar augue dictum vestibulum. Duis
maximus bibendum mauris, ut ultricies elit rhoncus eu. Praesent gravida
placerat mauris. Praesent tempor ipsum at nibh rhoncus sagittis. Duis
non sem turpis. Quisque et metus leo. Sed eu purus lorem. Pellentesque
dictum metus sed leo viverra interdum. Maecenas vel tincidunt mi.
</p>
<p>
Praesent consequat dapibus pellentesque. Fusce at enim id mauris laoreet
commodo. Nullam ut mauris euismod, rhoncus tellus vel, facilisis diam.
Aenean porta faucibus augue, a iaculis massa iaculis in. Praesent vel
metus purus. Etiam quis augue eget orci lobortis eleifend ac ut lorem.
Aenean non orci quis nisi molestie maximus. Mauris interdum, eros et
aliquam aliquam, lectus diam pharetra velit, in condimentum odio eros
non quam. Praesent bibendum pretium turpis vitae tristique. Mauris
convallis, massa ut fermentum fermentum, libero orci tempus ipsum,
malesuada ultrices metus sapien placerat lectus. Ut fringilla arcu nec
lorem ultrices mattis. Etiam id tortor feugiat magna gravida gravida.
Morbi aliquam, mi ac pellentesque mattis, erat ex venenatis erat, a
vestibulum eros turpis quis metus. Pellentesque tempus justo in ligula
ultricies porta. Phasellus congue felis sit amet dolor tristique
finibus. Nunc eget eros non est ultricies vestibulum.
</p>
<p>
Donec efficitur ligula quis odio tincidunt tristique. Duis urna dolor,
hendrerit quis enim at, accumsan auctor turpis. Vivamus ante lorem,
maximus vitae suscipit ut, congue eget velit. Maecenas sed ligula erat.
Aliquam mollis purus at nisi porta suscipit in ut magna. Vivamus a
turpis nec tellus egestas suscipit nec ornare nisi. Donec vestibulum
libero quis ex suscipit, sit amet luctus leo gravida.
</p>
<p>
Praesent pharetra dolor elit, sed volutpat lorem rhoncus non. Etiam a
neque ut velit dignissim sodales. Vestibulum neque risus, condimentum
nec consectetur vitae, ultricies ut sapien. Integer iaculis at urna sit
amet malesuada. Integer tincidunt, felis ac vulputate semper, velit leo
facilisis lorem, quis aliquet leo dui id lorem. Morbi non quam quis nisl
sagittis consequat nec vitae libero. Nunc molestie pretium libero, eu
eleifend nibh feugiat sed. Ut in bibendum diam, sit amet vehicula risus.
Nam ornare ac nisi ac euismod. Nullam id egestas nulla. Etiam porta
commodo ante sit amet pellentesque. Suspendisse eleifend purus in urna
euismod auctor non vel nisi. Suspendisse rutrum est nunc, sit amet
lacinia lacus dictum eget. Pellentesque habitant morbi tristique
senectus et netus et malesuada fames ac turpis egestas. Morbi a blandit
diam.
</p>
<p>
Donec eget efficitur sapien. Suspendisse diam lacus, varius eu interdum
et, congue ac justo. Proin ipsum odio, suscipit elementum mauris sed,
porttitor congue est. Cras dapibus dictum ante, vitae gravida elit
venenatis sed. Sed massa sem, posuere ut enim sit amet, vestibulum
condimentum nibh. Pellentesque pulvinar sodales lacinia. Proin id
pretium sapien, non convallis nulla. In mollis tincidunt sem et
porttitor.
</p>
<p>
Integer at sollicitudin sem. Suspendisse sed semper orci. Nulla at nibh
nec risus suscipit posuere egestas vitae enim. Nullam mauris justo,
mattis vel laoreet non, finibus nec nisl. Cras iaculis ultrices nibh,
non commodo eros aliquam non. Sed vitae mollis dui, at maximus metus. Ut
vestibulum, enim ut lobortis vulputate, lorem urna congue elit, non
dictum odio lorem eget velit. Morbi eleifend id ligula vitae vulputate.
Suspendisse ac laoreet justo. Proin eu mattis diam.
</p>
<p>
Nunc in ex quis enim ullamcorper scelerisque eget ac eros. Class aptent
taciti sociosqu ad litora torquent per conubia nostra, per inceptos
himenaeos. Aliquam turpis dui, egestas a rhoncus non, fermentum in
tellus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices
posuere cubilia curae; Aenean non risus arcu. Nam ultricies lacinia
volutpat. Class aptent taciti sociosqu ad litora torquent per conubia
nostra, per inceptos himenaeos. Lorem ipsum dolor sit amet, consectetur
adipiscing elit.
</p>
<p>
Aliquam a felis leo. Proin lorem ipsum, congue eu cursus in, rhoncus ut
libero. Vestibulum sit amet consequat nunc. Ut eleifend lobortis lacus,
vel molestie metus viverra eget. Nullam suscipit eu magna scelerisque
suscipit. Donec dictum in diam nec lacinia. Mauris pellentesque ex ut
purus facilisis, eget placerat turpis semper. Sed dapibus lorem ante, et
malesuada dui eleifend ac. Sed diam felis, semper ac nulla vel, posuere
ultricies ante.
</p>
<p>
Nunc elementum odio sapien, sit amet vulputate lorem varius at. Fusce
non sapien vitae lorem aliquam pretium sit amet congue dolor. Nunc quis
tortor luctus, pretium ex a, tincidunt urna. Aliquam fermentum massa a
erat pharetra varius. Curabitur at auctor dui. Sed posuere pellentesque
massa, vel bibendum urna dictum non. Fusce eget rhoncus urna. Maecenas
sed lectus tellus. Pellentesque convallis dapibus nisl vitae venenatis.
Quisque ornare a dolor ac pharetra. Nam cursus, mi a lacinia accumsan,
felis erat fringilla magna, ac mattis nunc ante a orci.
</p>
<p>
Nunc vel tortor euismod, commodo tortor non, aliquam nisi. Maecenas
tempus mollis velit non suscipit. Mauris sit amet dolor sed ex fringilla
varius. Suspendisse vel cursus risus. Vivamus pharetra massa nec dolor
aliquam feugiat. Fusce finibus enim commodo, scelerisque ante eu,
laoreet ex. Curabitur placerat magna quis imperdiet lacinia. Etiam
lectus mauris, porttitor ac lacinia sed, posuere eget lacus. Mauris
vulputate mattis imperdiet. Nunc id aliquet libero, vitae hendrerit
purus. Praesent vestibulum urna ac egestas tempor. In molestie, nunc sit
amet sagittis dapibus, ligula enim fermentum mi, lacinia molestie eros
dui in tortor. Mauris fermentum pulvinar faucibus. Curabitur laoreet
eleifend purus, non tincidunt tortor gravida nec. Nam eu lectus congue,
commodo libero et, porttitor est. Nullam tincidunt, nisi eu congue
congue, magna justo commodo massa, nec efficitur dui lectus non sem.
</p>
<p>
Nullam vehicula, ipsum quis lacinia tristique, elit nulla dignissim
augue, at pulvinar metus justo ac magna. Nullam nec nunc ac sapien
mollis cursus eu ac enim. Pellentesque a pharetra erat. Ut tempor magna
nisi, accumsan blandit lectus volutpat nec. Vivamus vel lorem nec eros
blandit dictum eget ac diam. Nulla nec turpis dolor. Morbi eu euismod
libero. Nam ut tortor at arcu porta tincidunt. In gravida ligula
fringilla ornare imperdiet. Nulla scelerisque ante erat, efficitur
dictum metus ullamcorper vel. Nam ac purus metus. Maecenas eget tempus
nulla. Ut magna lorem, efficitur ut ex a, semper aliquam magna. Praesent
lobortis, velit ac posuere mattis, justo est accumsan turpis, id
sagittis felis mi in lacus.
</p>
<p>
Aenean est mi, semper nec sem at, malesuada consectetur nunc. Aenean
consequat sem quis sem consequat, non aliquam est placerat. Cras
malesuada magna neque, et pellentesque nibh consequat at. Sed interdum
velit et ex interdum, vel lobortis ante vestibulum. Nam placerat lectus
eu commodo efficitur. Pellentesque in nunc ac massa porttitor eleifend
ut efficitur sem. Aenean at magna auctor, posuere augue in, ultrices
arcu. Praesent dignissim augue ex, malesuada maximus metus interdum a.
Proin nec odio in nulla vestibulum.
</p>
<p>
Aenean est mi, semper nec sem at, malesuada consectetur nunc. Aenean
consequat sem quis sem consequat, non aliquam est placerat. Cras
malesuada magna neque, et pellentesque nibh consequat at. Sed interdum
velit et ex interdum, vel lobortis ante vestibulum. Nam placerat lectus
eu commodo efficitur. Pellentesque in nunc ac massa porttitor eleifend
ut efficitur sem. Aenean at magna auctor, posuere augue in, ultrices
arcu. Praesent dignissim augue ex, malesuada maximus metus interdum a.
Proin nec odio in nulla vestibulum.
</p>
<p>
Aenean est mi, semper nec sem at, malesuada consectetur nunc. Aenean
consequat sem quis sem consequat, non aliquam est placerat. Cras
malesuada magna neque, et pellentesque nibh consequat at. Sed interdum
velit et ex interdum, vel lobortis ante vestibulum. Nam placerat lectus
eu commodo efficitur. Pellentesque in nunc ac massa porttitor eleifend
ut efficitur sem. Aenean at magna auctor, posuere augue in, ultrices
arcu. Praesent dignissim augue ex, malesuada maximus metus interdum a.
Proin nec odio in nulla vestibulum.
</p>
</Fragment>
);
}

View File

@@ -906,8 +906,8 @@ describe('InspectedElement', () => {
},
"usedRejectedPromise": {
"reason": Dehydrated {
"preview_short": Error: test-error-do-not-surface,
"preview_long": Error: test-error-do-not-surface,
"preview_short": Error,
"preview_long": Error,
},
},
}

View File

@@ -397,7 +397,7 @@ export function dehydrate(
return object;
}
case 'class_instance': {
case 'class_instance':
isPathAllowedCheck = isPathAllowed(path);
if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
@@ -433,69 +433,7 @@ export function dehydrate(
unserializable.push(path);
return value;
}
case 'error': {
isPathAllowedCheck = isPathAllowed(path);
if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
return createDehydrated(type, true, data, cleaned, path);
}
const value: Unserializable = {
unserializable: true,
type,
readonly: true,
preview_short: formatDataForPreview(data, false),
preview_long: formatDataForPreview(data, true),
name: data.name,
};
// name, message, stack and cause are not enumerable yet still interesting.
value.message = dehydrate(
data.message,
cleaned,
unserializable,
path.concat(['message']),
isPathAllowed,
isPathAllowedCheck ? 1 : level + 1,
);
value.stack = dehydrate(
data.stack,
cleaned,
unserializable,
path.concat(['stack']),
isPathAllowed,
isPathAllowedCheck ? 1 : level + 1,
);
if ('cause' in data) {
value.cause = dehydrate(
data.cause,
cleaned,
unserializable,
path.concat(['cause']),
isPathAllowed,
isPathAllowedCheck ? 1 : level + 1,
);
}
getAllEnumerableKeys(data).forEach(key => {
const keyAsString = key.toString();
value[keyAsString] = dehydrate(
data[key],
cleaned,
unserializable,
path.concat([keyAsString]),
isPathAllowed,
isPathAllowedCheck ? 1 : level + 1,
);
});
unserializable.push(path);
return value;
}
case 'infinity':
case 'nan':
case 'undefined':

View File

@@ -554,7 +554,6 @@ export type DataType =
| 'class_instance'
| 'data_view'
| 'date'
| 'error'
| 'function'
| 'html_all_collection'
| 'html_element'
@@ -574,21 +573,6 @@ export type DataType =
| 'undefined'
| 'unknown';
function isError(data: Object): boolean {
// If it doesn't event look like an error, it won't be an actual error.
if ('name' in data && 'message' in data) {
while (data) {
// $FlowFixMe[method-unbinding]
if (Object.prototype.toString.call(data) === '[object Error]') {
return true;
}
data = Object.getPrototypeOf(data);
}
}
return false;
}
/**
* Get a enhanced/artificial type string based on the object instance
*/
@@ -650,8 +634,6 @@ export function getDataType(data: Object): DataType {
return 'regexp';
} else if (typeof data.then === 'function') {
return 'thenable';
} else if (isError(data)) {
return 'error';
} else {
// $FlowFixMe[method-unbinding]
const toStringValue = Object.prototype.toString.call(data);
@@ -1014,8 +996,6 @@ export function formatDataForPreview(
} else {
return '{…}';
}
case 'error':
return truncateForDisplay(String(data));
case 'boolean':
case 'number':
case 'infinity':

View File

@@ -130,14 +130,6 @@ const usedRejectedPromise = Promise.reject(
new Error('test-error-do-not-surface'),
);
class DigestError extends Error {
digest: string;
constructor(message: string, options: any, digest: string) {
super(message, options);
this.digest = digest;
}
}
export default function Hydration(): React.Node {
return (
<Fragment>
@@ -157,13 +149,6 @@ export default function Hydration(): React.Node {
usedFulfilledRichPromise={usedFulfilledRichPromise}
usedPendingPromise={usedPendingPromise}
usedRejectedPromise={usedRejectedPromise}
// eslint-disable-next-line react-internal/prod-error-codes
error={new Error('test')}
// eslint-disable-next-line react-internal/prod-error-codes
errorWithCause={new Error('one', {cause: new TypeError('two')})}
errorWithDigest={new DigestError('test', {}, 'some-digest')}
// $FlowFixMe[cannot-resolve-name] Flow doesn't know about DOMException
domexception={new DOMException('test')}
/>
<DeepHooks />
</Fragment>

View File

@@ -695,7 +695,6 @@ export function resetResumableState(
resumableState.scriptResources = {};
resumableState.moduleUnknownResources = {};
resumableState.moduleScriptResources = {};
resumableState.instructions = NothingSent; // Nothing was flushed so no instructions could've flushed.
}
export function completeResumableState(resumableState: ResumableState): void {

View File

@@ -219,13 +219,13 @@ function addNestedProperty(
return updatePayload;
}
if (enableFastAddPropertiesInDiffing) {
return fastAddProperties(updatePayload, nextProp, validAttributes);
}
if (!isArray(nextProp)) {
// Add each property of the leaf.
return slowAddProperties(updatePayload, nextProp, validAttributes);
if (enableFastAddPropertiesInDiffing) {
return fastAddProperties(updatePayload, nextProp, validAttributes);
} else {
return addProperties(updatePayload, nextProp, validAttributes);
}
}
for (let i = 0; i < nextProp.length; i++) {
@@ -516,7 +516,7 @@ function fastAddProperties(
/**
* addProperties adds all the valid props to the payload after being processed.
*/
function slowAddProperties(
function addProperties(
updatePayload: null | Object,
props: Object,
validAttributes: AttributeConfiguration,

View File

@@ -98,8 +98,6 @@ const ReactNoopServer = ReactFizzServer({
closeWithError(destination: Destination, error: mixed): void {},
flushBuffered(destination: Destination): void {},
byteLengthOfChunk: null,
getChildFormatContext(): null {
return null;
},

View File

@@ -3355,16 +3355,8 @@ export function requestFormReset(formFiber: Fiber) {
);
}
let stateHook: Hook = ensureFormComponentIsStateful(formFiber);
const stateHook = ensureFormComponentIsStateful(formFiber);
const newResetState = {};
if (stateHook.next === null) {
// Hack alert. If formFiber is the workInProgress Fiber then
// we might get a broken intermediate state. Try the alternate
// instead.
// TODO: We should really stash the Queue somewhere stateful
// just like how setState binds the Queue.
stateHook = (formFiber.alternate: any).memoizedState;
}
const resetStateHook: Hook = (stateHook.next: any);
const resetStateQueue = resetStateHook.queue;
dispatchSetStateInternal(

View File

@@ -50,7 +50,6 @@ import {
flushBuffered,
close,
closeWithError,
byteLengthOfChunk,
} from './ReactServerStreamConfig';
import {
writeCompletedRoot,
@@ -349,7 +348,6 @@ export opaque type Request = {
pendingRootTasks: number, // when this reaches zero, we've finished at least the root boundary.
completedRootSegment: null | Segment, // Completed but not yet flushed root segments.
completedPreambleSegments: null | Array<Array<Segment>>, // contains the ready-to-flush segments that make up the preamble
byteSize: number, // counts the number of bytes accumulated in the shell
abortableTasks: Set<Task>,
pingedTasks: Array<Task>, // High priority tasks that should be worked on first.
// Queues to flush in order of priority
@@ -400,13 +398,6 @@ type Preamble = PreambleState;
// 500 * 1024 / 8 * .8 * 0.5 / 2
const DEFAULT_PROGRESSIVE_CHUNK_SIZE = 12800;
function isEligibleForOutlining(
request: Request,
boundary: SuspenseBoundary,
): boolean {
return boundary.byteSize > request.progressiveChunkSize;
}
function defaultErrorHandler(error: mixed) {
if (
typeof error === 'object' &&
@@ -456,7 +447,6 @@ function RequestInstance(
this.pendingRootTasks = 0;
this.completedRootSegment = null;
this.completedPreambleSegments = null;
this.byteSize = 0;
this.abortableTasks = abortSet;
this.pingedTasks = pingedTasks;
this.clientRenderedBoundaries = ([]: Array<SuspenseBoundary>);
@@ -1245,7 +1235,6 @@ function renderSuspenseBoundary(
boundarySegment.textEmbedded,
);
boundarySegment.status = COMPLETED;
finishedSegment(request, parentBoundary, boundarySegment);
} catch (thrownValue: mixed) {
if (request.status === ABORTING) {
boundarySegment.status = ABORTED;
@@ -1312,24 +1301,20 @@ function renderSuspenseBoundary(
contentRootSegment.textEmbedded,
);
contentRootSegment.status = COMPLETED;
finishedSegment(request, newBoundary, contentRootSegment);
queueCompletedSegment(newBoundary, contentRootSegment);
if (newBoundary.pendingTasks === 0 && newBoundary.status === PENDING) {
// This must have been the last segment we were waiting on. This boundary is now complete.
newBoundary.status = COMPLETED;
// Therefore we won't need the fallback. We early return so that we don't have to create
// the fallback. However, if this boundary ended up big enough to be eligible for outlining
// we can't do that because we might still need the fallback if we outline it.
if (!isEligibleForOutlining(request, newBoundary)) {
if (request.pendingRootTasks === 0 && task.blockedPreamble) {
// The root is complete and this boundary may contribute part of the preamble.
// We eagerly attempt to prepare the preamble here because we expect most requests
// to have few boundaries which contribute preambles and it allow us to do this
// preparation work during the work phase rather than the when flushing.
preparePreamble(request);
}
return;
// the fallback.
newBoundary.status = COMPLETED;
if (request.pendingRootTasks === 0 && task.blockedPreamble) {
// The root is complete and this boundary may contribute part of the preamble.
// We eagerly attempt to prepare the preamble here because we expect most requests
// to have few boundaries which contribute preambles and it allow us to do this
// preparation work during the work phase rather than the when flushing.
preparePreamble(request);
}
return;
}
} catch (thrownValue: mixed) {
newBoundary.status = CLIENT_RENDERED;
@@ -2466,7 +2451,6 @@ function resumeNode(
renderTask.blockedSegment = resumedSegment;
renderNode(request, task, node, childIndex);
resumedSegment.status = COMPLETED;
finishedSegment(request, blockedBoundary, resumedSegment);
if (blockedBoundary === null) {
request.completedRootSegment = resumedSegment;
} else {
@@ -4288,27 +4272,6 @@ function queueCompletedSegment(
}
}
function finishedSegment(
request: Request,
boundary: Root | SuspenseBoundary,
segment: Segment,
) {
if (byteLengthOfChunk !== null) {
// Count the bytes of all the chunks of this segment.
const chunks = segment.chunks;
let segmentByteSize = 0;
for (let i = 0; i < chunks.length; i++) {
segmentByteSize += byteLengthOfChunk(chunks[i]);
}
// Accumulate on the parent boundary to power heuristics.
if (boundary === null) {
request.byteSize += segmentByteSize;
} else {
boundary.byteSize += segmentByteSize;
}
}
}
function finishedTask(
request: Request,
boundary: Root | SuspenseBoundary,
@@ -4355,13 +4318,9 @@ function finishedTask(
// This needs to happen after we read the parentFlushed flags because aborting can finish
// work which can trigger user code, which can start flushing, which can change those flags.
// If the boundary was POSTPONED, we still need to finish the fallback first.
// If the boundary is eligible to be outlined during flushing we can't cancel the fallback
// since we might need it when it's being outlined.
if (boundary.status === COMPLETED) {
if (!isEligibleForOutlining(request, boundary)) {
boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request);
boundary.fallbackAbortableTasks.clear();
}
boundary.fallbackAbortableTasks.forEach(abortTaskSoft, request);
boundary.fallbackAbortableTasks.clear();
if (
request.pendingRootTasks === 0 &&
@@ -4459,7 +4418,6 @@ function retryRenderTask(
task.abortSet.delete(task);
segment.status = COMPLETED;
finishedSegment(request, task.blockedBoundary, segment);
finishedTask(request, task.blockedBoundary, segment);
} catch (thrownValue: mixed) {
resetHooksState();
@@ -4971,7 +4929,7 @@ function flushSegment(
flushSubtree(request, destination, segment, hoistableState);
return writeEndPendingSuspenseBoundary(destination, request.renderState);
} else if (isEligibleForOutlining(request, boundary)) {
} else if (boundary.byteSize > request.progressiveChunkSize) {
// This boundary is large and will be emitted separately so that we can progressively show
// other content. We add it to the queue during the flush because we have to ensure that
// the parent flushes first so that there's something to inject it into.

View File

@@ -60,9 +60,9 @@ export function typedArrayToBinaryChunk(
throw new Error('Not implemented.');
}
export const byteLengthOfChunk:
| null
| ((chunk: Chunk | PrecomputedChunk) => number) = null;
export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
throw new Error('Not implemented.');
}
export function byteLengthOfBinaryChunk(chunk: BinaryChunk): number {
throw new Error('Not implemented.');