compiler/packages/babel-plugin-react-compiler/docs/passes/13-alignMethodCallScopes.md MARKDOWN 132 lines View on github.com → Search inside
1# alignMethodCallScopes23## File4`src/ReactiveScopes/AlignMethodCallScopes.ts`56## Purpose7Ensures 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:81. Both the MethodCall lvalue and the property have the **same** reactive scope92. **Neither** has a reactive scope1011This 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.1213## Input Invariants14- The function has been converted to HIR form15- `inferReactiveScopeVariables` has already run, assigning initial reactive scopes to identifiers based on mutation analysis16- Each instruction's lvalue has an `identifier.scope` that is either a `ReactiveScope` or `null`17- For `MethodCall` instructions, the `value.property` field contains a `Place` referencing the loaded method1819## Output Guarantees20After this pass runs:21- For every `MethodCall` instruction in the function:22  - If the lvalue has a scope AND the property has a scope, they point to the **same merged scope**23  - If only the lvalue has a scope, the property's scope is set to match the lvalue's scope24  - If only the property has a scope, the property's scope is set to `null` (so neither has a scope)25- Merged scopes have their `range` extended to cover the union of the original scopes' ranges26- Nested functions (FunctionExpression, ObjectMethod) are recursively processed2728## Algorithm2930### Phase 1: Collect Scope Relationships31```32For each instruction in all blocks:33  If instruction is a MethodCall:34    lvalueScope = instruction.lvalue.identifier.scope35    propertyScope = instruction.value.property.identifier.scope3637    If both have scopes:38      Record that these scopes should be merged (using DisjointSet.union)39    Else if only lvalue has scope:40      Record that property should be assigned to lvalueScope41    Else if only property has scope:42      Record that property should be assigned to null (no scope)4344  If instruction is FunctionExpression or ObjectMethod:45    Recursively process the nested function46```4748### Phase 2: Merge Scopes49```50For each merged scope group:51  Pick a "root" scope52  Extend root's range to cover all merged scopes:53    root.range.start = min(all scope start points)54    root.range.end = max(all scope end points)55```5657### Phase 3: Apply Changes58```59For each instruction:60  If lvalue was recorded for remapping:61    Set identifier.scope to the mapped value62  Else if identifier has a scope that was merged:63    Set identifier.scope to the merged root scope64```6566## Key Data Structures67681. **`scopeMapping: Map<IdentifierId, ReactiveScope | null>`**69   - Maps property identifier IDs to their new scope assignment70   - Value of `null` means the scope should be removed71722. **`mergedScopes: DisjointSet<ReactiveScope>`**73   - Union-find data structure tracking scopes that need to be merged74   - Used when both MethodCall and property have different scopes75763. **`ReactiveScope`** (from HIR)77   - Contains `range: { start: InstructionId, end: InstructionId }`78   - The range defines which instructions are part of the scope7980## Edge Cases8182### Both Have the Same Scope Already83No action needed (implicit in the logic).8485### Nested Functions86The pass recursively processes `FunctionExpression` and `ObjectMethod` instructions to handle closures.8788### Multiple MethodCalls Sharing Scopes89The DisjointSet handles transitive merging - if A merges with B, and B merges with C, all three end up in the same scope.9091### Property Without Scope, MethodCall Without Scope92No action needed (both already aligned at `null`).9394## TODOs95There are no explicit TODO comments in the source code.9697## Example9899### Fixture: `alias-capture-in-method-receiver.js`100101**Source code:**102```javascript103function Component() {104  let a = someObj();105  let x = [];106  x.push(a);107  return [x, a];108}109```110111**Before AlignMethodCallScopes:**112```113[7] store $24_@1[4:10]:TFunction = PropertyLoad capture $23_@1.push114[9] mutate? $26:TPrimitive = MethodCall store $23_@1.read $24_@1(capture $25)115```116- PropertyLoad result `$24_@1` has scope `@1`117- MethodCall result `$26` has no scope (`null`)118119**After AlignMethodCallScopes:**120```121[7] store $24[4:10]:TFunction = PropertyLoad capture $23_@1.push122[9] mutate? $26:TPrimitive = MethodCall store $23_@1.read $24(capture $25)123```124- PropertyLoad result `$24` now has **no scope** (the `_@1` suffix removed)125- MethodCall result `$26` still has no scope126127**Why this matters:**128Without this alignment, later passes might try to memoize the `.push` property load separately from the actual `push()` call. This would be incorrect because:1291. Reading a method from an object and calling it are semantically one operation1302. The property load's value (the bound method) is only valid immediately when called on the same receiver1313. Separate memoization could lead to stale method references or incorrect this-binding

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.