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