@samitouri / QOS-React / commits / d50e024fd4

[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 +};