Autogenerated summaries of each of the compiler passes which allow agents to get the key ideas of a compiler pass, including key input/output invariants, without having to reprocess the file each time. In the subsequent diff this seemed to help. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35595). * #35607 * #35298 * #35596 * #35573 * __->__ #35595 * #35539
5.1 KiB
alignMethodCallScopes
File
src/ReactiveScopes/AlignMethodCallScopes.ts
Purpose
Ensures that MethodCall instructions and their associated PropertyLoad instructions (which load the method being called) have consistent scope assignments. The pass enforces one of two invariants:
- Both the MethodCall lvalue and the property have the same reactive scope
- Neither has a reactive scope
This alignment is critical because the PropertyLoad and MethodCall are semantically a single operation (receiver.method(args)) and must be memoized together as a unit. If they had different scopes, the generated code would incorrectly try to memoize the property load separately from the method call, which could break correctness.
Input Invariants
- The function has been converted to HIR form
inferReactiveScopeVariableshas already run, assigning initial reactive scopes to identifiers based on mutation analysis- Each instruction's lvalue has an
identifier.scopethat is either aReactiveScopeornull - For
MethodCallinstructions, thevalue.propertyfield contains aPlacereferencing the loaded method
Output Guarantees
After this pass runs:
- For every
MethodCallinstruction in the function:- If the lvalue has a scope AND the property has a scope, they point to the same merged scope
- If only the lvalue has a scope, the property's scope is set to match the lvalue's scope
- If only the property has a scope, the property's scope is set to
null(so neither has a scope)
- Merged scopes have their
rangeextended to cover the union of the original scopes' ranges - Nested functions (FunctionExpression, ObjectMethod) are recursively processed
Algorithm
Phase 1: Collect Scope Relationships
For each instruction in all blocks:
If instruction is a MethodCall:
lvalueScope = instruction.lvalue.identifier.scope
propertyScope = instruction.value.property.identifier.scope
If both have scopes:
Record that these scopes should be merged (using DisjointSet.union)
Else if only lvalue has scope:
Record that property should be assigned to lvalueScope
Else if only property has scope:
Record that property should be assigned to null (no scope)
If instruction is FunctionExpression or ObjectMethod:
Recursively process the nested function
Phase 2: Merge Scopes
For each merged scope group:
Pick a "root" scope
Extend root's range to cover all merged scopes:
root.range.start = min(all scope start points)
root.range.end = max(all scope end points)
Phase 3: Apply Changes
For each instruction:
If lvalue was recorded for remapping:
Set identifier.scope to the mapped value
Else if identifier has a scope that was merged:
Set identifier.scope to the merged root scope
Key Data Structures
-
scopeMapping: Map<IdentifierId, ReactiveScope | null>- Maps property identifier IDs to their new scope assignment
- Value of
nullmeans the scope should be removed
-
mergedScopes: DisjointSet<ReactiveScope>- Union-find data structure tracking scopes that need to be merged
- Used when both MethodCall and property have different scopes
-
ReactiveScope(from HIR)- Contains
range: { start: InstructionId, end: InstructionId } - The range defines which instructions are part of the scope
- Contains
Edge Cases
Both Have the Same Scope Already
No action needed (implicit in the logic).
Nested Functions
The pass recursively processes FunctionExpression and ObjectMethod instructions to handle closures.
Multiple MethodCalls Sharing Scopes
The DisjointSet handles transitive merging - if A merges with B, and B merges with C, all three end up in the same scope.
Property Without Scope, MethodCall Without Scope
No action needed (both already aligned at null).
TODOs
There are no explicit TODO comments in the source code.
Example
Fixture: alias-capture-in-method-receiver.js
Source code:
function Component() {
let a = someObj();
let x = [];
x.push(a);
return [x, a];
}
Before AlignMethodCallScopes:
[7] store $24_@1[4:10]:TFunction = PropertyLoad capture $23_@1.push
[9] mutate? $26:TPrimitive = MethodCall store $23_@1.read $24_@1(capture $25)
- PropertyLoad result
$24_@1has scope@1 - MethodCall result
$26has no scope (null)
After AlignMethodCallScopes:
[7] store $24[4:10]:TFunction = PropertyLoad capture $23_@1.push
[9] mutate? $26:TPrimitive = MethodCall store $23_@1.read $24(capture $25)
- PropertyLoad result
$24now has no scope (the_@1suffix removed) - MethodCall result
$26still has no scope
Why this matters:
Without this alignment, later passes might try to memoize the .push property load separately from the actual push() call. This would be incorrect because:
- Reading a method from an object and calling it are semantically one operation
- The property load's value (the bound method) is only valid immediately when called on the same receiver
- Separate memoization could lead to stale method references or incorrect this-binding