main
md 131 lines 5.06 KB
Rendered Raw
1 # alignMethodCallScopes
2
3 ## File
4 `src/ReactiveScopes/AlignMethodCallScopes.ts`
5
6 ## Purpose
7 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:
8 1. Both the MethodCall lvalue and the property have the **same** reactive scope
9 2. **Neither** has a reactive scope
10
11 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.
12
13 ## Input Invariants
14 - The function has been converted to HIR form
15 - `inferReactiveScopeVariables` has already run, assigning initial reactive scopes to identifiers based on mutation analysis
16 - 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 method
18
19 ## Output Guarantees
20 After 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 scope
24 - 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' ranges
26 - Nested functions (FunctionExpression, ObjectMethod) are recursively processed
27
28 ## Algorithm
29
30 ### Phase 1: Collect Scope Relationships
31 ```
32 For each instruction in all blocks:
33 If instruction is a MethodCall:
34 lvalueScope = instruction.lvalue.identifier.scope
35 propertyScope = instruction.value.property.identifier.scope
36
37 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 lvalueScope
41 Else if only property has scope:
42 Record that property should be assigned to null (no scope)
43
44 If instruction is FunctionExpression or ObjectMethod:
45 Recursively process the nested function
46 ```
47
48 ### Phase 2: Merge Scopes
49 ```
50 For each merged scope group:
51 Pick a "root" scope
52 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 ```
56
57 ### Phase 3: Apply Changes
58 ```
59 For each instruction:
60 If lvalue was recorded for remapping:
61 Set identifier.scope to the mapped value
62 Else if identifier has a scope that was merged:
63 Set identifier.scope to the merged root scope
64 ```
65
66 ## Key Data Structures
67
68 1. **`scopeMapping: Map<IdentifierId, ReactiveScope | null>`**
69 - Maps property identifier IDs to their new scope assignment
70 - Value of `null` means the scope should be removed
71
72 2. **`mergedScopes: DisjointSet<ReactiveScope>`**
73 - Union-find data structure tracking scopes that need to be merged
74 - Used when both MethodCall and property have different scopes
75
76 3. **`ReactiveScope`** (from HIR)
77 - Contains `range: { start: InstructionId, end: InstructionId }`
78 - The range defines which instructions are part of the scope
79
80 ## Edge Cases
81
82 ### Both Have the Same Scope Already
83 No action needed (implicit in the logic).
84
85 ### Nested Functions
86 The pass recursively processes `FunctionExpression` and `ObjectMethod` instructions to handle closures.
87
88 ### Multiple MethodCalls Sharing Scopes
89 The DisjointSet handles transitive merging - if A merges with B, and B merges with C, all three end up in the same scope.
90
91 ### Property Without Scope, MethodCall Without Scope
92 No action needed (both already aligned at `null`).
93
94 ## TODOs
95 There are no explicit TODO comments in the source code.
96
97 ## Example
98
99 ### Fixture: `alias-capture-in-method-receiver.js`
100
101 **Source code:**
102 ```javascript
103 function Component() {
104 let a = someObj();
105 let x = [];
106 x.push(a);
107 return [x, a];
108 }
109 ```
110
111 **Before AlignMethodCallScopes:**
112 ```
113 [7] store $24_@1[4:10]:TFunction = PropertyLoad capture $23_@1.push
114 [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`)
118
119 **After AlignMethodCallScopes:**
120 ```
121 [7] store $24[4:10]:TFunction = PropertyLoad capture $23_@1.push
122 [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 scope
126
127 **Why this matters:**
128 Without this alignment, later passes might try to memoize the `.push` property load separately from the actual `push()` call. This would be incorrect because:
129 1. Reading a method from an object and calling it are semantically one operation
130 2. The property load's value (the bound method) is only valid immediately when called on the same receiver
131 3. Separate memoization could lead to stale method references or incorrect this-binding