Skip to content

Commit

Permalink
[compiler] Collect temporaries and optional chains from inner functio…
Browse files Browse the repository at this point in the history
…ns (#31346)

Recursively collect identifier / property loads and optional chains from
inner functions. This PR is in preparation for #31200

Previously, we only did this in `collectHoistablePropertyLoads` to
understand hoistable property loads from inner functions.
1. collectTemporariesSidemap
2. collectOptionalChainSidemap
3. collectHoistablePropertyLoads
- ^ this recursively calls `collectTemporariesSidemap`,
`collectOptionalChainSidemap`, and `collectOptionalChainSidemap` on
inner functions
4. collectDependencies

Now, we have
1. collectTemporariesSidemap
- recursively record identifiers in inner functions. Note that we track
all temporaries in the same map as `IdentifierIds` are currently unique
across functions
2. collectOptionalChainSidemap
    - recursively records optional chain sidemaps in inner functions
3. collectHoistablePropertyLoads
    - (unchanged, except to remove recursive collection of temporaries)
4. collectDependencies
- unchanged: to be modified to recursively collect dependencies in next
PR

'
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/31346).
* #31202
* #31203
* #31201
* #31200
* __->__ #31346
* #31199
  • Loading branch information
mofeiZ authored Nov 6, 2024
1 parent fd018af commit c3570b1
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
Set_union,
getOrInsertDefault,
} from '../Utils/utils';
import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies';
import {
BasicBlock,
BlockId,
Expand All @@ -22,7 +21,6 @@ import {
ReactiveScopeDependency,
ScopeId,
} from './HIR';
import {collectTemporariesSidemap} from './PropagateScopeDependenciesHIR';

const DEBUG_PRINT = false;

Expand Down Expand Up @@ -373,17 +371,10 @@ function collectNonNullsInBlocks(
!fn.env.config.enableTreatFunctionDepsAsConditional
) {
const innerFn = instr.value.loweredFunc;
const innerTemporaries = collectTemporariesSidemap(
innerFn.func,
new Set(),
);
const innerOptionals = collectOptionalChainSidemap(innerFn.func);
const innerHoistableMap = collectHoistablePropertyLoadsImpl(
innerFn.func,
{
...context,
temporaries: innerTemporaries, // TODO: remove in later PR
hoistableFromOptionals: innerOptionals.hoistableObjects, // TODO: remove in later PR
nestedFnImmutableContext:
context.nestedFnImmutableContext ??
new Set(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {assertNonNull} from './CollectHoistablePropertyLoads';
import {
BlockId,
BasicBlock,
InstructionId,
IdentifierId,
ReactiveScopeDependency,
BranchTerminal,
Expand All @@ -15,32 +14,23 @@ import {
OptionalTerminal,
HIRFunction,
DependencyPathEntry,
Instruction,
Terminal,
} from './HIR';
import {printIdentifier} from './PrintHIR';

export function collectOptionalChainSidemap(
fn: HIRFunction,
): OptionalChainSidemap {
const context: OptionalTraversalContext = {
currFn: fn,
blocks: fn.body.blocks,
seenOptionals: new Set(),
processedInstrsInOptional: new Set(),
temporariesReadInOptional: new Map(),
hoistableObjects: new Map(),
};
for (const [_, block] of fn.body.blocks) {
if (
block.terminal.kind === 'optional' &&
!context.seenOptionals.has(block.id)
) {
traverseOptionalBlock(
block as TBasicBlock<OptionalTerminal>,
context,
null,
);
}
}

traverseFunction(fn, context);
return {
temporariesReadInOptional: context.temporariesReadInOptional,
processedInstrsInOptional: context.processedInstrsInOptional,
Expand Down Expand Up @@ -96,8 +86,10 @@ export type OptionalChainSidemap = {
* bb5:
* $5 = MethodCall $2.$4() <--- here, we want to take a dep on $2 and $4!
* ```
*
* Also note that InstructionIds are not unique across inner functions.
*/
processedInstrsInOptional: ReadonlySet<InstructionId>;
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>;
/**
* Records optional chains for which we can safely evaluate non-optional
* PropertyLoads. e.g. given `a?.b.c`, we can evaluate any load from `a?.b` at
Expand All @@ -115,16 +107,46 @@ export type OptionalChainSidemap = {
};

type OptionalTraversalContext = {
currFn: HIRFunction;
blocks: ReadonlyMap<BlockId, BasicBlock>;

// Track optional blocks to avoid outer calls into nested optionals
seenOptionals: Set<BlockId>;

processedInstrsInOptional: Set<InstructionId>;
processedInstrsInOptional: Set<Instruction | Terminal>;
temporariesReadInOptional: Map<IdentifierId, ReactiveScopeDependency>;
hoistableObjects: Map<BlockId, ReactiveScopeDependency>;
};

function traverseFunction(
fn: HIRFunction,
context: OptionalTraversalContext,
): void {
for (const [_, block] of fn.body.blocks) {
for (const instr of block.instructions) {
if (
instr.value.kind === 'FunctionExpression' ||
instr.value.kind === 'ObjectMethod'
) {
traverseFunction(instr.value.loweredFunc.func, {
...context,
currFn: instr.value.loweredFunc.func,
blocks: instr.value.loweredFunc.func.body.blocks,
});
}
}
if (
block.terminal.kind === 'optional' &&
!context.seenOptionals.has(block.id)
) {
traverseOptionalBlock(
block as TBasicBlock<OptionalTerminal>,
context,
null,
);
}
}
}
/**
* Match the consequent and alternate blocks of an optional.
* @returns propertyload computed by the consequent block, or null if the
Expand All @@ -137,7 +159,7 @@ function matchOptionalTestBlock(
consequentId: IdentifierId;
property: string;
propertyId: IdentifierId;
storeLocalInstrId: InstructionId;
storeLocalInstr: Instruction;
consequentGoto: BlockId;
} | null {
const consequentBlock = assertNonNull(blocks.get(terminal.consequent));
Expand All @@ -149,7 +171,7 @@ function matchOptionalTestBlock(
const propertyLoad: TInstruction<PropertyLoad> = consequentBlock
.instructions[0] as TInstruction<PropertyLoad>;
const storeLocal: StoreLocal = consequentBlock.instructions[1].value;
const storeLocalInstrId = consequentBlock.instructions[1].id;
const storeLocalInstr = consequentBlock.instructions[1];
CompilerError.invariant(
propertyLoad.value.object.identifier.id === terminal.test.identifier.id,
{
Expand Down Expand Up @@ -189,7 +211,7 @@ function matchOptionalTestBlock(
consequentId: storeLocal.lvalue.place.identifier.id,
property: propertyLoad.value.property,
propertyId: propertyLoad.lvalue.identifier.id,
storeLocalInstrId,
storeLocalInstr,
consequentGoto: consequentBlock.terminal.block,
};
}
Expand Down Expand Up @@ -369,10 +391,8 @@ function traverseOptionalBlock(
},
],
};
context.processedInstrsInOptional.add(
matchConsequentResult.storeLocalInstrId,
);
context.processedInstrsInOptional.add(test.id);
context.processedInstrsInOptional.add(matchConsequentResult.storeLocalInstr);
context.processedInstrsInOptional.add(test);
context.temporariesReadInOptional.set(
matchConsequentResult.consequentId,
load,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
DeclarationId,
areEqualPaths,
IdentifierId,
Terminal,
} from './HIR';
import {
collectHoistablePropertyLoads,
Expand Down Expand Up @@ -176,8 +177,10 @@ function findTemporariesUsedOutsideDeclaringScope(
* $2 = LoadLocal 'foo'
* $3 = CallExpression $2($1)
* ```
* Only map LoadLocal and PropertyLoad lvalues to their source if we know that
* reordering the read (from the time-of-load to time-of-use) is valid.
* @param usedOutsideDeclaringScope is used to check the correctness of
* reordering LoadLocal / PropertyLoad calls. We only track a LoadLocal /
* PropertyLoad in the returned temporaries map if reordering the read (from the
* time-of-load to time-of-use) is valid.
*
* If a LoadLocal or PropertyLoad instruction is within the reactive scope range
* (a proxy for mutable range) of the load source, later instructions may
Expand Down Expand Up @@ -215,7 +218,29 @@ export function collectTemporariesSidemap(
fn: HIRFunction,
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
): ReadonlyMap<IdentifierId, ReactiveScopeDependency> {
const temporaries = new Map<IdentifierId, ReactiveScopeDependency>();
const temporaries = new Map();
collectTemporariesSidemapImpl(
fn,
usedOutsideDeclaringScope,
temporaries,
false,
);
return temporaries;
}

/**
* Recursive collect a sidemap of all `LoadLocal` and `PropertyLoads` with a
* function and all nested functions.
*
* Note that IdentifierIds are currently unique, so we can use a single
* Map<IdentifierId, ...> across all nested functions.
*/
function collectTemporariesSidemapImpl(
fn: HIRFunction,
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
temporaries: Map<IdentifierId, ReactiveScopeDependency>,
isInnerFn: boolean,
): void {
for (const [_, block] of fn.body.blocks) {
for (const instr of block.instructions) {
const {value, lvalue} = instr;
Expand All @@ -224,27 +249,51 @@ export function collectTemporariesSidemap(
);

if (value.kind === 'PropertyLoad' && !usedOutside) {
const property = getProperty(
value.object,
value.property,
false,
temporaries,
);
temporaries.set(lvalue.identifier.id, property);
if (!isInnerFn || temporaries.has(value.object.identifier.id)) {
/**
* All dependencies of a inner / nested function must have a base
* identifier from the outermost component / hook. This is because the
* compiler cannot break an inner function into multiple granular
* scopes.
*/
const property = getProperty(
value.object,
value.property,
false,
temporaries,
);
temporaries.set(lvalue.identifier.id, property);
}
} else if (
value.kind === 'LoadLocal' &&
lvalue.identifier.name == null &&
value.place.identifier.name !== null &&
!usedOutside
) {
temporaries.set(lvalue.identifier.id, {
identifier: value.place.identifier,
path: [],
});
if (
!isInnerFn ||
fn.context.some(
context => context.identifier.id === value.place.identifier.id,
)
) {
temporaries.set(lvalue.identifier.id, {
identifier: value.place.identifier,
path: [],
});
}
} else if (
value.kind === 'FunctionExpression' ||
value.kind === 'ObjectMethod'
) {
collectTemporariesSidemapImpl(
value.loweredFunc.func,
usedOutsideDeclaringScope,
temporaries,
true,
);
}
}
}
return temporaries;
}

function getProperty(
Expand Down Expand Up @@ -310,6 +359,12 @@ class Context {
#temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>;
#temporariesUsedOutsideScope: ReadonlySet<DeclarationId>;

/**
* Tracks the traversal state. See Context.declare for explanation of why this
* is needed.
*/
inInnerFn: boolean = false;

constructor(
temporariesUsedOutsideScope: ReadonlySet<DeclarationId>,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
Expand Down Expand Up @@ -360,12 +415,23 @@ class Context {
}

/*
* Records where a value was declared, and optionally, the scope where the value originated from.
* This is later used to determine if a dependency should be added to a scope; if the current
* scope we are visiting is the same scope where the value originates, it can't be a dependency
* on itself.
* Records where a value was declared, and optionally, the scope where the
* value originated from. This is later used to determine if a dependency
* should be added to a scope; if the current scope we are visiting is the
* same scope where the value originates, it can't be a dependency on itself.
*
* Note that we do not track declarations or reassignments within inner
* functions for the following reasons:
* - inner functions cannot be split by scope boundaries and are guaranteed
* to consume their own declarations
* - reassignments within inner functions are tracked as context variables,
* which already have extended mutable ranges to account for reassignments
* - *most importantly* it's currently simply incorrect to compare inner
* function instruction ids (tracked by `decl`) with outer ones (as stored
* by root identifier mutable ranges).
*/
declare(identifier: Identifier, decl: Decl): void {
if (this.inInnerFn) return;
if (!this.#declarations.has(identifier.declarationId)) {
this.#declarations.set(identifier.declarationId, decl);
}
Expand Down Expand Up @@ -575,7 +641,7 @@ function collectDependencies(
fn: HIRFunction,
usedOutsideDeclaringScope: ReadonlySet<DeclarationId>,
temporaries: ReadonlyMap<IdentifierId, ReactiveScopeDependency>,
processedInstrsInOptional: ReadonlySet<InstructionId>,
processedInstrsInOptional: ReadonlySet<Instruction | Terminal>,
): Map<ReactiveScope, Array<ReactiveScopeDependency>> {
const context = new Context(usedOutsideDeclaringScope, temporaries);

Expand Down Expand Up @@ -614,12 +680,12 @@ function collectDependencies(
}
}
for (const instr of block.instructions) {
if (!processedInstrsInOptional.has(instr.id)) {
if (!processedInstrsInOptional.has(instr)) {
handleInstruction(instr, context);
}
}

if (!processedInstrsInOptional.has(block.terminal.id)) {
if (!processedInstrsInOptional.has(block.terminal)) {
for (const place of eachTerminalOperand(block.terminal)) {
context.visitOperand(place);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1215,9 +1215,17 @@ export class ScopeBlockTraversal {
}
}

/**
* @returns if the given scope is currently 'active', i.e. if the scope start
* block but not the scope fallthrough has been recorded.
*/
isScopeActive(scopeId: ScopeId): boolean {
return this.#activeScopes.indexOf(scopeId) !== -1;
}

/**
* The current, innermost active scope.
*/
get currentScope(): ScopeId | null {
return this.#activeScopes.at(-1) ?? null;
}
Expand Down

0 comments on commit c3570b1

Please sign in to comment.