| 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 |