| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | */ |
| 7 | |
| 8 | import { |
| 9 | DeclarationId, |
| 10 | Destructure, |
| 11 | Environment, |
| 12 | IdentifierId, |
| 13 | InstructionKind, |
| 14 | Place, |
| 15 | ReactiveFunction, |
| 16 | ReactiveInstruction, |
| 17 | ReactiveScopeBlock, |
| 18 | ReactiveStatement, |
| 19 | promoteTemporary, |
| 20 | } from '../HIR'; |
| 21 | import {clonePlaceToTemporary} from '../HIR/HIRBuilder'; |
| 22 | import { |
| 23 | eachInstructionLValueWithKind, |
| 24 | eachPatternOperand, |
| 25 | mapPatternOperands, |
| 26 | } from '../HIR/visitors'; |
| 27 | import { |
| 28 | ReactiveFunctionTransform, |
| 29 | Transformed, |
| 30 | visitReactiveFunction, |
| 31 | } from './visitors'; |
| 32 | |
| 33 | /* |
| 34 | * Destructuring statements may sometimes define some variables which are declared by the scope, |
| 35 | * and others that are only used locally within the scope, for example: |
| 36 | * |
| 37 | * ``` |
| 38 | * const {x, ...rest} = value; |
| 39 | * return rest; |
| 40 | * ``` |
| 41 | * |
| 42 | * Here the scope structure turns into: |
| 43 | * |
| 44 | * ``` |
| 45 | * let c_0 = $[0] !== value; |
| 46 | * let rest; |
| 47 | * if (c_0) { |
| 48 | * // OOPS! we want to reassign `rest` here, but |
| 49 | * // `x` isn't declared anywhere! |
| 50 | * {x, ...rest} = value; |
| 51 | * $[0] = value; |
| 52 | * $[1] = rest; |
| 53 | * } else { |
| 54 | * rest = $[1]; |
| 55 | * } |
| 56 | * return rest; |
| 57 | * ``` |
| 58 | * |
| 59 | * Note that because `rest` is declared by the scope, we can't redeclare it in the |
| 60 | * destructuring statement. But we have to declare `x`! |
| 61 | * |
| 62 | * This pass finds destructuring instructions that contain mixed values such as this, |
| 63 | * and rewrites them to ensure that any scope variable assignments are extracted first |
| 64 | * to a temporary and reassigned in a separate instruction. For example, the output |
| 65 | * for the above would be along the lines of: |
| 66 | * |
| 67 | * ``` |
| 68 | * let c_0 = $[0] !== value; |
| 69 | * let rest; |
| 70 | * if (c_0) { |
| 71 | * const {x, ...t0} = value; <-- replace `rest` with a temporary |
| 72 | * rest = t0; // <-- and create a separate instruction to assign that to `rest` |
| 73 | * $[0] = value; |
| 74 | * $[1] = rest; |
| 75 | * } else { |
| 76 | * rest = $[1]; |
| 77 | * } |
| 78 | * return rest; |
| 79 | * ``` |
| 80 | * |
| 81 | */ |
| 82 | export function extractScopeDeclarationsFromDestructuring( |
| 83 | fn: ReactiveFunction, |
| 84 | ): void { |
| 85 | const state = new State(fn.env); |
| 86 | for (const param of fn.params) { |
| 87 | const place = param.kind === 'Identifier' ? param : param.place; |
| 88 | state.declared.add(place.identifier.declarationId); |
| 89 | } |
| 90 | visitReactiveFunction(fn, new Visitor(), state); |
| 91 | } |
| 92 | |
| 93 | class State { |
| 94 | env: Environment; |
| 95 | /** |
| 96 | * We need to track which program variables are already declared to convert |
| 97 | * declarations into reassignments, so we use DeclarationId |
| 98 | */ |
| 99 | declared: Set<DeclarationId> = new Set(); |
| 100 | |
| 101 | constructor(env: Environment) { |
| 102 | this.env = env; |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | class Visitor extends ReactiveFunctionTransform<State> { |
| 107 | override visitScope(scope: ReactiveScopeBlock, state: State): void { |
| 108 | for (const [, declaration] of scope.scope.declarations) { |
| 109 | state.declared.add(declaration.identifier.declarationId); |
| 110 | } |
| 111 | this.traverseScope(scope, state); |
| 112 | } |
| 113 | |
| 114 | override transformInstruction( |
| 115 | instruction: ReactiveInstruction, |
| 116 | state: State, |
| 117 | ): Transformed<ReactiveStatement> { |
| 118 | this.visitInstruction(instruction, state); |
| 119 | |
| 120 | let instructionsToProcess: Array<ReactiveInstruction> = [instruction]; |
| 121 | let result: Transformed<ReactiveStatement> = {kind: 'keep'}; |
| 122 | |
| 123 | if (instruction.value.kind === 'Destructure') { |
| 124 | const transformed = transformDestructuring( |
| 125 | state, |
| 126 | instruction, |
| 127 | instruction.value, |
| 128 | ); |
| 129 | if (transformed) { |
| 130 | instructionsToProcess = transformed; |
| 131 | result = { |
| 132 | kind: 'replace-many', |
| 133 | value: transformed.map(instruction => ({ |
| 134 | kind: 'instruction', |
| 135 | instruction, |
| 136 | })), |
| 137 | }; |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | // Update state.declared with declarations from the instruction(s) |
| 142 | for (const instr of instructionsToProcess) { |
| 143 | for (const [place, kind] of eachInstructionLValueWithKind(instr)) { |
| 144 | if (kind !== InstructionKind.Reassign) { |
| 145 | state.declared.add(place.identifier.declarationId); |
| 146 | } |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | return result; |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | function transformDestructuring( |
| 155 | state: State, |
| 156 | instr: ReactiveInstruction, |
| 157 | destructure: Destructure, |
| 158 | ): null | Array<ReactiveInstruction> { |
| 159 | let reassigned: Set<IdentifierId> = new Set(); |
| 160 | let hasDeclaration = false; |
| 161 | for (const place of eachPatternOperand(destructure.lvalue.pattern)) { |
| 162 | const isDeclared = state.declared.has(place.identifier.declarationId); |
| 163 | if (isDeclared) { |
| 164 | reassigned.add(place.identifier.id); |
| 165 | } else { |
| 166 | hasDeclaration = true; |
| 167 | } |
| 168 | } |
| 169 | if (!hasDeclaration) { |
| 170 | // all reassignments |
| 171 | destructure.lvalue.kind = InstructionKind.Reassign; |
| 172 | return null; |
| 173 | } |
| 174 | /* |
| 175 | * Else it's a mix, replace the reassigned items in the destructuring with temporary |
| 176 | * variables and emit separate assignment statements for them |
| 177 | */ |
| 178 | const instructions: Array<ReactiveInstruction> = []; |
| 179 | const renamed: Map<Place, Place> = new Map(); |
| 180 | mapPatternOperands(destructure.lvalue.pattern, place => { |
| 181 | if (!reassigned.has(place.identifier.id)) { |
| 182 | return place; |
| 183 | } |
| 184 | const temporary = clonePlaceToTemporary(state.env, place); |
| 185 | promoteTemporary(temporary.identifier); |
| 186 | renamed.set(place, temporary); |
| 187 | return temporary; |
| 188 | }); |
| 189 | instructions.push(instr); |
| 190 | for (const [original, temporary] of renamed) { |
| 191 | instructions.push({ |
| 192 | id: instr.id, |
| 193 | lvalue: null, |
| 194 | value: { |
| 195 | kind: 'StoreLocal', |
| 196 | lvalue: { |
| 197 | kind: InstructionKind.Reassign, |
| 198 | place: original, |
| 199 | }, |
| 200 | value: temporary, |
| 201 | type: null, |
| 202 | loc: destructure.loc, |
| 203 | }, |
| 204 | loc: instr.loc, |
| 205 | }); |
| 206 | } |
| 207 | return instructions; |
| 208 | } |