main
md 151 lines 6.15 KB
Rendered Raw
1 # rewriteInstructionKindsBasedOnReassignment
2
3 ## File
4 `src/SSA/RewriteInstructionKindsBasedOnReassignment.ts`
5
6 ## Purpose
7 Rewrites the `InstructionKind` of variable declaration and assignment instructions to correctly reflect whether variables should be declared as `const` or `let` in the final output. It determines this based on whether a variable is subsequently reassigned after its initial declaration.
8
9 The key insight is that this pass runs **after dead code elimination (DCE)**, so a variable that was originally declared with `let` in the source (because it was reassigned) may be converted to `const` if the reassignment was removed by DCE. However, variables originally declared as `const` cannot become `let`.
10
11 ## Input Invariants
12 - SSA form: Each identifier has a unique `IdentifierId` and `DeclarationId`
13 - Dead code elimination has run: Unused assignments have been removed
14 - Mutation/aliasing inference complete: Runs after `InferMutationAliasingRanges` and `InferReactivePlaces` in the main pipeline
15 - All instruction kinds are initially set (typically `Let` for variables that may be reassigned)
16
17 ## Output Guarantees
18 - **First declaration gets `Const` or `Let`**: The first `StoreLocal` for a named variable is marked as:
19 - `InstructionKind.Const` if the variable is never reassigned after
20 - `InstructionKind.Let` if the variable has subsequent reassignments
21 - **Reassignments marked as `Reassign`**: Any subsequent `StoreLocal` to the same `DeclarationId` is marked as `InstructionKind.Reassign`
22 - **Destructure consistency**: All places in a destructuring pattern must have consistent kinds (all Const or all Reassign)
23 - **Update operations trigger Let**: `PrefixUpdate` and `PostfixUpdate` operations (like `++x` or `x--`) mark the original declaration as `Let`
24
25 ## Algorithm
26
27 1. **Initialize declarations map**: Create a `Map<DeclarationId, LValue | LValuePattern>` to track declared variables.
28
29 2. **Seed with parameters and context**: Add all named function parameters and captured context variables to the map with kind `Let` (since they're already "declared" outside the function body).
30
31 3. **Process blocks in order**: Iterate through all blocks and instructions:
32
33 - **DeclareLocal**: Record the declaration in the map (invariant: must not already exist)
34
35 - **StoreLocal**:
36 - If not in map: This is the first store, add to map with `kind = Const`
37 - If already in map: This is a reassignment. Update original declaration to `Let`, set current instruction to `Reassign`
38
39 - **Destructure**:
40 - For each operand in the pattern, check if it's already declared
41 - All operands must be consistent (all new declarations OR all reassignments)
42 - Set pattern kind to `Const` for new declarations, `Reassign` for existing ones
43
44 - **PrefixUpdate / PostfixUpdate**: Look up the declaration and mark it as `Let` (these always imply reassignment)
45
46 ## Key Data Structures
47
48 ```typescript
49 // Main tracking structure
50 const declarations = new Map<DeclarationId, LValue | LValuePattern>();
51
52 // InstructionKind enum (from HIR.ts)
53 enum InstructionKind {
54 Const = 'Const', // const declaration
55 Let = 'Let', // let declaration
56 Reassign = 'Reassign', // reassignment to existing binding
57 Catch = 'Catch', // catch clause binding
58 HoistedLet = 'HoistedLet', // hoisted let
59 HoistedConst = 'HoistedConst', // hoisted const
60 HoistedFunction = 'HoistedFunction', // hoisted function
61 Function = 'Function', // function declaration
62 }
63 ```
64
65 ## Edge Cases
66
67 ### DCE Removes Reassignment
68 A `let x = 0; x = 1;` where `x = 1` is unused becomes `const x = 0;` after DCE.
69
70 ### Destructuring with Mixed Operands
71 The invariant checks ensure all operands in a destructure pattern are either all new declarations or all reassignments. Mixed cases cause a compiler error.
72
73 ### Value Blocks with DCE
74 There's a TODO for handling reassignment in value blocks where the original declaration was removed by DCE.
75
76 ### Parameters and Context Variables
77 These are pre-seeded as `Let` in the declarations map since they're conceptually "declared" at function entry.
78
79 ### Update Expressions
80 `++x` and `x--` always mark the variable as `Let`, even if used inline.
81
82 ## TODOs
83 ```typescript
84 CompilerError.invariant(block.kind !== 'value', {
85 reason: `TODO: Handle reassignment in a value block where the original
86 declaration was removed by dead code elimination (DCE)`,
87 ...
88 });
89 ```
90
91 This indicates an edge case where a destructuring reassignment occurs in a value block but the original declaration was eliminated by DCE. This is currently an invariant violation rather than handled gracefully.
92
93 ## Example
94
95 ### Fixture: `reassignment.js`
96
97 **Input Source:**
98 ```javascript
99 function Component(props) {
100 let x = [];
101 x.push(props.p0);
102 let y = x;
103
104 x = [];
105 let _ = <Component x={x} />;
106
107 y.push(props.p1);
108
109 return <Component x={x} y={y} />;
110 }
111 ```
112
113 **Before Pass (InferReactivePlaces output):**
114 ```
115 [2] StoreLocal Let x$32 = $31 // x is initially marked Let
116 [9] StoreLocal Let y$40 = $39 // y is initially marked Let
117 [11] StoreLocal Reassign x$43 = $42 // reassignment already marked
118 ```
119
120 **After Pass:**
121 ```
122 [2] StoreLocal Let x$32 = $31 // x stays Let (has reassignment at line 11)
123 [9] StoreLocal Const y$40 = $39 // y becomes Const (never reassigned)
124 [11] StoreLocal Reassign x$43 = $42 // stays Reassign
125 ```
126
127 **Final Generated Code:**
128 ```javascript
129 function Component(props) {
130 const $ = _c(4);
131 let t0;
132 if ($[0] !== props.p0 || $[1] !== props.p1) {
133 let x = []; // let because reassigned
134 x.push(props.p0);
135 const y = x; // const because never reassigned
136 // ... x = t1; (reassignment)
137 y.push(props.p1);
138 t0 = <Component x={x} y={y} />;
139 // ...
140 }
141 return t0;
142 }
143 ```
144
145 The pass correctly identified that `x` needs `let` (since it's reassigned on line 6 of the source) while `y` can use `const` (it's never reassigned after initialization).
146
147 ## Where This Pass is Called
148
149 1. **Main Pipeline** (`src/Entrypoint/Pipeline.ts:322`): Called after `InferReactivePlaces` and before `InferReactiveScopeVariables`.
150
151 2. **AnalyseFunctions** (`src/Inference/AnalyseFunctions.ts:58`): Called when lowering inner function expressions as part of the function analysis phase.