main
md 128 lines 5.79 KB
Rendered Raw
1 # alignObjectMethodScopes
2
3 ## File
4 `src/ReactiveScopes/AlignObjectMethodScopes.ts`
5
6 ## Purpose
7 Ensures that object method values and their enclosing object expressions share the same reactive scope. This is critical for code generation because JavaScript requires object method definitions to be inlined within their containing object literals. If the object method and object expression were in different reactive scopes (which map to different memoization blocks), the generated code would be invalid since you cannot reference an object method defined in one block from an object literal in a different block.
8
9 From the file's documentation:
10 > "To produce a well-formed JS program in Codegen, object methods and object expressions must be in the same ReactiveBlock as object method definitions must be inlined."
11
12 ## Input Invariants
13 - Reactive scopes have been inferred: This pass runs after `InferReactiveScopeVariables`
14 - ObjectMethod and ObjectExpression have non-null scopes: The pass asserts this with an invariant check
15 - Scopes are disjoint across functions: The pass assumes that scopes do not overlap between parent and nested functions
16
17 ## Output Guarantees
18 - ObjectMethod and ObjectExpression share the same scope: Any ObjectMethod used as a property in an ObjectExpression will have its scope merged with the ObjectExpression's scope
19 - Merged scope covers both ranges: The resulting merged scope's range is expanded to cover the minimum start and maximum end of all merged scopes
20 - All identifiers are repointed: All identifiers whose scopes were merged are updated to point to the canonical root scope
21 - Inner functions are also processed: The pass recursively handles nested ObjectMethod and FunctionExpression values
22
23 ## Algorithm
24
25 ### Phase 1: Find Scopes to Merge (`findScopesToMerge`)
26 1. Iterate through all blocks and instructions in the function
27 2. Track all ObjectMethod declarations in a set by their lvalue identifier
28 3. When encountering an ObjectExpression, check each operand:
29 - If an operand's identifier was previously recorded as an ObjectMethod declaration
30 - Get the scope of both the ObjectMethod operand and the ObjectExpression lvalue
31 - Assert both scopes are non-null
32 - Union these two scopes together in a DisjointSet data structure
33
34 ### Phase 2: Merge and Repoint Scopes (`alignObjectMethodScopes`)
35 1. Recursively process inner functions first (ObjectMethod and FunctionExpression values)
36 2. Canonicalize the DisjointSet to get a mapping from each scope to its root
37 3. **Step 1 - Merge ranges**: For each scope that maps to a different root:
38 - Expand the root's range to encompass both the original range and the merged scope's range
39 - `root.range.start = min(scope.range.start, root.range.start)`
40 - `root.range.end = max(scope.range.end, root.range.end)`
41 4. **Step 2 - Repoint identifiers**: For each instruction's lvalue:
42 - If the identifier has a scope that was merged
43 - Update the identifier's scope reference to point to the canonical root
44
45 ## Key Data Structures
46
47 1. **DisjointSet<ReactiveScope>** - A union-find data structure that tracks which scopes should be merged together. Uses path compression for efficient `find()` operations.
48
49 2. **Set<Identifier>** - Tracks which identifiers are ObjectMethod declarations, used to identify when an ObjectExpression operand is an object method.
50
51 3. **ReactiveScope** - Contains:
52 - `id: ScopeId` - Unique identifier
53 - `range: MutableRange` - Start and end instruction IDs
54 - `dependencies` - Inputs to the scope
55 - `declarations` - Values produced by the scope
56
57 4. **MutableRange** - Has `start` and `end` InstructionId fields that define the scope's extent.
58
59 ## Edge Cases
60
61 ### Nested Object Methods
62 When an object method itself contains another object with methods, the pass recursively processes inner functions first before handling the outer function's scopes.
63
64 ### Multiple Object Methods in Same Object
65 If an object has multiple method properties, all their scopes will be merged with the object's scope through the DisjointSet.
66
67 ### Object Methods in Conditional Expressions
68 Object methods inside ternary expressions still need scope alignment to ensure the method and its containing object are in the same reactive block.
69
70 ### Method Call After Object Creation
71 The pass works in conjunction with `AlignMethodCallScopes` (which runs immediately before) to ensure that method calls on objects with object methods are also properly scoped.
72
73 ## TODOs
74 None explicitly marked in the source file.
75
76 ## Example
77
78 ### Fixture: `object-method-shorthand.js`
79
80 **Input:**
81 ```javascript
82 function Component() {
83 let obj = {
84 method() {
85 return 1;
86 },
87 };
88 return obj.method();
89 }
90 ```
91
92 **Before AlignObjectMethodScopes:**
93 ```
94 InferReactiveScopeVariables:
95 [1] mutate? $12_@0:TObjectMethod = ObjectMethod ... // scope @0
96 [2] mutate? $14_@1[2:7]:TObject = Object { method: ... } // scope @1 (range 2:7)
97 ```
98 The ObjectMethod `$12` is in scope `@0` while the ObjectExpression `$14` is in scope `@1` with range `[2:7]`.
99
100 **After AlignObjectMethodScopes:**
101 ```
102 AlignObjectMethodScopes:
103 [1] mutate? $12_@0[1:7]:TObjectMethod = ObjectMethod ... // scope @0, range now 1:7
104 [2] mutate? $14_@0[1:7]:TObject = Object { method: ... } // also scope @0, range 1:7
105 ```
106 Both identifiers are in the same scope `@0`, and the scope's range has been expanded to `[1:7]` to cover both instructions.
107
108 **Final Generated Code:**
109 ```javascript
110 function Component() {
111 const $ = _c(1);
112 let t0;
113 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
114 const obj = {
115 method() {
116 return 1;
117 },
118 };
119 t0 = obj.method();
120 $[0] = t0;
121 } else {
122 t0 = $[0];
123 }
124 return t0;
125 }
126 ```
127
128 The object literal with its method and the subsequent method call are all inside the same memoization block, producing valid JavaScript where the method definition is inlined within the object literal.