[compiler] Promote temporaries when necessary to prevent codegen reordering over side-effectful operations
ghstack-source-id: 639191e63a0d2b4290d1265a2da12fb17de750d9 Pull Request resolved: https://github.com/facebook/react/pull/30554
Mike Vitousek committed
Aug 12, 2024 at 12:36 UTC
d50e024fd49cbd701e7e286441ef2b6b0b59ba62
11 files changed
+441
-136
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+4
-4
@@ -452,17 +452,17 @@ function* runWithEnvironment(
452
value: reactiveFunction,
453
});
454
455
- promoteUsedTemporaries(reactiveFunction);
455
+ pruneUnusedLValues(reactiveFunction);
456
yield log({
457
kind: 'reactive',
458
- name: 'PromoteUsedTemporaries',
458
+ name: 'PruneUnusedLValues',
459
value: reactiveFunction,
460
});
461
462
- pruneUnusedLValues(reactiveFunction);
462
+ promoteUsedTemporaries(reactiveFunction);
463
yield log({
464
kind: 'reactive',
465
- name: 'PruneUnusedLValues',
465
+ name: 'PromoteUsedTemporaries',
466
value: reactiveFunction,
467
});
468
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts
+4
-4
@@ -1209,7 +1209,7 @@ function codegenInstructionNullable(
1209
value = null;
1210
} else {
1211
lvalue = instr.value.lvalue.pattern;
1212
- let hasReasign = false;
1212
+ let hasReassign = false;
1213
let hasDeclaration = false;
1214
for (const place of eachPatternOperand(lvalue)) {
1215
if (
@@ -1219,10 +1219,10 @@ function codegenInstructionNullable(
1219
cx.temp.set(place.identifier.declarationId, null);
1220
}
1221
const isDeclared = cx.hasDeclared(place.identifier);
1222
- hasReasign ||= isDeclared;
1222
+ hasReassign ||= isDeclared;
1223
hasDeclaration ||= !isDeclared;
1224
}
1225
- if (hasReasign && hasDeclaration) {
1225
+ if (hasReassign && hasDeclaration) {
1226
CompilerError.invariant(false, {
1227
reason:
1228
'Encountered a destructuring operation where some identifiers are already declared (reassignments) but others are not (declarations)',
@@ -1230,7 +1230,7 @@ function codegenInstructionNullable(
1230
loc: instr.loc,
1231
suggestions: null,
1232
});
1233
- } else if (hasReasign) {
1233
+ } else if (hasReassign) {
1234
kind = InstructionKind.Reassign;
1235
}
1236
value = codegenPlaceToExpression(cx, instr.value.value);
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PromoteUsedTemporaries.ts
+203
@@ -15,13 +15,17 @@ import {
15
PrunedReactiveScopeBlock,
16
ReactiveFunction,
17
ReactiveScope,
18
+ ReactiveInstruction,
19
ReactiveScopeBlock,
20
ReactiveValue,
21
ScopeId,
22
+ SpreadPattern,
23
promoteTemporary,
24
promoteTemporaryJsxTag,
25
+ IdentifierId,
26
} from '../HIR/HIR';
27
import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors';
28
+import {eachInstructionValueLValue, eachPatternOperand} from '../HIR/visitors';
29
30
/**
31
* Phase 2: Promote identifiers which are used in a place that requires a named variable.
@@ -225,6 +229,199 @@ class CollectPromotableTemporaries extends ReactiveFunctionVisitor<State> {
229
}
230
}
231
232
+type InterState = Map<IdentifierId, [Identifier, boolean]>;
233
+class PromoteInterposedTemporaries extends ReactiveFunctionVisitor<InterState> {
234
+ #promotable: State;
235
+ #consts: Set<IdentifierId> = new Set();
236
+ #globals: Set<IdentifierId> = new Set();
237
+
238
+ /*
239
+ * Unpromoted temporaries will be emitted at their use sites rather than as separate
240
+ * declarations. However, this causes errors if an interposing temporary has been
241
+ * promoted, or if an interposing instruction has had its lvalues deleted, because such
242
+ * temporaries will be emitted as separate statements, which can effectively cause
243
+ * code to be reordered, and when that code has side effects that changes program behavior.
244
+ * This visitor promotes temporarties that have such interposing instructions to preserve
245
+ * source ordering.
246
+ */
247
+ constructor(promotable: State, params: Array<Place | SpreadPattern>) {
248
+ super();
249
+ params.forEach(param => {
250
+ switch (param.kind) {
251
+ case 'Identifier':
252
+ this.#consts.add(param.identifier.id);
253
+ break;
254
+ case 'Spread':
255
+ this.#consts.add(param.place.identifier.id);
256
+ break;
257
+ }
258
+ });
259
+ this.#promotable = promotable;
260
+ }
261
+
262
+ override visitPlace(
263
+ _id: InstructionId,
264
+ place: Place,
265
+ state: InterState,
266
+ ): void {
267
+ const promo = state.get(place.identifier.id);
268
+ if (promo) {
269
+ const [identifier, needsPromotion] = promo;
270
+ if (
271
+ needsPromotion &&
272
+ identifier.name === null &&
273
+ !this.#consts.has(identifier.id)
274
+ ) {
275
+ /*
276
+ * If the identifier hasn't been promoted but is marked as needing
277
+ * promotion by the logic in `visitInstruction`, and we've seen a
278
+ * use of it after said marking, promote it
279
+ */
280
+ promoteIdentifier(identifier, this.#promotable);
281
+ }
282
+ }
283
+ }
284
+
285
+ override visitInstruction(
286
+ instruction: ReactiveInstruction,
287
+ state: InterState,
288
+ ): void {
289
+ for (const lval of eachInstructionValueLValue(instruction.value)) {
290
+ CompilerError.invariant(lval.identifier.name != null, {
291
+ reason:
292
+ 'PromoteInterposedTemporaries: Assignment targets not expected to be temporaries',
293
+ loc: instruction.loc,
294
+ });
295
+ }
296
+
297
+ switch (instruction.value.kind) {
298
+ case 'CallExpression':
299
+ case 'MethodCall':
300
+ case 'Await':
301
+ case 'PropertyStore':
302
+ case 'PropertyDelete':
303
+ case 'ComputedStore':
304
+ case 'ComputedDelete':
305
+ case 'PostfixUpdate':
306
+ case 'PrefixUpdate':
307
+ case 'StoreLocal':
308
+ case 'StoreContext':
309
+ case 'StoreGlobal':
310
+ case 'Destructure': {
311
+ let constStore = false;
312
+
313
+ if (
314
+ (instruction.value.kind === 'StoreContext' ||
315
+ instruction.value.kind === 'StoreLocal') &&
316
+ (instruction.value.lvalue.kind === 'Const' ||
317
+ instruction.value.lvalue.kind === 'HoistedConst')
318
+ ) {
319
+ /*
320
+ * If an identifier is const, we don't need to worry about it
321
+ * being mutated between being loaded and being used
322
+ */
323
+ this.#consts.add(instruction.value.lvalue.place.identifier.id);
324
+ constStore = true;
325
+ }
326
+ if (
327
+ instruction.value.kind === 'Destructure' &&
328
+ (instruction.value.lvalue.kind === 'Const' ||
329
+ instruction.value.lvalue.kind === 'HoistedConst')
330
+ ) {
331
+ [...eachPatternOperand(instruction.value.lvalue.pattern)].forEach(
332
+ ident => this.#consts.add(ident.identifier.id),
333
+ );
334
+ constStore = true;
335
+ }
336
+ if (instruction.value.kind === 'MethodCall') {
337
+ // Treat property of method call as constlike so we don't promote it.
338
+ this.#consts.add(instruction.value.property.identifier.id);
339
+ }
340
+
341
+ super.visitInstruction(instruction, state);
342
+ if (
343
+ !constStore &&
344
+ (instruction.lvalue == null ||
345
+ instruction.lvalue.identifier.name != null)
346
+ ) {
347
+ /*
348
+ * If we've stripped the lvalue or promoted the lvalue, then we will emit this instruction
349
+ * as a statement in codegen.
350
+ *
351
+ * If this instruction will be emitted directly as a statement rather than as a temporary
352
+ * during codegen, then it can interpose between the defs and the uses of other temporaries.
353
+ * Since this instruction could potentially mutate those defs, it's not safe to relocate
354
+ * the definition of those temporaries to after this instruction. Mark all those temporaries
355
+ * as needing promotion, but don't promote them until we actually see them being used.
356
+ */
357
+ for (const [key, [ident, _]] of state.entries()) {
358
+ state.set(key, [ident, true]);
359
+ }
360
+ }
361
+ if (instruction.lvalue && instruction.lvalue.identifier.name === null) {
362
+ // Add this instruction's lvalue to the state, initially not marked as needing promotion
363
+ state.set(instruction.lvalue.identifier.id, [
364
+ instruction.lvalue.identifier,
365
+ false,
366
+ ]);
367
+ }
368
+ break;
369
+ }
370
+ case 'DeclareContext':
371
+ case 'DeclareLocal': {
372
+ if (
373
+ instruction.value.lvalue.kind === 'Const' ||
374
+ instruction.value.lvalue.kind === 'HoistedConst'
375
+ ) {
376
+ this.#consts.add(instruction.value.lvalue.place.identifier.id);
377
+ }
378
+ super.visitInstruction(instruction, state);
379
+ break;
380
+ }
381
+ case 'LoadContext':
382
+ case 'LoadLocal': {
383
+ if (instruction.lvalue && instruction.lvalue.identifier.name === null) {
384
+ if (this.#consts.has(instruction.value.place.identifier.id)) {
385
+ this.#consts.add(instruction.lvalue.identifier.id);
386
+ }
387
+ state.set(instruction.lvalue.identifier.id, [
388
+ instruction.lvalue.identifier,
389
+ false,
390
+ ]);
391
+ }
392
+ super.visitInstruction(instruction, state);
393
+ break;
394
+ }
395
+ case 'PropertyLoad':
396
+ case 'ComputedLoad': {
397
+ if (instruction.lvalue) {
398
+ if (this.#globals.has(instruction.value.object.identifier.id)) {
399
+ this.#globals.add(instruction.lvalue.identifier.id);
400
+ this.#consts.add(instruction.lvalue.identifier.id);
401
+ }
402
+ if (instruction.lvalue.identifier.name === null) {
403
+ state.set(instruction.lvalue.identifier.id, [
404
+ instruction.lvalue.identifier,
405
+ false,
406
+ ]);
407
+ }
408
+ }
409
+ super.visitInstruction(instruction, state);
410
+ break;
411
+ }
412
+ case 'LoadGlobal': {
413
+ instruction.lvalue &&
414
+ this.#globals.add(instruction.lvalue.identifier.id);
415
+ super.visitInstruction(instruction, state);
416
+ break;
417
+ }
418
+ default: {
419
+ super.visitInstruction(instruction, state);
420
+ }
421
+ }
422
+ }
423
+}
424
+
425
export function promoteUsedTemporaries(fn: ReactiveFunction): void {
426
const state: State = {
427
tags: new Set(),
@@ -239,6 +436,12 @@ export function promoteUsedTemporaries(fn: ReactiveFunction): void {
436
}
437
}
438
visitReactiveFunction(fn, new PromoteTemporaries(), state);
439
+
440
+ visitReactiveFunction(
441
+ fn,
442
+ new PromoteInterposedTemporaries(state, fn.params),
443
+ new Map(),
444
+ );
445
visitReactiveFunction(
446
fn,
447
new PromoteAllInstancedOfPromotedTemporaries(),
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-codegen-inline-iife.expect.md
deleted
-92
@@ -1,92 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-import {makeArray, print} from 'shared-runtime';
6
-
7
-/**
8
- * Exposes bug involving iife inlining + codegen.
9
- * We currently inline iifes to labeled blocks (not value-blocks).
10
- *
11
- * Here, print(1) and the evaluation of makeArray(...) get the same scope
12
- * as the compiler infers that the makeArray call may mutate its arguments.
13
- * Since print(1) does not get its own scope (and is thus not a declaration
14
- * or dependency), it does not get promoted.
15
- * As a result, print(1) gets reordered across the labeled-block instructions
16
- * to be inlined at the makeArray callsite.
17
- *
18
- * Current evaluator results:
19
- * Found differences in evaluator results
20
- * Non-forget (expected):
21
- * (kind: ok) [null,2]
22
- * logs: [1,2]
23
- * Forget:
24
- * (kind: ok) [null,2]
25
- * logs: [2,1]
26
- */
27
-function useTest() {
28
- return makeArray<number | void>(
29
- print(1),
30
- (function foo() {
31
- print(2);
32
- return 2;
33
- })(),
34
- );
35
-}
36
-
37
-export const FIXTURE_ENTRYPOINT = {
38
- fn: useTest,
39
- params: [],
40
-};
41
-
42
-```
43
-
44
-## Code
45
-
46
-```javascript
47
-import { c as _c } from "react/compiler-runtime";
48
-import { makeArray, print } from "shared-runtime";
49
-
50
-/**
51
- * Exposes bug involving iife inlining + codegen.
52
- * We currently inline iifes to labeled blocks (not value-blocks).
53
- *
54
- * Here, print(1) and the evaluation of makeArray(...) get the same scope
55
- * as the compiler infers that the makeArray call may mutate its arguments.
56
- * Since print(1) does not get its own scope (and is thus not a declaration
57
- * or dependency), it does not get promoted.
58
- * As a result, print(1) gets reordered across the labeled-block instructions
59
- * to be inlined at the makeArray callsite.
60
- *
61
- * Current evaluator results:
62
- * Found differences in evaluator results
63
- * Non-forget (expected):
64
- * (kind: ok) [null,2]
65
- * logs: [1,2]
66
- * Forget:
67
- * (kind: ok) [null,2]
68
- * logs: [2,1]
69
- */
70
-function useTest() {
71
- const $ = _c(1);
72
- let t0;
73
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
74
- let t1;
75
-
76
- print(2);
77
- t1 = 2;
78
- t0 = makeArray(print(1), t1);
79
- $[0] = t0;
80
- } else {
81
- t0 = $[0];
82
- }
83
- return t0;
84
-}
85
-
86
-export const FIXTURE_ENTRYPOINT = {
87
- fn: useTest,
88
- params: [],
89
-};
90
-
91
-```
92
-
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-codegen-inline-iife.ts
deleted
-36
@@ -1,36 +0,0 @@
1
-import {makeArray, print} from 'shared-runtime';
2
-
3
-/**
4
- * Exposes bug involving iife inlining + codegen.
5
- * We currently inline iifes to labeled blocks (not value-blocks).
6
- *
7
- * Here, print(1) and the evaluation of makeArray(...) get the same scope
8
- * as the compiler infers that the makeArray call may mutate its arguments.
9
- * Since print(1) does not get its own scope (and is thus not a declaration
10
- * or dependency), it does not get promoted.
11
- * As a result, print(1) gets reordered across the labeled-block instructions
12
- * to be inlined at the makeArray callsite.
13
- *
14
- * Current evaluator results:
15
- * Found differences in evaluator results
16
- * Non-forget (expected):
17
- * (kind: ok) [null,2]
18
- * logs: [1,2]
19
- * Forget:
20
- * (kind: ok) [null,2]
21
- * logs: [2,1]
22
- */
23
-function useTest() {
24
- return makeArray<number | void>(
25
- print(1),
26
- (function foo() {
27
- print(2);
28
- return 2;
29
- })(),
30
- );
31
-}
32
-
33
-export const FIXTURE_ENTRYPOINT = {
34
- fn: useTest,
35
- params: [],
36
-};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-reassign.expect.md
new
+62
@@ -0,0 +1,62 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import {makeArray, print} from 'shared-runtime';
6
+
7
+function useTest() {
8
+ let w = {};
9
+ return makeArray(
10
+ (w = 42),
11
+ w,
12
+ (function foo() {
13
+ w = 999;
14
+ return 2;
15
+ })(),
16
+ );
17
+}
18
+
19
+export const FIXTURE_ENTRYPOINT = {
20
+ fn: useTest,
21
+ params: [],
22
+};
23
+
24
+```
25
+
26
+## Code
27
+
28
+```javascript
29
+import { c as _c } from "react/compiler-runtime";
30
+import { makeArray, print } from "shared-runtime";
31
+
32
+function useTest() {
33
+ const $ = _c(1);
34
+ let t0;
35
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
36
+ let w;
37
+ w = {};
38
+
39
+ const t1 = (w = 42);
40
+ const t2 = w;
41
+
42
+ w;
43
+ let t3;
44
+ w = 999;
45
+ t3 = 2;
46
+ t0 = makeArray(t1, t2, t3);
47
+ $[0] = t0;
48
+ } else {
49
+ t0 = $[0];
50
+ }
51
+ return t0;
52
+}
53
+
54
+export const FIXTURE_ENTRYPOINT = {
55
+ fn: useTest,
56
+ params: [],
57
+};
58
+
59
+```
60
+
61
+### Eval output
62
+(kind: ok) [42,42,2]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-reassign.ts
new
+18
@@ -0,0 +1,18 @@
1
+import {makeArray, print} from 'shared-runtime';
2
+
3
+function useTest() {
4
+ let w = {};
5
+ return makeArray(
6
+ (w = 42),
7
+ w,
8
+ (function foo() {
9
+ w = 999;
10
+ return 2;
11
+ })(),
12
+ );
13
+}
14
+
15
+export const FIXTURE_ENTRYPOINT = {
16
+ fn: useTest,
17
+ params: [],
18
+};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-storeprop.expect.md
new
+60
@@ -0,0 +1,60 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import {makeArray, print} from 'shared-runtime';
6
+
7
+function useTest() {
8
+ let w = {};
9
+ return makeArray(
10
+ (w.x = 42),
11
+ w.x,
12
+ (function foo() {
13
+ w.x = 999;
14
+ return 2;
15
+ })(),
16
+ );
17
+}
18
+
19
+export const FIXTURE_ENTRYPOINT = {
20
+ fn: useTest,
21
+ params: [],
22
+};
23
+
24
+```
25
+
26
+## Code
27
+
28
+```javascript
29
+import { c as _c } from "react/compiler-runtime";
30
+import { makeArray, print } from "shared-runtime";
31
+
32
+function useTest() {
33
+ const $ = _c(1);
34
+ let t0;
35
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
36
+ const w = {};
37
+
38
+ const t1 = (w.x = 42);
39
+ const t2 = w.x;
40
+ let t3;
41
+
42
+ w.x = 999;
43
+ t3 = 2;
44
+ t0 = makeArray(t1, t2, t3);
45
+ $[0] = t0;
46
+ } else {
47
+ t0 = $[0];
48
+ }
49
+ return t0;
50
+}
51
+
52
+export const FIXTURE_ENTRYPOINT = {
53
+ fn: useTest,
54
+ params: [],
55
+};
56
+
57
+```
58
+
59
+### Eval output
60
+(kind: ok) [42,42,2]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-storeprop.ts
new
+18
@@ -0,0 +1,18 @@
1
+import {makeArray, print} from 'shared-runtime';
2
+
3
+function useTest() {
4
+ let w = {};
5
+ return makeArray(
6
+ (w.x = 42),
7
+ w.x,
8
+ (function foo() {
9
+ w.x = 999;
10
+ return 2;
11
+ })(),
12
+ );
13
+}
14
+
15
+export const FIXTURE_ENTRYPOINT = {
16
+ fn: useTest,
17
+ params: [],
18
+};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife.expect.md
new
+56
@@ -0,0 +1,56 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import {makeArray, print} from 'shared-runtime';
6
+
7
+function useTest() {
8
+ return makeArray<number | void>(
9
+ print(1),
10
+ (function foo() {
11
+ print(2);
12
+ return 2;
13
+ })(),
14
+ );
15
+}
16
+
17
+export const FIXTURE_ENTRYPOINT = {
18
+ fn: useTest,
19
+ params: [],
20
+};
21
+
22
+```
23
+
24
+## Code
25
+
26
+```javascript
27
+import { c as _c } from "react/compiler-runtime";
28
+import { makeArray, print } from "shared-runtime";
29
+
30
+function useTest() {
31
+ const $ = _c(1);
32
+ let t0;
33
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
34
+ const t1 = print(1);
35
+ let t2;
36
+
37
+ print(2);
38
+ t2 = 2;
39
+ t0 = makeArray(t1, t2);
40
+ $[0] = t0;
41
+ } else {
42
+ t0 = $[0];
43
+ }
44
+ return t0;
45
+}
46
+
47
+export const FIXTURE_ENTRYPOINT = {
48
+ fn: useTest,
49
+ params: [],
50
+};
51
+
52
+```
53
+
54
+### Eval output
55
+(kind: ok) [null,2]
56
+logs: [1,2]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife.ts
new
+16
@@ -0,0 +1,16 @@
1
+import {makeArray, print} from 'shared-runtime';
2
+
3
+function useTest() {
4
+ return makeArray<number | void>(
5
+ print(1),
6
+ (function foo() {
7
+ print(2);
8
+ return 2;
9
+ })(),
10
+ );
11
+}
12
+
13
+export const FIXTURE_ENTRYPOINT = {
14
+ fn: useTest,
15
+ params: [],
16
+};