main
ts 208 lines 5.55 KB
Raw
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 }