@samitouri / QOS-React-1 / commits / 66cfe048d3

[compiler] New mutability/aliasing model (#33494)

Squashed, review-friendly version of the stack from https://github.com/facebook/react/pull/33488. This is new version of our mutability and inference model, designed to replace the core algorithm for determining the sets of instructions involved in constructing a given value or set of values. The new model replaces InferReferenceEffects, InferMutableRanges (and all of its subcomponents), and parts of AnalyzeFunctions. The new model does not use per-Place effect values, but in order to make this drop-in the end _result_ of the inference adds these per-Place effects. I'll write up a larger document on the model, first i'm doing some housekeeping to rebase the PR. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33494). * #33571 * #33558 * #33547 * #33543 * #33533 * #33532 * #33530 * #33526 * #33522 * #33518 * #33514 * #33513 * #33512 * #33504 * #33500 * #33497 * #33496 * #33495 * __->__ #33494 * #33572

Joseph Savona committed Jun 18, 2025 at 12:58 UTC 66cfe048d3ab02afd3eeba9e8d7710acb3a4ab38
119 files changed +7247 -343
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts
+39 -9
@@ -104,6 +104,8 @@ import {validateNoImpureFunctionsInRender} from '../Validation/ValidateNoImpureF
104 import {CompilerError} from '..';
105 import {validateStaticComponents} from '../Validation/ValidateStaticComponents';
106 import {validateNoFreezingKnownMutableFunctions} from '../Validation/ValidateNoFreezingKnownMutableFunctions';
107 +import {inferMutationAliasingEffects} from '../Inference/InferMutationAliasingEffects';
108 +import {inferMutationAliasingRanges} from '../Inference/InferMutationAliasingRanges';
109
110 export type CompilerPipelineValue =
111 | {kind: 'ast'; name: string; value: CodegenFunction}
@@ -227,15 +229,27 @@ function runWithEnvironment(
229 analyseFunctions(hir);
230 log({kind: 'hir', name: 'AnalyseFunctions', value: hir});
231
230 - const fnEffectErrors = inferReferenceEffects(hir);
231 - if (env.isInferredMemoEnabled) {
232 - if (fnEffectErrors.length > 0) {
233 - CompilerError.throw(fnEffectErrors[0]);
232 + if (!env.config.enableNewMutationAliasingModel) {
233 + const fnEffectErrors = inferReferenceEffects(hir);
234 + if (env.isInferredMemoEnabled) {
235 + if (fnEffectErrors.length > 0) {
236 + CompilerError.throw(fnEffectErrors[0]);
237 + }
238 + }
239 + log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
240 + } else {
241 + const mutabilityAliasingErrors = inferMutationAliasingEffects(hir);
242 + log({kind: 'hir', name: 'InferMutationAliasingEffects', value: hir});
243 + if (env.isInferredMemoEnabled) {
244 + if (mutabilityAliasingErrors.isErr()) {
245 + throw mutabilityAliasingErrors.unwrapErr();
246 + }
247 }
248 }
236 - log({kind: 'hir', name: 'InferReferenceEffects', value: hir});
249
238 - validateLocalsNotReassignedAfterRender(hir);
250 + if (!env.config.enableNewMutationAliasingModel) {
251 + validateLocalsNotReassignedAfterRender(hir);
252 + }
253
254 // Note: Has to come after infer reference effects because "dead" code may still affect inference
255 deadCodeElimination(hir);
@@ -249,8 +263,21 @@ function runWithEnvironment(
263 pruneMaybeThrows(hir);
264 log({kind: 'hir', name: 'PruneMaybeThrows', value: hir});
265
252 - inferMutableRanges(hir);
253 - log({kind: 'hir', name: 'InferMutableRanges', value: hir});
266 + if (!env.config.enableNewMutationAliasingModel) {
267 + inferMutableRanges(hir);
268 + log({kind: 'hir', name: 'InferMutableRanges', value: hir});
269 + } else {
270 + const mutabilityAliasingErrors = inferMutationAliasingRanges(hir, {
271 + isFunctionExpression: false,
272 + });
273 + log({kind: 'hir', name: 'InferMutationAliasingRanges', value: hir});
274 + if (env.isInferredMemoEnabled) {
275 + if (mutabilityAliasingErrors.isErr()) {
276 + throw mutabilityAliasingErrors.unwrapErr();
277 + }
278 + validateLocalsNotReassignedAfterRender(hir);
279 + }
280 + }
281
282 if (env.isInferredMemoEnabled) {
283 if (env.config.assertValidMutableRanges) {
@@ -277,7 +304,10 @@ function runWithEnvironment(
304 validateNoImpureFunctionsInRender(hir).unwrap();
305 }
306
280 - if (env.config.validateNoFreezingKnownMutableFunctions) {
307 + if (
308 + env.config.validateNoFreezingKnownMutableFunctions ||
309 + env.config.enableNewMutationAliasingModel
310 + ) {
311 validateNoFreezingKnownMutableFunctions(hir).unwrap();
312 }
313 }
compiler/packages/babel-plugin-react-compiler/src/HIR/AssertValidMutableRanges.ts
+25 -19
@@ -5,13 +5,14 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import invariant from 'invariant';
9 -import {HIRFunction, Identifier, MutableRange} from './HIR';
8 +import {HIRFunction, MutableRange, Place} from './HIR';
9 import {
10 eachInstructionLValue,
11 eachInstructionOperand,
12 eachTerminalOperand,
13 } from './visitors';
14 +import {CompilerError} from '..';
15 +import {printPlace} from './PrintHIR';
16
17 /*
18 * Checks that all mutable ranges in the function are well-formed, with
@@ -20,38 +21,43 @@ import {
21 export function assertValidMutableRanges(fn: HIRFunction): void {
22 for (const [, block] of fn.body.blocks) {
23 for (const phi of block.phis) {
23 - visitIdentifier(phi.place.identifier);
24 - for (const [, operand] of phi.operands) {
25 - visitIdentifier(operand.identifier);
24 + visit(phi.place, `phi for block bb${block.id}`);
25 + for (const [pred, operand] of phi.operands) {
26 + visit(operand, `phi predecessor bb${pred} for block bb${block.id}`);
27 }
28 }
29 for (const instr of block.instructions) {
30 for (const operand of eachInstructionLValue(instr)) {
30 - visitIdentifier(operand.identifier);
31 + visit(operand, `instruction [${instr.id}]`);
32 }
33 for (const operand of eachInstructionOperand(instr)) {
33 - visitIdentifier(operand.identifier);
34 + visit(operand, `instruction [${instr.id}]`);
35 }
36 }
37 for (const operand of eachTerminalOperand(block.terminal)) {
37 - visitIdentifier(operand.identifier);
38 + visit(operand, `terminal [${block.terminal.id}]`);
39 }
40 }
41 }
42
42 -function visitIdentifier(identifier: Identifier): void {
43 - validateMutableRange(identifier.mutableRange);
44 - if (identifier.scope !== null) {
45 - validateMutableRange(identifier.scope.range);
43 +function visit(place: Place, description: string): void {
44 + validateMutableRange(place, place.identifier.mutableRange, description);
45 + if (place.identifier.scope !== null) {
46 + validateMutableRange(place, place.identifier.scope.range, description);
47 }
48 }
49
49 -function validateMutableRange(mutableRange: MutableRange): void {
50 - invariant(
51 - (mutableRange.start === 0 && mutableRange.end === 0) ||
52 - mutableRange.end > mutableRange.start,
53 - 'Identifier scope mutableRange was invalid: [%s:%s]',
54 - mutableRange.start,
55 - mutableRange.end,
50 +function validateMutableRange(
51 + place: Place,
52 + range: MutableRange,
53 + description: string,
54 +): void {
55 + CompilerError.invariant(
56 + (range.start === 0 && range.end === 0) || range.end > range.start,
57 + {
58 + reason: `Invalid mutable range: [${range.start}:${range.end}]`,
59 + description: `${printPlace(place)} in ${description}`,
60 + loc: place.loc,
61 + },
62 );
63 }
compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts
+12 -2
@@ -47,7 +47,7 @@ import {
47 makeType,
48 promoteTemporary,
49 } from './HIR';
50 -import HIRBuilder, {Bindings} from './HIRBuilder';
50 +import HIRBuilder, {Bindings, createTemporaryPlace} from './HIRBuilder';
51 import {BuiltInArrayId} from './ObjectShape';
52
53 /*
@@ -181,6 +181,7 @@ export function lower(
181 loc: GeneratedSource,
182 value: lowerExpressionToTemporary(builder, body),
183 id: makeInstructionId(0),
184 + effects: null,
185 };
186 builder.terminateWithContinuation(terminal, fallthrough);
187 } else if (body.isBlockStatement()) {
@@ -210,6 +211,7 @@ export function lower(
211 loc: GeneratedSource,
212 }),
213 id: makeInstructionId(0),
214 + effects: null,
215 },
216 null,
217 );
@@ -220,6 +222,7 @@ export function lower(
222 fnType: bindings == null ? env.fnType : 'Other',
223 returnTypeAnnotation: null, // TODO: extract the actual return type node if present
224 returnType: makeType(),
225 + returns: createTemporaryPlace(env, func.node.loc ?? GeneratedSource),
226 body: builder.build(),
227 context,
228 generator: func.node.generator === true,
@@ -227,6 +230,7 @@ export function lower(
230 loc: func.node.loc ?? GeneratedSource,
231 env,
232 effects: null,
233 + aliasingEffects: null,
234 directives,
235 });
236 }
@@ -287,6 +291,7 @@ function lowerStatement(
291 loc: stmt.node.loc ?? GeneratedSource,
292 value,
293 id: makeInstructionId(0),
294 + effects: null,
295 };
296 builder.terminate(terminal, 'block');
297 return;
@@ -1237,6 +1242,7 @@ function lowerStatement(
1242 kind: 'Debugger',
1243 loc,
1244 },
1245 + effects: null,
1246 loc,
1247 });
1248 return;
@@ -1894,6 +1900,7 @@ function lowerExpression(
1900 place: leftValue,
1901 loc: exprLoc,
1902 },
1903 + effects: null,
1904 loc: exprLoc,
1905 });
1906 builder.terminateWithContinuation(
@@ -2829,6 +2836,7 @@ function lowerOptionalCallExpression(
2836 args,
2837 loc,
2838 },
2839 + effects: null,
2840 loc,
2841 });
2842 } else {
@@ -2842,6 +2850,7 @@ function lowerOptionalCallExpression(
2850 args,
2851 loc,
2852 },
2853 + effects: null,
2854 loc,
2855 });
2856 }
@@ -3465,9 +3474,10 @@ export function lowerValueToTemporary(
3474 const place: Place = buildTemporaryPlace(builder, value.loc);
3475 builder.push({
3476 id: makeInstructionId(0),
3477 + lvalue: {...place},
3478 value: value,
3479 + effects: null,
3480 loc: value.loc,
3470 - lvalue: {...place},
3481 });
3482 return place;
3483 }
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+5
@@ -243,6 +243,11 @@ export const EnvironmentConfigSchema = z.object({
243 */
244 enableUseTypeAnnotations: z.boolean().default(false),
245
246 + /**
247 + * Enable a new model for mutability and aliasing inference
248 + */
249 + enableNewMutationAliasingModel: z.boolean().default(false),
250 +
251 /**
252 * Enables inference of optional dependency chains. Without this flag
253 * a property chain such as `props?.items?.foo` will infer as a dep on
compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts
+37 -1
@@ -5,7 +5,7 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {Effect, ValueKind, ValueReason} from './HIR';
8 +import {Effect, makeIdentifierId, ValueKind, ValueReason} from './HIR';
9 import {
10 BUILTIN_SHAPES,
11 BuiltInArrayId,
@@ -34,6 +34,7 @@ import {
34 addFunction,
35 addHook,
36 addObject,
37 + signatureArgument,
38 } from './ObjectShape';
39 import {BuiltInType, ObjectType, PolyType} from './Types';
40 import {TypeConfig} from './TypeSchema';
@@ -644,6 +645,41 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
645 calleeEffect: Effect.Read,
646 hookKind: 'useEffect',
647 returnValueKind: ValueKind.Frozen,
648 + aliasing: {
649 + receiver: makeIdentifierId(0),
650 + params: [],
651 + rest: makeIdentifierId(1),
652 + returns: makeIdentifierId(2),
653 + temporaries: [signatureArgument(3)],
654 + effects: [
655 + // Freezes the function and deps
656 + {
657 + kind: 'Freeze',
658 + value: signatureArgument(1),
659 + reason: ValueReason.Effect,
660 + },
661 + // Internally creates an effect object that captures the function and deps
662 + {
663 + kind: 'Create',
664 + into: signatureArgument(3),
665 + value: ValueKind.Frozen,
666 + reason: ValueReason.KnownReturnSignature,
667 + },
668 + // The effect stores the function and dependencies
669 + {
670 + kind: 'Capture',
671 + from: signatureArgument(1),
672 + into: signatureArgument(3),
673 + },
674 + // Returns undefined
675 + {
676 + kind: 'Create',
677 + into: signatureArgument(2),
678 + value: ValueKind.Primitive,
679 + reason: ValueReason.KnownReturnSignature,
680 + },
681 + ],
682 + },
683 },
684 BuiltInUseEffectHookId,
685 ),
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+13
@@ -13,6 +13,7 @@ import {Environment, ReactFunctionType} from './Environment';
13 import type {HookKind} from './ObjectShape';
14 import {Type, makeType} from './Types';
15 import {z} from 'zod';
16 +import type {AliasingEffect} from '../Inference/AliasingEffects';
17
18 /*
19 * *******************************************************************************************
@@ -100,6 +101,7 @@ export type ReactiveInstruction = {
101 id: InstructionId;
102 lvalue: Place | null;
103 value: ReactiveValue;
104 + effects?: Array<AliasingEffect> | null; // TODO make non-optional
105 loc: SourceLocation;
106 };
107
@@ -278,12 +280,14 @@ export type HIRFunction = {
280 params: Array<Place | SpreadPattern>;
281 returnTypeAnnotation: t.FlowType | t.TSType | null;
282 returnType: Type;
283 + returns: Place;
284 context: Array<Place>;
285 effects: Array<FunctionEffect> | null;
286 body: HIR;
287 generator: boolean;
288 async: boolean;
289 directives: Array<string>;
290 + aliasingEffects?: Array<AliasingEffect> | null;
291 };
292
293 export type FunctionEffect =
@@ -449,6 +453,7 @@ export type ReturnTerminal = {
453 value: Place;
454 id: InstructionId;
455 fallthrough?: never;
456 + effects: Array<AliasingEffect> | null;
457 };
458
459 export type GotoTerminal = {
@@ -609,6 +614,7 @@ export type MaybeThrowTerminal = {
614 id: InstructionId;
615 loc: SourceLocation;
616 fallthrough?: never;
617 + effects: Array<AliasingEffect> | null;
618 };
619
620 export type ReactiveScopeTerminal = {
@@ -645,12 +651,14 @@ export type Instruction = {
651 lvalue: Place;
652 value: InstructionValue;
653 loc: SourceLocation;
654 + effects: Array<AliasingEffect> | null;
655 };
656
657 export type TInstruction<T extends InstructionValue> = {
658 id: InstructionId;
659 lvalue: Place;
660 value: T;
661 + effects: Array<AliasingEffect> | null;
662 loc: SourceLocation;
663 };
664
@@ -1380,6 +1388,11 @@ export enum ValueReason {
1388 */
1389 JsxCaptured = 'jsx-captured',
1390
1391 + /**
1392 + * Passed to an effect
1393 + */
1394 + Effect = 'effect',
1395 +
1396 /**
1397 * Return value of a function with known frozen return value, e.g. `useState`.
1398 */
compiler/packages/babel-plugin-react-compiler/src/HIR/HIRBuilder.ts
+1
@@ -165,6 +165,7 @@ export default class HIRBuilder {
165 handler: exceptionHandler,
166 id: makeInstructionId(0),
167 loc: instruction.loc,
168 + effects: null,
169 },
170 continuationBlock,
171 );
compiler/packages/babel-plugin-react-compiler/src/HIR/MergeConsecutiveBlocks.ts
+10 -7
@@ -12,6 +12,7 @@ import {
12 GeneratedSource,
13 HIRFunction,
14 Instruction,
15 + Place,
16 } from './HIR';
17 import {markPredecessors} from './HIRBuilder';
18 import {terminalFallthrough, terminalHasFallthrough} from './visitors';
@@ -80,20 +81,22 @@ export function mergeConsecutiveBlocks(fn: HIRFunction): void {
81 suggestions: null,
82 });
83 const operand = Array.from(phi.operands.values())[0]!;
84 + const lvalue: Place = {
85 + kind: 'Identifier',
86 + identifier: phi.place.identifier,
87 + effect: Effect.ConditionallyMutate,
88 + reactive: false,
89 + loc: GeneratedSource,
90 + };
91 const instr: Instruction = {
92 id: predecessor.terminal.id,
85 - lvalue: {
86 - kind: 'Identifier',
87 - identifier: phi.place.identifier,
88 - effect: Effect.ConditionallyMutate,
89 - reactive: false,
90 - loc: GeneratedSource,
91 - },
93 + lvalue: {...lvalue},
94 value: {
95 kind: 'LoadLocal',
96 place: {...operand},
97 loc: GeneratedSource,
98 },
99 + effects: [{kind: 'Alias', from: {...operand}, into: {...lvalue}}],
100 loc: GeneratedSource,
101 };
102 predecessor.instructions.push(instr);
compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts
+140 -1
@@ -6,10 +6,21 @@
6 */
7
8 import {CompilerError} from '../CompilerError';
9 -import {Effect, ValueKind, ValueReason} from './HIR';
9 +import {AliasingSignature} from '../Inference/AliasingEffects';
10 +import {
11 + Effect,
12 + GeneratedSource,
13 + makeDeclarationId,
14 + makeIdentifierId,
15 + makeInstructionId,
16 + Place,
17 + ValueKind,
18 + ValueReason,
19 +} from './HIR';
20 import {
21 BuiltInType,
22 FunctionType,
23 + makeType,
24 ObjectType,
25 PolyType,
26 PrimitiveType,
@@ -180,6 +191,9 @@ export type FunctionSignature = {
191 impure?: boolean;
192
193 canonicalName?: string;
194 +
195 + aliasing?: AliasingSignature | null;
196 + todo_aliasing?: AliasingSignature | null;
197 };
198
199 /*
@@ -305,6 +319,30 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
319 returnType: PRIMITIVE_TYPE,
320 calleeEffect: Effect.Store,
321 returnValueKind: ValueKind.Primitive,
322 + aliasing: {
323 + receiver: makeIdentifierId(0),
324 + params: [],
325 + rest: makeIdentifierId(1),
326 + returns: makeIdentifierId(2),
327 + temporaries: [],
328 + effects: [
329 + // Push directly mutates the array itself
330 + {kind: 'Mutate', value: signatureArgument(0)},
331 + // The arguments are captured into the array
332 + {
333 + kind: 'Capture',
334 + from: signatureArgument(1),
335 + into: signatureArgument(0),
336 + },
337 + // Returns the new length, a primitive
338 + {
339 + kind: 'Create',
340 + into: signatureArgument(2),
341 + value: ValueKind.Primitive,
342 + reason: ValueReason.KnownReturnSignature,
343 + },
344 + ],
345 + },
346 }),
347 ],
348 [
@@ -335,6 +373,62 @@ addObject(BUILTIN_SHAPES, BuiltInArrayId, [
373 returnValueKind: ValueKind.Mutable,
374 noAlias: true,
375 mutableOnlyIfOperandsAreMutable: true,
376 + aliasing: {
377 + receiver: makeIdentifierId(0),
378 + params: [makeIdentifierId(1)],
379 + rest: null,
380 + returns: makeIdentifierId(2),
381 + temporaries: [
382 + // Temporary representing captured items of the receiver
383 + signatureArgument(3),
384 + // Temporary representing the result of the callback
385 + signatureArgument(4),
386 + /*
387 + * Undefined `this` arg to the callback. Note the signature does not
388 + * support passing an explicit thisArg second param
389 + */
390 + signatureArgument(5),
391 + ],
392 + effects: [
393 + // Map creates a new mutable array
394 + {
395 + kind: 'Create',
396 + into: signatureArgument(2),
397 + value: ValueKind.Mutable,
398 + reason: ValueReason.KnownReturnSignature,
399 + },
400 + // The first arg to the callback is an item extracted from the receiver array
401 + {
402 + kind: 'CreateFrom',
403 + from: signatureArgument(0),
404 + into: signatureArgument(3),
405 + },
406 + // The undefined this for the callback
407 + {
408 + kind: 'Create',
409 + into: signatureArgument(5),
410 + value: ValueKind.Primitive,
411 + reason: ValueReason.KnownReturnSignature,
412 + },
413 + // calls the callback, returning the result into a temporary
414 + {
415 + kind: 'Apply',
416 + receiver: signatureArgument(5),
417 + args: [signatureArgument(3), {kind: 'Hole'}, signatureArgument(0)],
418 + function: signatureArgument(1),
419 + into: signatureArgument(4),
420 + signature: null,
421 + mutatesFunction: false,
422 + loc: GeneratedSource,
423 + },
424 + // captures the result of the callback into the return array
425 + {
426 + kind: 'Capture',
427 + from: signatureArgument(4),
428 + into: signatureArgument(2),
429 + },
430 + ],
431 + },
432 }),
433 ],
434 [
@@ -482,6 +576,32 @@ addObject(BUILTIN_SHAPES, BuiltInSetId, [
576 calleeEffect: Effect.Store,
577 // returnValueKind is technically dependent on the ValueKind of the set itself
578 returnValueKind: ValueKind.Mutable,
579 + aliasing: {
580 + receiver: makeIdentifierId(0),
581 + params: [],
582 + rest: makeIdentifierId(1),
583 + returns: makeIdentifierId(2),
584 + temporaries: [],
585 + effects: [
586 + // Set.add returns the receiver Set
587 + {
588 + kind: 'Assign',
589 + from: signatureArgument(0),
590 + into: signatureArgument(2),
591 + },
592 + // Set.add mutates the set itself
593 + {
594 + kind: 'Mutate',
595 + value: signatureArgument(0),
596 + },
597 + // Captures the rest params into the set
598 + {
599 + kind: 'Capture',
600 + from: signatureArgument(1),
601 + into: signatureArgument(0),
602 + },
603 + ],
604 + },
605 }),
606 ],
607 [
@@ -1185,3 +1305,22 @@ export const DefaultNonmutatingHook = addHook(
1305 },
1306 'DefaultNonmutatingHook',
1307 );
1308 +
1309 +export function signatureArgument(id: number): Place {
1310 + const place: Place = {
1311 + kind: 'Identifier',
1312 + effect: Effect.Unknown,
1313 + loc: GeneratedSource,
1314 + reactive: false,
1315 + identifier: {
1316 + declarationId: makeDeclarationId(id),
1317 + id: makeIdentifierId(id),
1318 + loc: GeneratedSource,
1319 + mutableRange: {start: makeInstructionId(0), end: makeInstructionId(0)},
1320 + name: null,
1321 + scope: null,
1322 + type: makeType(),
1323 + },
1324 + };
1325 + return place;
1326 +}
compiler/packages/babel-plugin-react-compiler/src/HIR/PrintHIR.ts
+124 -5
@@ -35,6 +35,7 @@ import type {
35 Type,
36 } from './HIR';
37 import {GotoVariant, InstructionKind} from './HIR';
38 +import {AliasingEffect, AliasingSignature} from '../Inference/AliasingEffects';
39
40 export type Options = {
41 indent: number;
@@ -67,13 +68,15 @@ export function printFunction(fn: HIRFunction): string {
68 })
69 .join(', ') +
70 ')';
71 + } else {
72 + definition += '()';
73 }
74 if (definition.length !== 0) {
75 output.push(definition);
76 }
74 - output.push(printType(fn.returnType));
75 - output.push(printHIR(fn.body));
77 + output.push(`: ${printType(fn.returnType)} @ ${printPlace(fn.returns)}`);
78 output.push(...fn.directives);
79 + output.push(printHIR(fn.body));
80 return output.join('\n');
81 }
82
@@ -151,7 +154,10 @@ export function printMixedHIR(
154
155 export function printInstruction(instr: ReactiveInstruction): string {
156 const id = `[${instr.id}]`;
154 - const value = printInstructionValue(instr.value);
157 + let value = printInstructionValue(instr.value);
158 + if (instr.effects != null) {
159 + value += `\n ${instr.effects.map(printAliasingEffect).join('\n ')}`;
160 + }
161
162 if (instr.lvalue !== null) {
163 return `${id} ${printPlace(instr.lvalue)} = ${value}`;
@@ -213,6 +219,9 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
219 value = `[${terminal.id}] Return${
220 terminal.value != null ? ' ' + printPlace(terminal.value) : ''
221 }`;
222 + if (terminal.effects != null) {
223 + value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
224 + }
225 break;
226 }
227 case 'goto': {
@@ -281,6 +290,9 @@ export function printTerminal(terminal: Terminal): Array<string> | string {
290 }
291 case 'maybe-throw': {
292 value = `[${terminal.id}] MaybeThrow continuation=bb${terminal.continuation} handler=bb${terminal.handler}`;
293 + if (terminal.effects != null) {
294 + value += `\n ${terminal.effects.map(printAliasingEffect).join('\n ')}`;
295 + }
296 break;
297 }
298 case 'scope': {
@@ -555,8 +567,11 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
567 }
568 })
569 .join(', ') ?? '';
558 - const type = printType(instrValue.loweredFunc.func.returnType).trim();
559 - value = `${kind} ${name} @context[${context}] @effects[${effects}]${type !== '' ? ` return${type}` : ''}:\n${fn}`;
570 + const aliasingEffects =
571 + instrValue.loweredFunc.func.aliasingEffects
572 + ?.map(printAliasingEffect)
573 + ?.join(', ') ?? '';
574 + value = `${kind} ${name} @context[${context}] @effects[${effects}] @aliasingEffects=[${aliasingEffects}]\n${fn}`;
575 break;
576 }
577 case 'TaggedTemplateExpression': {
@@ -922,3 +937,107 @@ function getFunctionName(
937 return defaultValue;
938 }
939 }
940 +
941 +export function printAliasingEffect(effect: AliasingEffect): string {
942 + switch (effect.kind) {
943 + case 'Assign': {
944 + return `Assign ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
945 + }
946 + case 'Alias': {
947 + return `Alias ${printPlaceForAliasEffect(effect.into)} = ${printPlaceForAliasEffect(effect.from)}`;
948 + }
949 + case 'Capture': {
950 + return `Capture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
951 + }
952 + case 'ImmutableCapture': {
953 + return `ImmutableCapture ${printPlaceForAliasEffect(effect.into)} <- ${printPlaceForAliasEffect(effect.from)}`;
954 + }
955 + case 'Create': {
956 + return `Create ${printPlaceForAliasEffect(effect.into)} = ${effect.value}`;
957 + }
958 + case 'CreateFrom': {
959 + return `Create ${printPlaceForAliasEffect(effect.into)} = kindOf(${printPlaceForAliasEffect(effect.from)})`;
960 + }
961 + case 'CreateFunction': {
962 + return `Function ${printPlaceForAliasEffect(effect.into)} = Function captures=[${effect.captures.map(printPlaceForAliasEffect).join(', ')}]`;
963 + }
964 + case 'Apply': {
965 + const receiverCallee =
966 + effect.receiver.identifier.id === effect.function.identifier.id
967 + ? printPlaceForAliasEffect(effect.receiver)
968 + : `${printPlaceForAliasEffect(effect.receiver)}.${printPlaceForAliasEffect(effect.function)}`;
969 + const args = effect.args
970 + .map(arg => {
971 + if (arg.kind === 'Identifier') {
972 + return printPlaceForAliasEffect(arg);
973 + } else if (arg.kind === 'Hole') {
974 + return ' ';
975 + }
976 + return `...${printPlaceForAliasEffect(arg.place)}`;
977 + })
978 + .join(', ');
979 + let signature = '';
980 + if (effect.signature != null) {
981 + if (effect.signature.aliasing != null) {
982 + signature = printAliasingSignature(effect.signature.aliasing);
983 + } else {
984 + signature = JSON.stringify(effect.signature, null, 2);
985 + }
986 + }
987 + return `Apply ${printPlaceForAliasEffect(effect.into)} = ${receiverCallee}(${args})${signature != '' ? '\n ' : ''}${signature}`;
988 + }
989 + case 'Freeze': {
990 + return `Freeze ${printPlaceForAliasEffect(effect.value)} ${effect.reason}`;
991 + }
992 + case 'Mutate':
993 + case 'MutateConditionally':
994 + case 'MutateTransitive':
995 + case 'MutateTransitiveConditionally': {
996 + return `${effect.kind} ${printPlaceForAliasEffect(effect.value)}`;
997 + }
998 + case 'MutateFrozen': {
999 + return `MutateFrozen ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
1000 + }
1001 + case 'MutateGlobal': {
1002 + return `MutateGlobal ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
1003 + }
1004 + case 'Impure': {
1005 + return `Impure ${printPlaceForAliasEffect(effect.place)} reason=${JSON.stringify(effect.error.reason)}`;
1006 + }
1007 + case 'Render': {
1008 + return `Render ${printPlaceForAliasEffect(effect.place)}`;
1009 + }
1010 + default: {
1011 + assertExhaustive(effect, `Unexpected kind '${(effect as any).kind}'`);
1012 + }
1013 + }
1014 +}
1015 +
1016 +function printPlaceForAliasEffect(place: Place): string {
1017 + return printIdentifier(place.identifier);
1018 +}
1019 +
1020 +export function printAliasingSignature(signature: AliasingSignature): string {
1021 + const tokens: Array<string> = ['function '];
1022 + if (signature.temporaries.length !== 0) {
1023 + tokens.push('<');
1024 + tokens.push(
1025 + signature.temporaries.map(temp => `$${temp.identifier.id}`).join(', '),
1026 + );
1027 + tokens.push('>');
1028 + }
1029 + tokens.push('(');
1030 + tokens.push('this=$' + String(signature.receiver));
1031 + for (const param of signature.params) {
1032 + tokens.push(', $' + String(param));
1033 + }
1034 + if (signature.rest != null) {
1035 + tokens.push(`, ...$${String(signature.rest)}`);
1036 + }
1037 + tokens.push('): ');
1038 + tokens.push('$' + String(signature.returns) + ':');
1039 + for (const effect of signature.effects) {
1040 + tokens.push('\n ' + printAliasingEffect(effect));
1041 + }
1042 + return tokens.join('');
1043 +}
compiler/packages/babel-plugin-react-compiler/src/HIR/ScopeDependencyUtils.ts
+2
@@ -88,6 +88,7 @@ function writeNonOptionalDependency(
88 },
89 id: makeInstructionId(1),
90 loc: loc,
91 + effects: null,
92 });
93
94 /**
@@ -118,6 +119,7 @@ function writeNonOptionalDependency(
119 },
120 id: makeInstructionId(1),
121 loc: loc,
122 + effects: null,
123 });
124 curr = next;
125 }
compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts
+2
@@ -735,6 +735,7 @@ export function mapTerminalSuccessors(
735 loc: terminal.loc,
736 value: terminal.value,
737 id: makeInstructionId(0),
738 + effects: terminal.effects,
739 };
740 }
741 case 'throw': {
@@ -842,6 +843,7 @@ export function mapTerminalSuccessors(
843 handler,
844 id: makeInstructionId(0),
845 loc: terminal.loc,
846 + effects: terminal.effects,
847 };
848 }
849 case 'try': {
compiler/packages/babel-plugin-react-compiler/src/Inference/AliasingEffects.ts new
+233
@@ -0,0 +1,233 @@
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 {CompilerErrorDetailOptions} from '../CompilerError';
9 +import {
10 + FunctionExpression,
11 + Hole,
12 + IdentifierId,
13 + ObjectMethod,
14 + Place,
15 + SourceLocation,
16 + SpreadPattern,
17 + ValueKind,
18 + ValueReason,
19 +} from '../HIR';
20 +import {FunctionSignature} from '../HIR/ObjectShape';
21 +
22 +/**
23 + * `AliasingEffect` describes a set of "effects" that an instruction/terminal has on one or
24 + * more values in a program. These effects include mutation of values, freezing values,
25 + * tracking data flow between values, and other specialized cases.
26 + */
27 +export type AliasingEffect =
28 + /**
29 + * Marks the given value and its direct aliases as frozen.
30 + *
31 + * Captured values are *not* considered frozen, because we cannot be sure that a previously
32 + * captured value will still be captured at the point of the freeze.
33 + *
34 + * For example:
35 + * const x = {};
36 + * const y = [x];
37 + * y.pop(); // y dosn't contain x anymore!
38 + * freeze(y);
39 + * mutate(x); // safe to mutate!
40 + *
41 + * The exception to this is FunctionExpressions - since it is impossible to change which
42 + * value a function closes over[1] we can transitively freeze functions and their captures.
43 + *
44 + * [1] Except for `let` values that are reassigned and closed over by a function, but we
45 + * handle this explicitly with StoreContext/LoadContext.
46 + */
47 + | {kind: 'Freeze'; value: Place; reason: ValueReason}
48 + /**
49 + * Mutate the value and any direct aliases (not captures). Errors if the value is not mutable.
50 + */
51 + | {kind: 'Mutate'; value: Place}
52 + /**
53 + * Mutate the value and any direct aliases (not captures), but only if the value is known mutable.
54 + * This should be rare.
55 + *
56 + * TODO: this is only used for IteratorNext, but even then MutateTransitiveConditionally is more
57 + * correct for iterators of unknown types.
58 + */
59 + | {kind: 'MutateConditionally'; value: Place}
60 + /**
61 + * Mutate the value, any direct aliases, and any transitive captures. Errors if the value is not mutable.
62 + */
63 + | {kind: 'MutateTransitive'; value: Place}
64 + /**
65 + * Mutates any of the value, its direct aliases, and its transitive captures that are mutable.
66 + */
67 + | {kind: 'MutateTransitiveConditionally'; value: Place}
68 + /**
69 + * Records information flow from `from` to `into` in cases where local mutation of the destination
70 + * will *not* mutate the source:
71 + *
72 + * - Capture a -> b and Mutate(b) X=> (does not imply) Mutate(a)
73 + * - Capture a -> b and MutateTransitive(b) => (does imply) Mutate(a)
74 + *
75 + * Example: `array.push(item)`. Information from item is captured into array, but there is not a
76 + * direct aliasing, and local mutations of array will not modify item.
77 + */
78 + | {kind: 'Capture'; from: Place; into: Place}
79 + /**
80 + * Records information flow from `from` to `into` in cases where local mutation of the destination
81 + * *will* mutate the source:
82 + *
83 + * - Alias a -> b and Mutate(b) => (does imply) Mutate(a)
84 + * - Alias a -> b and MutateTransitive(b) => (does imply) Mutate(a)
85 + *
86 + * Example: `c = identity(a)`. We don't know what `identity()` returns so we can't use Assign.
87 + * But we have to assume that it _could_ be returning its input, such that a local mutation of
88 + * c could be mutating a.
89 + */
90 + | {kind: 'Alias'; from: Place; into: Place}
91 + /**
92 + * Records direct assignment: `into = from`.
93 + */
94 + | {kind: 'Assign'; from: Place; into: Place}
95 + /**
96 + * Creates a value of the given type at the given place
97 + */
98 + | {kind: 'Create'; into: Place; value: ValueKind; reason: ValueReason}
99 + /**
100 + * Creates a new value with the same kind as the starting value.
101 + */
102 + | {kind: 'CreateFrom'; from: Place; into: Place}
103 + /**
104 + * Immutable data flow, used for escape analysis. Does not influence mutable range analysis:
105 + */
106 + | {kind: 'ImmutableCapture'; from: Place; into: Place}
107 + /**
108 + * Calls the function at the given place with the given arguments either captured or aliased,
109 + * and captures/aliases the result into the given place.
110 + */
111 + | {
112 + kind: 'Apply';
113 + receiver: Place;
114 + function: Place;
115 + mutatesFunction: boolean;
116 + args: Array<Place | SpreadPattern | Hole>;
117 + into: Place;
118 + signature: FunctionSignature | null;
119 + loc: SourceLocation;
120 + }
121 + /**
122 + * Constructs a function value with the given captures. The mutability of the function
123 + * will be determined by the mutability of the capture values when evaluated.
124 + */
125 + | {
126 + kind: 'CreateFunction';
127 + captures: Array<Place>;
128 + function: FunctionExpression | ObjectMethod;
129 + into: Place;
130 + }
131 + /**
132 + * Mutation of a value known to be immutable
133 + */
134 + | {kind: 'MutateFrozen'; place: Place; error: CompilerErrorDetailOptions}
135 + /**
136 + * Mutation of a global
137 + */
138 + | {
139 + kind: 'MutateGlobal';
140 + place: Place;
141 + error: CompilerErrorDetailOptions;
142 + }
143 + /**
144 + * Indicates a side-effect that is not safe during render
145 + */
146 + | {kind: 'Impure'; place: Place; error: CompilerErrorDetailOptions}
147 + /**
148 + * Indicates that a given place is accessed during render. Used to distingush
149 + * hook arguments that are known to be called immediately vs those used for
150 + * event handlers/effects, and for JSX values known to be called during render
151 + * (tags, children) vs those that may be events/effect (other props).
152 + */
153 + | {
154 + kind: 'Render';
155 + place: Place;
156 + };
157 +
158 +export function hashEffect(effect: AliasingEffect): string {
159 + switch (effect.kind) {
160 + case 'Apply': {
161 + return [
162 + effect.kind,
163 + effect.receiver.identifier.id,
164 + effect.function.identifier.id,
165 + effect.mutatesFunction,
166 + effect.args
167 + .map(a => {
168 + if (a.kind === 'Hole') {
169 + return '';
170 + } else if (a.kind === 'Identifier') {
171 + return a.identifier.id;
172 + } else {
173 + return `...${a.place.identifier.id}`;
174 + }
175 + })
176 + .join(','),
177 + effect.into.identifier.id,
178 + ].join(':');
179 + }
180 + case 'CreateFrom':
181 + case 'ImmutableCapture':
182 + case 'Assign':
183 + case 'Alias':
184 + case 'Capture': {
185 + return [
186 + effect.kind,
187 + effect.from.identifier.id,
188 + effect.into.identifier.id,
189 + ].join(':');
190 + }
191 + case 'Create': {
192 + return [
193 + effect.kind,
194 + effect.into.identifier.id,
195 + effect.value,
196 + effect.reason,
197 + ].join(':');
198 + }
199 + case 'Freeze': {
200 + return [effect.kind, effect.value.identifier.id, effect.reason].join(':');
201 + }
202 + case 'Impure':
203 + case 'Render':
204 + case 'MutateFrozen':
205 + case 'MutateGlobal': {
206 + return [effect.kind, effect.place.identifier.id].join(':');
207 + }
208 + case 'Mutate':
209 + case 'MutateConditionally':
210 + case 'MutateTransitive':
211 + case 'MutateTransitiveConditionally': {
212 + return [effect.kind, effect.value.identifier.id].join(':');
213 + }
214 + case 'CreateFunction': {
215 + return [
216 + effect.kind,
217 + effect.into.identifier.id,
218 + // return places are a unique way to identify functions themselves
219 + effect.function.loweredFunc.func.returns.identifier.id,
220 + effect.captures.map(p => p.identifier.id).join(','),
221 + ].join(':');
222 + }
223 + }
224 +}
225 +
226 +export type AliasingSignature = {
227 + receiver: IdentifierId;
228 + params: Array<IdentifierId>;
229 + rest: IdentifierId | null;
230 + returns: IdentifierId;
231 + effects: Array<AliasingEffect>;
232 + temporaries: Array<Place>;
233 +};
compiler/packages/babel-plugin-react-compiler/src/Inference/AnalyseFunctions.ts
+92 -2
@@ -10,6 +10,7 @@ import {
10 Effect,
11 HIRFunction,
12 Identifier,
13 + IdentifierId,
14 LoweredFunction,
15 isRefOrRefValue,
16 makeInstructionId,
@@ -19,6 +20,10 @@ import {inferReactiveScopeVariables} from '../ReactiveScopes';
20 import {rewriteInstructionKindsBasedOnReassignment} from '../SSA';
21 import {inferMutableRanges} from './InferMutableRanges';
22 import inferReferenceEffects from './InferReferenceEffects';
23 +import {assertExhaustive} from '../Utils/utils';
24 +import {inferMutationAliasingEffects} from './InferMutationAliasingEffects';
25 +import {inferMutationAliasingFunctionEffects} from './InferMutationAliasingFunctionEffects';
26 +import {inferMutationAliasingRanges} from './InferMutationAliasingRanges';
27
28 export default function analyseFunctions(func: HIRFunction): void {
29 for (const [_, block] of func.body.blocks) {
@@ -26,8 +31,12 @@ export default function analyseFunctions(func: HIRFunction): void {
31 switch (instr.value.kind) {
32 case 'ObjectMethod':
33 case 'FunctionExpression': {
29 - lower(instr.value.loweredFunc.func);
30 - infer(instr.value.loweredFunc);
34 + if (!func.env.config.enableNewMutationAliasingModel) {
35 + lower(instr.value.loweredFunc.func);
36 + infer(instr.value.loweredFunc);
37 + } else {
38 + lowerWithMutationAliasing(instr.value.loweredFunc.func);
39 + }
40
41 /**
42 * Reset mutable range for outer inferReferenceEffects
@@ -44,6 +53,87 @@ export default function analyseFunctions(func: HIRFunction): void {
53 }
54 }
55
56 +function lowerWithMutationAliasing(fn: HIRFunction): void {
57 + /**
58 + * Phase 1: similar to lower(), but using the new mutation/aliasing inference
59 + */
60 + analyseFunctions(fn);
61 + inferMutationAliasingEffects(fn, {isFunctionExpression: true});
62 + deadCodeElimination(fn);
63 + inferMutationAliasingRanges(fn, {isFunctionExpression: true});
64 + rewriteInstructionKindsBasedOnReassignment(fn);
65 + inferReactiveScopeVariables(fn);
66 + const effects = inferMutationAliasingFunctionEffects(fn);
67 + fn.env.logger?.debugLogIRs?.({
68 + kind: 'hir',
69 + name: 'AnalyseFunction (inner)',
70 + value: fn,
71 + });
72 + if (effects != null) {
73 + fn.aliasingEffects ??= [];
74 + fn.aliasingEffects?.push(...effects);
75 + }
76 +
77 + /**
78 + * Phase 2: populate the Effect of each context variable to use in inferring
79 + * the outer function. For example, InferMutationAliasingEffects uses context variable
80 + * effects to decide if the function may be mutable or not.
81 + */
82 + const capturedOrMutated = new Set<IdentifierId>();
83 + for (const effect of effects ?? []) {
84 + switch (effect.kind) {
85 + case 'Assign':
86 + case 'Alias':
87 + case 'Capture':
88 + case 'CreateFrom': {
89 + capturedOrMutated.add(effect.from.identifier.id);
90 + break;
91 + }
92 + case 'Apply': {
93 + CompilerError.invariant(false, {
94 + reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
95 + loc: effect.function.loc,
96 + });
97 + }
98 + case 'Mutate':
99 + case 'MutateConditionally':
100 + case 'MutateTransitive':
101 + case 'MutateTransitiveConditionally': {
102 + capturedOrMutated.add(effect.value.identifier.id);
103 + break;
104 + }
105 + case 'Impure':
106 + case 'Render':
107 + case 'MutateFrozen':
108 + case 'MutateGlobal':
109 + case 'CreateFunction':
110 + case 'Create':
111 + case 'Freeze':
112 + case 'ImmutableCapture': {
113 + // no-op
114 + break;
115 + }
116 + default: {
117 + assertExhaustive(
118 + effect,
119 + `Unexpected effect kind ${(effect as any).kind}`,
120 + );
121 + }
122 + }
123 + }
124 +
125 + for (const operand of fn.context) {
126 + if (
127 + capturedOrMutated.has(operand.identifier.id) ||
128 + operand.effect === Effect.Capture
129 + ) {
130 + operand.effect = Effect.Capture;
131 + } else {
132 + operand.effect = Effect.Read;
133 + }
134 + }
135 +}
136 +
137 function lower(func: HIRFunction): void {
138 analyseFunctions(func);
139 inferReferenceEffects(func, {isFunctionExpression: true});
compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts
+2
@@ -197,6 +197,7 @@ function makeManualMemoizationMarkers(
197 deps: depsList,
198 loc: fnExpr.loc,
199 },
200 + effects: null,
201 loc: fnExpr.loc,
202 },
203 {
@@ -208,6 +209,7 @@ function makeManualMemoizationMarkers(
209 decl: {...memoDecl},
210 loc: fnExpr.loc,
211 },
212 + effects: null,
213 loc: fnExpr.loc,
214 },
215 ];
compiler/packages/babel-plugin-react-compiler/src/Inference/InferEffectDependencies.ts
+2
@@ -257,6 +257,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
257 loc: GeneratedSource,
258 lvalue: {...depsPlace, effect: Effect.Mutate},
259 value: deps,
260 + effects: null,
261 },
262 });
263 value.args.push({...depsPlace, effect: Effect.Freeze});
@@ -271,6 +272,7 @@ export function inferEffectDependencies(fn: HIRFunction): void {
272 loc: GeneratedSource,
273 lvalue: {...depsPlace, effect: Effect.Mutate},
274 value: deps,
275 + effects: null,
276 },
277 });
278 value.args.push({...depsPlace, effect: Effect.Freeze});
compiler/packages/babel-plugin-react-compiler/src/Inference/InferFunctionEffects.ts
+3 -1
@@ -324,7 +324,7 @@ function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
324 return effect.kind === 'GlobalMutation';
325 }
326
327 -function getWriteErrorReason(abstractValue: AbstractValue): string {
327 +export function getWriteErrorReason(abstractValue: AbstractValue): string {
328 if (abstractValue.reason.has(ValueReason.Global)) {
329 return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect';
330 } else if (abstractValue.reason.has(ValueReason.JsxCaptured)) {
@@ -339,6 +339,8 @@ function getWriteErrorReason(abstractValue: AbstractValue): string {
339 return "Mutating a value returned from 'useState()', which should not be mutated. Use the setter function to update instead";
340 } else if (abstractValue.reason.has(ValueReason.ReducerState)) {
341 return "Mutating a value returned from 'useReducer()', which should not be mutated. Use the dispatch function to update instead";
342 + } else if (abstractValue.reason.has(ValueReason.Effect)) {
343 + return 'Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect()';
344 } else {
345 return 'This mutates a variable that React considers immutable';
346 }
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableRanges.ts
+1 -1
@@ -86,7 +86,7 @@ export function inferMutableRanges(ir: HIRFunction): void {
86 }
87 }
88
89 -function areEqualMaps<T>(a: Map<T, T>, b: Map<T, T>): boolean {
89 +function areEqualMaps<T, U>(a: Map<T, U>, b: Map<T, U>): boolean {
90 if (a.size !== b.size) {
91 return false;
92 }
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts new
+2378
@@ -0,0 +1,2378 @@
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 + CompilerError,
10 + Effect,
11 + ErrorSeverity,
12 + SourceLocation,
13 + ValueKind,
14 +} from '..';
15 +import {
16 + BasicBlock,
17 + BlockId,
18 + DeclarationId,
19 + Environment,
20 + FunctionExpression,
21 + HIRFunction,
22 + Hole,
23 + IdentifierId,
24 + Instruction,
25 + InstructionKind,
26 + InstructionValue,
27 + isArrayType,
28 + isMapType,
29 + isPrimitiveType,
30 + isRefOrRefValue,
31 + isSetType,
32 + makeIdentifierId,
33 + Phi,
34 + Place,
35 + SpreadPattern,
36 + ValueReason,
37 +} from '../HIR';
38 +import {
39 + eachInstructionValueLValue,
40 + eachInstructionValueOperand,
41 + eachTerminalSuccessor,
42 +} from '../HIR/visitors';
43 +import {Ok, Result} from '../Utils/Result';
44 +import {
45 + getArgumentEffect,
46 + getFunctionCallSignature,
47 + isKnownMutableEffect,
48 + mergeValueKinds,
49 +} from './InferReferenceEffects';
50 +import {
51 + assertExhaustive,
52 + getOrInsertWith,
53 + Set_isSuperset,
54 +} from '../Utils/utils';
55 +import {
56 + printAliasingEffect,
57 + printAliasingSignature,
58 + printIdentifier,
59 + printInstruction,
60 + printInstructionValue,
61 + printPlace,
62 + printSourceLocation,
63 +} from '../HIR/PrintHIR';
64 +import {FunctionSignature} from '../HIR/ObjectShape';
65 +import {getWriteErrorReason} from './InferFunctionEffects';
66 +import prettyFormat from 'pretty-format';
67 +import {createTemporaryPlace} from '../HIR/HIRBuilder';
68 +import {AliasingEffect, AliasingSignature, hashEffect} from './AliasingEffects';
69 +
70 +const DEBUG = false;
71 +
72 +/**
73 + * Infers the mutation/aliasing effects for instructions and terminals and annotates
74 + * them on the HIR, making the effects of builtin instructions/functions as well as
75 + * user-defined functions explicit. These effects then form the basis for subsequent
76 + * analysis to determine the mutable range of each value in the program — the set of
77 + * instructions over which the value is created and mutated — as well as validation
78 + * against invalid code.
79 + *
80 + * At a high level the approach is:
81 + * - Determine a set of candidate effects based purely on the syntax of the instruction
82 + * and the types involved. These candidate effects are cached the first time each
83 + * instruction is visited. The idea is to reason about the semantics of the instruction
84 + * or function in isolation, separately from how those effects may interact with later
85 + * abstract interpretation.
86 + * - Then we do abstract interpretation over the HIR, iterating until reaching a fixpoint.
87 + * This phase tracks the abstract kind of each value (mutable, primitive, frozen, etc)
88 + * and the set of values pointed to by each identifier. Each candidate effect is "applied"
89 + * to the current abtract state, and effects may be dropped or rewritten accordingly.
90 + * For example, a "MutateConditionally <x>" effect may be dropped if x is not a mutable
91 + * value. A "Mutate <y>" effect may get converted into a "MutateFrozen <error>" effect
92 + * if y is mutable, etc.
93 + */
94 +export function inferMutationAliasingEffects(
95 + fn: HIRFunction,
96 + {isFunctionExpression}: {isFunctionExpression: boolean} = {
97 + isFunctionExpression: false,
98 + },
99 +): Result<void, CompilerError> {
100 + const initialState = InferenceState.empty(fn.env, isFunctionExpression);
101 +
102 + // Map of blocks to the last (merged) incoming state that was processed
103 + const statesByBlock: Map<BlockId, InferenceState> = new Map();
104 +
105 + for (const ref of fn.context) {
106 + // TODO: using InstructionValue as a bit of a hack, but it's pragmatic
107 + const value: InstructionValue = {
108 + kind: 'ObjectExpression',
109 + properties: [],
110 + loc: ref.loc,
111 + };
112 + initialState.initialize(value, {
113 + kind: ValueKind.Context,
114 + reason: new Set([ValueReason.Other]),
115 + });
116 + initialState.define(ref, value);
117 + }
118 +
119 + const paramKind: AbstractValue = isFunctionExpression
120 + ? {
121 + kind: ValueKind.Mutable,
122 + reason: new Set([ValueReason.Other]),
123 + }
124 + : {
125 + kind: ValueKind.Frozen,
126 + reason: new Set([ValueReason.ReactiveFunctionArgument]),
127 + };
128 +
129 + if (fn.fnType === 'Component') {
130 + CompilerError.invariant(fn.params.length <= 2, {
131 + reason:
132 + 'Expected React component to have not more than two parameters: one for props and for ref',
133 + description: null,
134 + loc: fn.loc,
135 + suggestions: null,
136 + });
137 + const [props, ref] = fn.params;
138 + if (props != null) {
139 + inferParam(props, initialState, paramKind);
140 + }
141 + if (ref != null) {
142 + const place = ref.kind === 'Identifier' ? ref : ref.place;
143 + const value: InstructionValue = {
144 + kind: 'ObjectExpression',
145 + properties: [],
146 + loc: place.loc,
147 + };
148 + initialState.initialize(value, {
149 + kind: ValueKind.Mutable,
150 + reason: new Set([ValueReason.Other]),
151 + });
152 + initialState.define(place, value);
153 + }
154 + } else {
155 + for (const param of fn.params) {
156 + inferParam(param, initialState, paramKind);
157 + }
158 + }
159 +
160 + /*
161 + * Multiple predecessors may be visited prior to reaching a given successor,
162 + * so track the list of incoming state for each successor block.
163 + * These are merged when reaching that block again.
164 + */
165 + const queuedStates: Map<BlockId, InferenceState> = new Map();
166 + function queue(blockId: BlockId, state: InferenceState): void {
167 + let queuedState = queuedStates.get(blockId);
168 + if (queuedState != null) {
169 + // merge the queued states for this block
170 + state = queuedState.merge(state) ?? queuedState;
171 + queuedStates.set(blockId, state);
172 + } else {
173 + /*
174 + * this is the first queued state for this block, see whether
175 + * there are changed relative to the last time it was processed.
176 + */
177 + const prevState = statesByBlock.get(blockId);
178 + const nextState = prevState != null ? prevState.merge(state) : state;
179 + if (nextState != null) {
180 + queuedStates.set(blockId, nextState);
181 + }
182 + }
183 + }
184 + queue(fn.body.entry, initialState);
185 +
186 + const hoistedContextDeclarations = findHoistedContextDeclarations(fn);
187 +
188 + const context = new Context(
189 + isFunctionExpression,
190 + fn,
191 + hoistedContextDeclarations,
192 + );
193 +
194 + let count = 0;
195 + while (queuedStates.size !== 0) {
196 + count++;
197 + if (count > 1000) {
198 + console.log(
199 + 'oops infinite loop',
200 + fn.id,
201 + typeof fn.loc !== 'symbol' ? fn.loc?.filename : null,
202 + );
203 + throw new Error('infinite loop');
204 + }
205 + for (const [blockId, block] of fn.body.blocks) {
206 + const incomingState = queuedStates.get(blockId);
207 + queuedStates.delete(blockId);
208 + if (incomingState == null) {
209 + continue;
210 + }
211 +
212 + statesByBlock.set(blockId, incomingState);
213 + const state = incomingState.clone();
214 + inferBlock(context, state, block);
215 +
216 + for (const nextBlockId of eachTerminalSuccessor(block.terminal)) {
217 + queue(nextBlockId, state);
218 + }
219 + }
220 + }
221 + return Ok(undefined);
222 +}
223 +
224 +function findHoistedContextDeclarations(fn: HIRFunction): Set<DeclarationId> {
225 + const hoisted = new Set<DeclarationId>();
226 + for (const block of fn.body.blocks.values()) {
227 + for (const instr of block.instructions) {
228 + if (instr.value.kind === 'DeclareContext') {
229 + const kind = instr.value.lvalue.kind;
230 + if (
231 + kind == InstructionKind.HoistedConst ||
232 + kind == InstructionKind.HoistedFunction ||
233 + kind == InstructionKind.HoistedLet
234 + ) {
235 + hoisted.add(instr.value.lvalue.place.identifier.declarationId);
236 + }
237 + }
238 + }
239 + }
240 + return hoisted;
241 +}
242 +
243 +class Context {
244 + internedEffects: Map<string, AliasingEffect> = new Map();
245 + instructionSignatureCache: Map<Instruction, InstructionSignature> = new Map();
246 + effectInstructionValueCache: Map<AliasingEffect, InstructionValue> =
247 + new Map();
248 + catchHandlers: Map<BlockId, Place> = new Map();
249 + isFuctionExpression: boolean;
250 + fn: HIRFunction;
251 + hoistedContextDeclarations: Set<DeclarationId>;
252 +
253 + constructor(
254 + isFunctionExpression: boolean,
255 + fn: HIRFunction,
256 + hoistedContextDeclarations: Set<DeclarationId>,
257 + ) {
258 + this.isFuctionExpression = isFunctionExpression;
259 + this.fn = fn;
260 + this.hoistedContextDeclarations = hoistedContextDeclarations;
261 + }
262 +
263 + internEffect(effect: AliasingEffect): AliasingEffect {
264 + const hash = hashEffect(effect);
265 + let interned = this.internedEffects.get(hash);
266 + if (interned == null) {
267 + this.internedEffects.set(hash, effect);
268 + interned = effect;
269 + }
270 + return interned;
271 + }
272 +}
273 +
274 +function inferParam(
275 + param: Place | SpreadPattern,
276 + initialState: InferenceState,
277 + paramKind: AbstractValue,
278 +): void {
279 + const place = param.kind === 'Identifier' ? param : param.place;
280 + const value: InstructionValue = {
281 + kind: 'Primitive',
282 + loc: place.loc,
283 + value: undefined,
284 + };
285 + initialState.initialize(value, paramKind);
286 + initialState.define(place, value);
287 +}
288 +
289 +function inferBlock(
290 + context: Context,
291 + state: InferenceState,
292 + block: BasicBlock,
293 +): void {
294 + for (const phi of block.phis) {
295 + state.inferPhi(phi);
296 + }
297 +
298 + for (const instr of block.instructions) {
299 + let instructionSignature = context.instructionSignatureCache.get(instr);
300 + if (instructionSignature == null) {
301 + instructionSignature = computeSignatureForInstruction(
302 + context,
303 + state.env,
304 + instr,
305 + );
306 + context.instructionSignatureCache.set(instr, instructionSignature);
307 + }
308 + const effects = applySignature(context, state, instructionSignature, instr);
309 + instr.effects = effects;
310 + }
311 + const terminal = block.terminal;
312 + if (terminal.kind === 'try' && terminal.handlerBinding != null) {
313 + context.catchHandlers.set(terminal.handler, terminal.handlerBinding);
314 + } else if (terminal.kind === 'maybe-throw') {
315 + const handlerParam = context.catchHandlers.get(terminal.handler);
316 + if (handlerParam != null) {
317 + const effects: Array<AliasingEffect> = [];
318 + for (const instr of block.instructions) {
319 + if (
320 + instr.value.kind === 'CallExpression' ||
321 + instr.value.kind === 'MethodCall'
322 + ) {
323 + /**
324 + * Many instructions can error, but only calls can throw their result as the error
325 + * itself. For example, `c = a.b` can throw if `a` is nullish, but the thrown value
326 + * is an error object synthesized by the JS runtime. Whereas `throwsInput(x)` can
327 + * throw (effectively) the result of the call.
328 + *
329 + * TODO: call applyEffect() instead. This meant that the catch param wasn't inferred
330 + * as a mutable value, though. See `try-catch-try-value-modified-in-catch-escaping.js`
331 + * fixture as an example
332 + */
333 + state.appendAlias(handlerParam, instr.lvalue);
334 + const kind = state.kind(instr.lvalue).kind;
335 + if (kind === ValueKind.Mutable || kind == ValueKind.Context) {
336 + effects.push({
337 + kind: 'Alias',
338 + from: instr.lvalue,
339 + into: handlerParam,
340 + });
341 + }
342 + }
343 + }
344 + terminal.effects = effects.length !== 0 ? effects : null;
345 + }
346 + } else if (terminal.kind === 'return') {
347 + if (!context.isFuctionExpression) {
348 + terminal.effects = [
349 + {
350 + kind: 'Freeze',
351 + value: terminal.value,
352 + reason: ValueReason.JsxCaptured,
353 + },
354 + ];
355 + }
356 + }
357 +}
358 +
359 +/**
360 + * Applies the signature to the given state to determine the precise set of effects
361 + * that will occur in practice. This takes into account the inferred state of each
362 + * variable. For example, the signature may have a `ConditionallyMutate x` effect.
363 + * Here, we check the abstract type of `x` and either record a `Mutate x` if x is mutable
364 + * or no effect if x is a primitive, global, or frozen.
365 + *
366 + * This phase may also emit errors, for example MutateLocal on a frozen value is invalid.
367 + */
368 +function applySignature(
369 + context: Context,
370 + state: InferenceState,
371 + signature: InstructionSignature,
372 + instruction: Instruction,
373 +): Array<AliasingEffect> | null {
374 + const effects: Array<AliasingEffect> = [];
375 + /**
376 + * For function instructions, eagerly validate that they aren't mutating
377 + * a known-frozen value.
378 + *
379 + * TODO: make sure we're also validating against global mutations somewhere, but
380 + * account for this being allowed in effects/event handlers.
381 + */
382 + if (
383 + instruction.value.kind === 'FunctionExpression' ||
384 + instruction.value.kind === 'ObjectMethod'
385 + ) {
386 + const aliasingEffects =
387 + instruction.value.loweredFunc.func.aliasingEffects ?? [];
388 + const context = new Set(
389 + instruction.value.loweredFunc.func.context.map(p => p.identifier.id),
390 + );
391 + for (const effect of aliasingEffects) {
392 + if (effect.kind === 'Mutate' || effect.kind === 'MutateTransitive') {
393 + if (!context.has(effect.value.identifier.id)) {
394 + continue;
395 + }
396 + const value = state.kind(effect.value);
397 + switch (value.kind) {
398 + case ValueKind.Frozen: {
399 + const reason = getWriteErrorReason({
400 + kind: value.kind,
401 + reason: value.reason,
402 + context: new Set(),
403 + });
404 + effects.push({
405 + kind: 'MutateFrozen',
406 + place: effect.value,
407 + error: {
408 + severity: ErrorSeverity.InvalidReact,
409 + reason,
410 + description:
411 + effect.value.identifier.name !== null &&
412 + effect.value.identifier.name.kind === 'named'
413 + ? `Found mutation of \`${effect.value.identifier.name.value}\``
414 + : null,
415 + loc: effect.value.loc,
416 + suggestions: null,
417 + },
418 + });
419 + }
420 + }
421 + }
422 + }
423 + }
424 +
425 + /*
426 + * Track which values we've already aliased once, so that we can switch to
427 + * appendAlias() for subsequent aliases into the same value
428 + */
429 + const aliased = new Set<IdentifierId>();
430 +
431 + if (DEBUG) {
432 + console.log(printInstruction(instruction));
433 + }
434 +
435 + for (const effect of signature.effects) {
436 + applyEffect(context, state, effect, aliased, effects);
437 + }
438 + if (DEBUG) {
439 + console.log(
440 + prettyFormat(state.debugAbstractValue(state.kind(instruction.lvalue))),
441 + );
442 + console.log(
443 + effects.map(effect => ` ${printAliasingEffect(effect)}`).join('\n'),
444 + );
445 + }
446 + if (
447 + !(state.isDefined(instruction.lvalue) && state.kind(instruction.lvalue))
448 + ) {
449 + CompilerError.invariant(false, {
450 + reason: `Expected instruction lvalue to be initialized`,
451 + loc: instruction.loc,
452 + });
453 + }
454 + return effects.length !== 0 ? effects : null;
455 +}
456 +
457 +function applyEffect(
458 + context: Context,
459 + state: InferenceState,
460 + _effect: AliasingEffect,
461 + aliased: Set<IdentifierId>,
462 + effects: Array<AliasingEffect>,
463 +): void {
464 + const effect = context.internEffect(_effect);
465 + if (DEBUG) {
466 + console.log(printAliasingEffect(effect));
467 + }
468 + switch (effect.kind) {
469 + case 'Freeze': {
470 + const didFreeze = state.freeze(effect.value, effect.reason);
471 + if (didFreeze) {
472 + effects.push(effect);
473 + }
474 + break;
475 + }
476 + case 'Create': {
477 + let value = context.effectInstructionValueCache.get(effect);
478 + if (value == null) {
479 + value = {
480 + kind: 'ObjectExpression',
481 + properties: [],
482 + loc: effect.into.loc,
483 + };
484 + context.effectInstructionValueCache.set(effect, value);
485 + }
486 + state.initialize(value, {
487 + kind: effect.value,
488 + reason: new Set([effect.reason]),
489 + });
490 + state.define(effect.into, value);
491 + break;
492 + }
493 + case 'ImmutableCapture': {
494 + const kind = state.kind(effect.from).kind;
495 + switch (kind) {
496 + case ValueKind.Global:
497 + case ValueKind.Primitive: {
498 + // no-op: we don't need to track data flow for copy types
499 + break;
500 + }
501 + default: {
502 + effects.push(effect);
503 + }
504 + }
505 + break;
506 + }
507 + case 'CreateFrom': {
508 + const fromValue = state.kind(effect.from);
509 + let value = context.effectInstructionValueCache.get(effect);
510 + if (value == null) {
511 + value = {
512 + kind: 'ObjectExpression',
513 + properties: [],
514 + loc: effect.into.loc,
515 + };
516 + context.effectInstructionValueCache.set(effect, value);
517 + }
518 + state.initialize(value, {
519 + kind: fromValue.kind,
520 + reason: new Set(fromValue.reason),
521 + });
522 + state.define(effect.into, value);
523 + switch (fromValue.kind) {
524 + case ValueKind.Primitive:
525 + case ValueKind.Global: {
526 + // no need to track this data flow
527 + break;
528 + }
529 + case ValueKind.Frozen: {
530 + effects.push({
531 + kind: 'ImmutableCapture',
532 + from: effect.from,
533 + into: effect.into,
534 + });
535 + break;
536 + }
537 + default: {
538 + effects.push({
539 + // OK: recording information flow
540 + kind: 'CreateFrom', // prev Alias
541 + from: effect.from,
542 + into: effect.into,
543 + });
544 + }
545 + }
546 + break;
547 + }
548 + case 'CreateFunction': {
549 + effects.push(effect);
550 + /**
551 + * We consider the function mutable if it has any mutable context variables or
552 + * any side-effects that need to be tracked if the function is called.
553 + */
554 + const hasCaptures = effect.captures.some(capture => {
555 + switch (state.kind(capture).kind) {
556 + case ValueKind.Context:
557 + case ValueKind.Mutable: {
558 + return true;
559 + }
560 + default: {
561 + return false;
562 + }
563 + }
564 + });
565 + const hasTrackedSideEffects =
566 + effect.function.loweredFunc.func.aliasingEffects?.some(
567 + effect =>
568 + // TODO; include "render" here?
569 + effect.kind === 'MutateFrozen' ||
570 + effect.kind === 'MutateGlobal' ||
571 + effect.kind === 'Impure',
572 + );
573 + // For legacy compatibility
574 + const capturesRef = effect.function.loweredFunc.func.context.some(
575 + operand => isRefOrRefValue(operand.identifier),
576 + );
577 + const isMutable = hasCaptures || hasTrackedSideEffects || capturesRef;
578 + for (const operand of effect.function.loweredFunc.func.context) {
579 + if (operand.effect !== Effect.Capture) {
580 + continue;
581 + }
582 + const kind = state.kind(operand).kind;
583 + if (
584 + kind === ValueKind.Primitive ||
585 + kind == ValueKind.Frozen ||
586 + kind == ValueKind.Global
587 + ) {
588 + operand.effect = Effect.Read;
589 + }
590 + }
591 + state.initialize(effect.function, {
592 + kind: isMutable ? ValueKind.Mutable : ValueKind.Frozen,
593 + reason: new Set([]),
594 + });
595 + state.define(effect.into, effect.function);
596 + for (const capture of effect.captures) {
597 + applyEffect(
598 + context,
599 + state,
600 + {
601 + kind: 'Capture',
602 + from: capture,
603 + into: effect.into,
604 + },
605 + aliased,
606 + effects,
607 + );
608 + }
609 + break;
610 + }
611 + case 'Alias':
612 + case 'Capture': {
613 + /*
614 + * Capture describes potential information flow: storing a pointer to one value
615 + * within another. If the destination is not mutable, or the source value has
616 + * copy-on-write semantics, then we can prune the effect
617 + */
618 + const intoKind = state.kind(effect.into).kind;
619 + let isMutableDesination: boolean;
620 + switch (intoKind) {
621 + case ValueKind.Context:
622 + case ValueKind.Mutable:
623 + case ValueKind.MaybeFrozen: {
624 + isMutableDesination = true;
625 + break;
626 + }
627 + default: {
628 + isMutableDesination = false;
629 + break;
630 + }
631 + }
632 + const fromKind = state.kind(effect.from).kind;
633 + let isMutableReferenceType: boolean;
634 + switch (fromKind) {
635 + case ValueKind.Global:
636 + case ValueKind.Primitive: {
637 + isMutableReferenceType = false;
638 + break;
639 + }
640 + case ValueKind.Frozen: {
641 + isMutableReferenceType = false;
642 + effects.push({
643 + kind: 'ImmutableCapture',
644 + from: effect.from,
645 + into: effect.into,
646 + });
647 + break;
648 + }
649 + default: {
650 + isMutableReferenceType = true;
651 + break;
652 + }
653 + }
654 + if (isMutableDesination && isMutableReferenceType) {
655 + effects.push(effect);
656 + }
657 + break;
658 + }
659 + case 'Assign': {
660 + /*
661 + * Alias represents potential pointer aliasing. If the type is a global,
662 + * a primitive (copy-on-write semantics) then we can prune the effect
663 + */
664 + const fromValue = state.kind(effect.from);
665 + const fromKind = fromValue.kind;
666 + switch (fromKind) {
667 + case ValueKind.Frozen: {
668 + effects.push({
669 + kind: 'ImmutableCapture',
670 + from: effect.from,
671 + into: effect.into,
672 + });
673 + let value = context.effectInstructionValueCache.get(effect);
674 + if (value == null) {
675 + value = {
676 + kind: 'Primitive',
677 + value: undefined,
678 + loc: effect.from.loc,
679 + };
680 + context.effectInstructionValueCache.set(effect, value);
681 + }
682 + state.initialize(value, {
683 + kind: fromKind,
684 + reason: new Set(fromValue.reason),
685 + });
686 + state.define(effect.into, value);
687 + break;
688 + }
689 + case ValueKind.Global:
690 + case ValueKind.Primitive: {
691 + let value = context.effectInstructionValueCache.get(effect);
692 + if (value == null) {
693 + value = {
694 + kind: 'Primitive',
695 + value: undefined,
696 + loc: effect.from.loc,
697 + };
698 + context.effectInstructionValueCache.set(effect, value);
699 + }
700 + state.initialize(value, {
701 + kind: fromKind,
702 + reason: new Set(fromValue.reason),
703 + });
704 + state.define(effect.into, value);
705 + break;
706 + }
707 + default: {
708 + if (aliased.has(effect.into.identifier.id)) {
709 + state.appendAlias(effect.into, effect.from);
710 + } else {
711 + aliased.add(effect.into.identifier.id);
712 + state.alias(effect.into, effect.from);
713 + }
714 + effects.push(effect);
715 + break;
716 + }
717 + }
718 + break;
719 + }
720 + case 'Apply': {
721 + const functionValues = state.values(effect.function);
722 + if (
723 + functionValues.length === 1 &&
724 + functionValues[0].kind === 'FunctionExpression'
725 + ) {
726 + /*
727 + * We're calling a locally declared function, we already know it's effects!
728 + * We just have to substitute in the args for the params
729 + */
730 + const signature = buildSignatureFromFunctionExpression(
731 + state.env,
732 + functionValues[0],
733 + );
734 + if (DEBUG) {
735 + console.log(
736 + `constructed alias signature:\n${printAliasingSignature(signature)}`,
737 + );
738 + }
739 + const signatureEffects = computeEffectsForSignature(
740 + state.env,
741 + signature,
742 + effect.into,
743 + effect.receiver,
744 + effect.args,
745 + functionValues[0].loweredFunc.func.context,
746 + effect.loc,
747 + );
748 + if (signatureEffects != null) {
749 + if (DEBUG) {
750 + console.log('apply function expression effects');
751 + }
752 + applyEffect(
753 + context,
754 + state,
755 + {kind: 'MutateTransitiveConditionally', value: effect.function},
756 + aliased,
757 + effects,
758 + );
759 + for (const signatureEffect of signatureEffects) {
760 + applyEffect(context, state, signatureEffect, aliased, effects);
761 + }
762 + break;
763 + }
764 + }
765 + const signatureEffects =
766 + effect.signature?.aliasing != null
767 + ? computeEffectsForSignature(
768 + state.env,
769 + effect.signature.aliasing,
770 + effect.into,
771 + effect.receiver,
772 + effect.args,
773 + [],
774 + effect.loc,
775 + )
776 + : null;
777 + if (signatureEffects != null) {
778 + if (DEBUG) {
779 + console.log('apply aliasing signature effects');
780 + }
781 + for (const signatureEffect of signatureEffects) {
782 + applyEffect(context, state, signatureEffect, aliased, effects);
783 + }
784 + } else if (effect.signature != null) {
785 + if (DEBUG) {
786 + console.log('apply legacy signature effects');
787 + }
788 + const legacyEffects = computeEffectsForLegacySignature(
789 + state,
790 + effect.signature,
791 + effect.into,
792 + effect.receiver,
793 + effect.args,
794 + effect.loc,
795 + );
796 + for (const legacyEffect of legacyEffects) {
797 + applyEffect(context, state, legacyEffect, aliased, effects);
798 + }
799 + } else {
800 + if (DEBUG) {
801 + console.log('default effects');
802 + }
803 + applyEffect(
804 + context,
805 + state,
806 + {
807 + kind: 'Create',
808 + into: effect.into,
809 + value: ValueKind.Mutable,
810 + reason: ValueReason.Other,
811 + },
812 + aliased,
813 + effects,
814 + );
815 + /*
816 + * If no signature then by default:
817 + * - All operands are conditionally mutated, except some instruction
818 + * variants are assumed to not mutate the callee (such as `new`)
819 + * - All operands are captured into (but not directly aliased as)
820 + * every other argument.
821 + */
822 + for (const arg of [effect.receiver, effect.function, ...effect.args]) {
823 + if (arg.kind === 'Hole') {
824 + continue;
825 + }
826 + const operand = arg.kind === 'Identifier' ? arg : arg.place;
827 + if (operand !== effect.function || effect.mutatesFunction) {
828 + applyEffect(
829 + context,
830 + state,
831 + {
832 + kind: 'MutateTransitiveConditionally',
833 + value: operand,
834 + },
835 + aliased,
836 + effects,
837 + );
838 + }
839 + const mutateIterator =
840 + arg.kind === 'Spread' ? conditionallyMutateIterator(operand) : null;
841 + if (mutateIterator) {
842 + applyEffect(context, state, mutateIterator, aliased, effects);
843 + }
844 + applyEffect(
845 + context,
846 + state,
847 + // OK: recording information flow
848 + {kind: 'Alias', from: operand, into: effect.into},
849 + aliased,
850 + effects,
851 + );
852 + for (const otherArg of [
853 + effect.receiver,
854 + effect.function,
855 + ...effect.args,
856 + ]) {
857 + if (otherArg.kind === 'Hole') {
858 + continue;
859 + }
860 + const other =
861 + otherArg.kind === 'Identifier' ? otherArg : otherArg.place;
862 + if (other === arg) {
863 + continue;
864 + }
865 + applyEffect(
866 + context,
867 + state,
868 + {
869 + /*
870 + * OK: a function might store one operand into another,
871 + * but it can't force one to alias another
872 + */
873 + kind: 'Capture',
874 + from: operand,
875 + into: other,
876 + },
877 + aliased,
878 + effects,
879 + );
880 + }
881 + }
882 + }
883 + break;
884 + }
885 + case 'Mutate':
886 + case 'MutateConditionally':
887 + case 'MutateTransitive':
888 + case 'MutateTransitiveConditionally': {
889 + const mutationKind = state.mutate(effect.kind, effect.value);
890 + if (mutationKind === 'mutate') {
891 + effects.push(effect);
892 + } else if (mutationKind === 'mutate-ref') {
893 + // no-op
894 + } else if (
895 + mutationKind !== 'none' &&
896 + (effect.kind === 'Mutate' || effect.kind === 'MutateTransitive')
897 + ) {
898 + const value = state.kind(effect.value);
899 + if (DEBUG) {
900 + console.log(`invalid mutation: ${printAliasingEffect(effect)}`);
901 + console.log(prettyFormat(state.debugAbstractValue(value)));
902 + }
903 +
904 + const reason = getWriteErrorReason({
905 + kind: value.kind,
906 + reason: value.reason,
907 + context: new Set(),
908 + });
909 + effects.push({
910 + kind:
911 + value.kind === ValueKind.Frozen ? 'MutateFrozen' : 'MutateGlobal',
912 + place: effect.value,
913 + error: {
914 + severity: ErrorSeverity.InvalidReact,
915 + reason,
916 + description:
917 + effect.value.identifier.name !== null &&
918 + effect.value.identifier.name.kind === 'named'
919 + ? `Found mutation of \`${effect.value.identifier.name.value}\``
920 + : null,
921 + loc: effect.value.loc,
922 + suggestions: null,
923 + },
924 + });
925 + }
926 + break;
927 + }
928 + case 'Impure':
929 + case 'Render':
930 + case 'MutateFrozen':
931 + case 'MutateGlobal': {
932 + effects.push(effect);
933 + break;
934 + }
935 + default: {
936 + assertExhaustive(
937 + effect,
938 + `Unexpected effect kind '${(effect as any).kind as any}'`,
939 + );
940 + }
941 + }
942 +}
943 +
944 +class InferenceState {
945 + env: Environment;
946 + #isFunctionExpression: boolean;
947 +
948 + // The kind of each value, based on its allocation site
949 + #values: Map<InstructionValue, AbstractValue>;
950 + /*
951 + * The set of values pointed to by each identifier. This is a set
952 + * to accomodate phi points (where a variable may have different
953 + * values from different control flow paths).
954 + */
955 + #variables: Map<IdentifierId, Set<InstructionValue>>;
956 +
957 + constructor(
958 + env: Environment,
959 + isFunctionExpression: boolean,
960 + values: Map<InstructionValue, AbstractValue>,
961 + variables: Map<IdentifierId, Set<InstructionValue>>,
962 + ) {
963 + this.env = env;
964 + this.#isFunctionExpression = isFunctionExpression;
965 + this.#values = values;
966 + this.#variables = variables;
967 + }
968 +
969 + static empty(
970 + env: Environment,
971 + isFunctionExpression: boolean,
972 + ): InferenceState {
973 + return new InferenceState(env, isFunctionExpression, new Map(), new Map());
974 + }
975 +
976 + get isFunctionExpression(): boolean {
977 + return this.#isFunctionExpression;
978 + }
979 +
980 + // (Re)initializes a @param value with its default @param kind.
981 + initialize(value: InstructionValue, kind: AbstractValue): void {
982 + CompilerError.invariant(value.kind !== 'LoadLocal', {
983 + reason:
984 + '[InferMutationAliasingEffects] Expected all top-level identifiers to be defined as variables, not values',
985 + description: null,
986 + loc: value.loc,
987 + suggestions: null,
988 + });
989 + this.#values.set(value, kind);
990 + }
991 +
992 + values(place: Place): Array<InstructionValue> {
993 + const values = this.#variables.get(place.identifier.id);
994 + CompilerError.invariant(values != null, {
995 + reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`,
996 + description: `${printPlace(place)}`,
997 + loc: place.loc,
998 + suggestions: null,
999 + });
1000 + return Array.from(values);
1001 + }
1002 +
1003 + // Lookup the kind of the given @param value.
1004 + kind(place: Place): AbstractValue {
1005 + const values = this.#variables.get(place.identifier.id);
1006 + CompilerError.invariant(values != null, {
1007 + reason: `[InferMutationAliasingEffects] Expected value kind to be initialized`,
1008 + description: `${printPlace(place)}`,
1009 + loc: place.loc,
1010 + suggestions: null,
1011 + });
1012 + let mergedKind: AbstractValue | null = null;
1013 + for (const value of values) {
1014 + const kind = this.#values.get(value)!;
1015 + mergedKind =
1016 + mergedKind !== null ? mergeAbstractValues(mergedKind, kind) : kind;
1017 + }
1018 + CompilerError.invariant(mergedKind !== null, {
1019 + reason: `[InferMutationAliasingEffects] Expected at least one value`,
1020 + description: `No value found at \`${printPlace(place)}\``,
1021 + loc: place.loc,
1022 + suggestions: null,
1023 + });
1024 + return mergedKind;
1025 + }
1026 +
1027 + // Updates the value at @param place to point to the same value as @param value.
1028 + alias(place: Place, value: Place): void {
1029 + const values = this.#variables.get(value.identifier.id);
1030 + CompilerError.invariant(values != null, {
1031 + reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
1032 + description: `${printIdentifier(value.identifier)}`,
1033 + loc: value.loc,
1034 + suggestions: null,
1035 + });
1036 + this.#variables.set(place.identifier.id, new Set(values));
1037 + }
1038 +
1039 + appendAlias(place: Place, value: Place): void {
1040 + const values = this.#variables.get(value.identifier.id);
1041 + CompilerError.invariant(values != null, {
1042 + reason: `[InferMutationAliasingEffects] Expected value for identifier to be initialized`,
1043 + description: `${printIdentifier(value.identifier)}`,
1044 + loc: value.loc,
1045 + suggestions: null,
1046 + });
1047 + const prevValues = this.values(place);
1048 + this.#variables.set(
1049 + place.identifier.id,
1050 + new Set([...prevValues, ...values]),
1051 + );
1052 + }
1053 +
1054 + // Defines (initializing or updating) a variable with a specific kind of value.
1055 + define(place: Place, value: InstructionValue): void {
1056 + CompilerError.invariant(this.#values.has(value), {
1057 + reason: `[InferMutationAliasingEffects] Expected value to be initialized at '${printSourceLocation(
1058 + value.loc,
1059 + )}'`,
1060 + description: printInstructionValue(value),
1061 + loc: value.loc,
1062 + suggestions: null,
1063 + });
1064 + this.#variables.set(place.identifier.id, new Set([value]));
1065 + }
1066 +
1067 + isDefined(place: Place): boolean {
1068 + return this.#variables.has(place.identifier.id);
1069 + }
1070 +
1071 + /**
1072 + * Marks @param place as transitively frozen. Returns true if the value was not
1073 + * already frozen, false if the value is already frozen (or already known immutable).
1074 + */
1075 + freeze(place: Place, reason: ValueReason): boolean {
1076 + const value = this.kind(place);
1077 + switch (value.kind) {
1078 + case ValueKind.Context:
1079 + case ValueKind.Mutable:
1080 + case ValueKind.MaybeFrozen: {
1081 + const values = this.values(place);
1082 + for (const instrValue of values) {
1083 + this.freezeValue(instrValue, reason);
1084 + }
1085 + return true;
1086 + }
1087 + case ValueKind.Frozen:
1088 + case ValueKind.Global:
1089 + case ValueKind.Primitive: {
1090 + return false;
1091 + }
1092 + default: {
1093 + assertExhaustive(
1094 + value.kind,
1095 + `Unexpected value kind '${(value as any).kind}'`,
1096 + );
1097 + }
1098 + }
1099 + }
1100 +
1101 + freezeValue(value: InstructionValue, reason: ValueReason): void {
1102 + this.#values.set(value, {
1103 + kind: ValueKind.Frozen,
1104 + reason: new Set([reason]),
1105 + });
1106 + if (DEBUG) {
1107 + console.log(`freeze value: ${printInstructionValue(value)} ${reason}`);
1108 + }
1109 + if (
1110 + value.kind === 'FunctionExpression' &&
1111 + (this.env.config.enablePreserveExistingMemoizationGuarantees ||
1112 + this.env.config.enableTransitivelyFreezeFunctionExpressions)
1113 + ) {
1114 + for (const place of value.loweredFunc.func.context) {
1115 + this.freeze(place, reason);
1116 + }
1117 + }
1118 + }
1119 +
1120 + mutate(
1121 + variant:
1122 + | 'Mutate'
1123 + | 'MutateConditionally'
1124 + | 'MutateTransitive'
1125 + | 'MutateTransitiveConditionally',
1126 + place: Place,
1127 + ): 'none' | 'mutate' | 'mutate-frozen' | 'mutate-global' | 'mutate-ref' {
1128 + if (isRefOrRefValue(place.identifier)) {
1129 + return 'mutate-ref';
1130 + }
1131 + const kind = this.kind(place).kind;
1132 + switch (variant) {
1133 + case 'MutateConditionally':
1134 + case 'MutateTransitiveConditionally': {
1135 + switch (kind) {
1136 + case ValueKind.Mutable:
1137 + case ValueKind.Context: {
1138 + return 'mutate';
1139 + }
1140 + default: {
1141 + return 'none';
1142 + }
1143 + }
1144 + }
1145 + case 'Mutate':
1146 + case 'MutateTransitive': {
1147 + switch (kind) {
1148 + case ValueKind.Mutable:
1149 + case ValueKind.Context: {
1150 + return 'mutate';
1151 + }
1152 + case ValueKind.Primitive: {
1153 + // technically an error, but it's not React specific
1154 + return 'none';
1155 + }
1156 + case ValueKind.Frozen: {
1157 + return 'mutate-frozen';
1158 + }
1159 + case ValueKind.Global: {
1160 + return 'mutate-global';
1161 + }
1162 + case ValueKind.MaybeFrozen: {
1163 + return 'none';
1164 + }
1165 + default: {
1166 + assertExhaustive(kind, `Unexpected kind ${kind}`);
1167 + }
1168 + }
1169 + }
1170 + default: {
1171 + assertExhaustive(variant, `Unexpected mutation variant ${variant}`);
1172 + }
1173 + }
1174 + }
1175 +
1176 + /*
1177 + * Combine the contents of @param this and @param other, returning a new
1178 + * instance with the combined changes _if_ there are any changes, or
1179 + * returning null if no changes would occur. Changes include:
1180 + * - new entries in @param other that did not exist in @param this
1181 + * - entries whose values differ in @param this and @param other,
1182 + * and where joining the values produces a different value than
1183 + * what was in @param this.
1184 + *
1185 + * Note that values are joined using a lattice operation to ensure
1186 + * termination.
1187 + */
1188 + merge(other: InferenceState): InferenceState | null {
1189 + let nextValues: Map<InstructionValue, AbstractValue> | null = null;
1190 + let nextVariables: Map<IdentifierId, Set<InstructionValue>> | null = null;
1191 +
1192 + for (const [id, thisValue] of this.#values) {
1193 + const otherValue = other.#values.get(id);
1194 + if (otherValue !== undefined) {
1195 + const mergedValue = mergeAbstractValues(thisValue, otherValue);
1196 + if (mergedValue !== thisValue) {
1197 + nextValues = nextValues ?? new Map(this.#values);
1198 + nextValues.set(id, mergedValue);
1199 + }
1200 + }
1201 + }
1202 + for (const [id, otherValue] of other.#values) {
1203 + if (this.#values.has(id)) {
1204 + // merged above
1205 + continue;
1206 + }
1207 + nextValues = nextValues ?? new Map(this.#values);
1208 + nextValues.set(id, otherValue);
1209 + }
1210 +
1211 + for (const [id, thisValues] of this.#variables) {
1212 + const otherValues = other.#variables.get(id);
1213 + if (otherValues !== undefined) {
1214 + let mergedValues: Set<InstructionValue> | null = null;
1215 + for (const otherValue of otherValues) {
1216 + if (!thisValues.has(otherValue)) {
1217 + mergedValues = mergedValues ?? new Set(thisValues);
1218 + mergedValues.add(otherValue);
1219 + }
1220 + }
1221 + if (mergedValues !== null) {
1222 + nextVariables = nextVariables ?? new Map(this.#variables);
1223 + nextVariables.set(id, mergedValues);
1224 + }
1225 + }
1226 + }
1227 + for (const [id, otherValues] of other.#variables) {
1228 + if (this.#variables.has(id)) {
1229 + continue;
1230 + }
1231 + nextVariables = nextVariables ?? new Map(this.#variables);
1232 + nextVariables.set(id, new Set(otherValues));
1233 + }
1234 +
1235 + if (nextVariables === null && nextValues === null) {
1236 + return null;
1237 + } else {
1238 + return new InferenceState(
1239 + this.env,
1240 + this.#isFunctionExpression,
1241 + nextValues ?? new Map(this.#values),
1242 + nextVariables ?? new Map(this.#variables),
1243 + );
1244 + }
1245 + }
1246 +
1247 + /*
1248 + * Returns a copy of this state.
1249 + * TODO: consider using persistent data structures to make
1250 + * clone cheaper.
1251 + */
1252 + clone(): InferenceState {
1253 + return new InferenceState(
1254 + this.env,
1255 + this.#isFunctionExpression,
1256 + new Map(this.#values),
1257 + new Map(this.#variables),
1258 + );
1259 + }
1260 +
1261 + /*
1262 + * For debugging purposes, dumps the state to a plain
1263 + * object so that it can printed as JSON.
1264 + */
1265 + debug(): any {
1266 + const result: any = {values: {}, variables: {}};
1267 + const objects: Map<InstructionValue, number> = new Map();
1268 + function identify(value: InstructionValue): number {
1269 + let id = objects.get(value);
1270 + if (id == null) {
1271 + id = objects.size;
1272 + objects.set(value, id);
1273 + }
1274 + return id;
1275 + }
1276 + for (const [value, kind] of this.#values) {
1277 + const id = identify(value);
1278 + result.values[id] = {
1279 + abstract: this.debugAbstractValue(kind),
1280 + value: printInstructionValue(value),
1281 + };
1282 + }
1283 + for (const [variable, values] of this.#variables) {
1284 + result.variables[`$${variable}`] = [...values].map(identify);
1285 + }
1286 + return result;
1287 + }
1288 +
1289 + debugAbstractValue(value: AbstractValue): any {
1290 + return {
1291 + kind: value.kind,
1292 + reason: [...value.reason],
1293 + };
1294 + }
1295 +
1296 + inferPhi(phi: Phi): void {
1297 + const values: Set<InstructionValue> = new Set();
1298 + for (const [_, operand] of phi.operands) {
1299 + const operandValues = this.#variables.get(operand.identifier.id);
1300 + // This is a backedge that will be handled later by State.merge
1301 + if (operandValues === undefined) continue;
1302 + for (const v of operandValues) {
1303 + values.add(v);
1304 + }
1305 + }
1306 +
1307 + if (values.size > 0) {
1308 + this.#variables.set(phi.place.identifier.id, values);
1309 + }
1310 + }
1311 +}
1312 +
1313 +/**
1314 + * Returns a value that represents the combined states of the two input values.
1315 + * If the two values are semantically equivalent, it returns the first argument.
1316 + */
1317 +function mergeAbstractValues(
1318 + a: AbstractValue,
1319 + b: AbstractValue,
1320 +): AbstractValue {
1321 + const kind = mergeValueKinds(a.kind, b.kind);
1322 + if (
1323 + kind === a.kind &&
1324 + kind === b.kind &&
1325 + Set_isSuperset(a.reason, b.reason)
1326 + ) {
1327 + return a;
1328 + }
1329 + const reason = new Set(a.reason);
1330 + for (const r of b.reason) {
1331 + reason.add(r);
1332 + }
1333 + return {kind, reason};
1334 +}
1335 +
1336 +type InstructionSignature = {
1337 + effects: ReadonlyArray<AliasingEffect>;
1338 +};
1339 +
1340 +function conditionallyMutateIterator(place: Place): AliasingEffect | null {
1341 + if (
1342 + !(
1343 + isArrayType(place.identifier) ||
1344 + isSetType(place.identifier) ||
1345 + isMapType(place.identifier)
1346 + )
1347 + ) {
1348 + return {
1349 + kind: 'MutateTransitiveConditionally',
1350 + value: place,
1351 + };
1352 + }
1353 + return null;
1354 +}
1355 +
1356 +/**
1357 + * Computes an effect signature for the instruction _without_ looking at the inference state,
1358 + * and only using the semantics of the instructions and the inferred types. The idea is to make
1359 + * it easy to check that the semantics of each instruction are preserved by describing only the
1360 + * effects and not making decisions based on the inference state.
1361 + *
1362 + * Then in applySignature(), above, we refine this signature based on the inference state.
1363 + *
1364 + * NOTE: this function is designed to be cached so it's only computed once upon first visiting
1365 + * an instruction.
1366 + */
1367 +function computeSignatureForInstruction(
1368 + context: Context,
1369 + env: Environment,
1370 + instr: Instruction,
1371 +): InstructionSignature {
1372 + const {lvalue, value} = instr;
1373 + const effects: Array<AliasingEffect> = [];
1374 + switch (value.kind) {
1375 + case 'ArrayExpression': {
1376 + effects.push({
1377 + kind: 'Create',
1378 + into: lvalue,
1379 + value: ValueKind.Mutable,
1380 + reason: ValueReason.Other,
1381 + });
1382 + // All elements are captured into part of the output value
1383 + for (const element of value.elements) {
1384 + if (element.kind === 'Identifier') {
1385 + effects.push({
1386 + kind: 'Capture',
1387 + from: element,
1388 + into: lvalue,
1389 + });
1390 + } else if (element.kind === 'Spread') {
1391 + const mutateIterator = conditionallyMutateIterator(element.place);
1392 + if (mutateIterator != null) {
1393 + effects.push(mutateIterator);
1394 + }
1395 + effects.push({
1396 + kind: 'Capture',
1397 + from: element.place,
1398 + into: lvalue,
1399 + });
1400 + } else {
1401 + continue;
1402 + }
1403 + }
1404 + break;
1405 + }
1406 + case 'ObjectExpression': {
1407 + effects.push({
1408 + kind: 'Create',
1409 + into: lvalue,
1410 + value: ValueKind.Mutable,
1411 + reason: ValueReason.Other,
1412 + });
1413 + for (const property of value.properties) {
1414 + if (property.kind === 'ObjectProperty') {
1415 + effects.push({
1416 + kind: 'Capture',
1417 + from: property.place,
1418 + into: lvalue,
1419 + });
1420 + } else {
1421 + effects.push({
1422 + kind: 'Capture',
1423 + from: property.place,
1424 + into: lvalue,
1425 + });
1426 + }
1427 + }
1428 + break;
1429 + }
1430 + case 'Await': {
1431 + effects.push({
1432 + kind: 'Create',
1433 + into: lvalue,
1434 + value: ValueKind.Mutable,
1435 + reason: ValueReason.Other,
1436 + });
1437 + // Potentially mutates the receiver (awaiting it changes its state and can run side effects)
1438 + effects.push({kind: 'MutateTransitiveConditionally', value: value.value});
1439 + /**
1440 + * Data from the promise may be returned into the result, but await does not directly return
1441 + * the promise itself
1442 + */
1443 + effects.push({
1444 + kind: 'Capture',
1445 + from: value.value,
1446 + into: lvalue,
1447 + });
1448 + break;
1449 + }
1450 + case 'NewExpression':
1451 + case 'CallExpression':
1452 + case 'MethodCall': {
1453 + let callee;
1454 + let receiver;
1455 + let mutatesCallee;
1456 + if (value.kind === 'NewExpression') {
1457 + callee = value.callee;
1458 + receiver = value.callee;
1459 + mutatesCallee = false;
1460 + } else if (value.kind === 'CallExpression') {
1461 + callee = value.callee;
1462 + receiver = value.callee;
1463 + mutatesCallee = true;
1464 + } else if (value.kind === 'MethodCall') {
1465 + callee = value.property;
1466 + receiver = value.receiver;
1467 + mutatesCallee = false;
1468 + } else {
1469 + assertExhaustive(
1470 + value,
1471 + `Unexpected value kind '${(value as any).kind}'`,
1472 + );
1473 + }
1474 + const signature = getFunctionCallSignature(env, callee.identifier.type);
1475 + effects.push({
1476 + kind: 'Apply',
1477 + receiver,
1478 + function: callee,
1479 + mutatesFunction: mutatesCallee,
1480 + args: value.args,
1481 + into: lvalue,
1482 + signature,
1483 + loc: value.loc,
1484 + });
1485 + break;
1486 + }
1487 + case 'PropertyDelete':
1488 + case 'ComputedDelete': {
1489 + effects.push({
1490 + kind: 'Create',
1491 + into: lvalue,
1492 + value: ValueKind.Primitive,
1493 + reason: ValueReason.Other,
1494 + });
1495 + // Mutates the object by removing the property, no aliasing
1496 + effects.push({kind: 'Mutate', value: value.object});
1497 + break;
1498 + }
1499 + case 'PropertyLoad':
1500 + case 'ComputedLoad': {
1501 + if (isPrimitiveType(lvalue.identifier)) {
1502 + effects.push({
1503 + kind: 'Create',
1504 + into: lvalue,
1505 + value: ValueKind.Primitive,
1506 + reason: ValueReason.Other,
1507 + });
1508 + } else {
1509 + effects.push({
1510 + kind: 'CreateFrom',
1511 + from: value.object,
1512 + into: lvalue,
1513 + });
1514 + }
1515 + break;
1516 + }
1517 + case 'PropertyStore':
1518 + case 'ComputedStore': {
1519 + effects.push({kind: 'Mutate', value: value.object});
1520 + effects.push({
1521 + kind: 'Capture',
1522 + from: value.value,
1523 + into: value.object,
1524 + });
1525 + effects.push({
1526 + kind: 'Create',
1527 + into: lvalue,
1528 + value: ValueKind.Primitive,
1529 + reason: ValueReason.Other,
1530 + });
1531 + break;
1532 + }
1533 + case 'ObjectMethod':
1534 + case 'FunctionExpression': {
1535 + /**
1536 + * We've already analyzed the function expression in AnalyzeFunctions. There, we assign
1537 + * a Capture effect to any context variable that appears (locally) to be aliased and/or
1538 + * mutated. The precise effects are annotated on the function expression's aliasingEffects
1539 + * property, but we don't want to execute those effects yet. We can only use those when
1540 + * we know exactly how the function is invoked — via an Apply effect from a custom signature.
1541 + *
1542 + * But in the general case, functions can be passed around and possibly called in ways where
1543 + * we don't know how to interpret their precise effects. For example:
1544 + *
1545 + * ```
1546 + * const a = {};
1547 + *
1548 + * // We don't want to consider a as mutating here, this just declares the function
1549 + * const f = () => { maybeMutate(a) };
1550 + *
1551 + * // We don't want to consider a as mutating here either, it can't possibly call f yet
1552 + * const x = [f];
1553 + *
1554 + * // Here we have to assume that f can be called (transitively), and have to consider a
1555 + * // as mutating
1556 + * callAllFunctionInArray(x);
1557 + * ```
1558 + *
1559 + * So for any context variables that were inferred as captured or mutated, we record a
1560 + * Capture effect. If the resulting function is transitively mutated, this will mean
1561 + * that those operands are also considered mutated. If the function is never called,
1562 + * they won't be!
1563 + *
1564 + * This relies on the rule that:
1565 + * Capture a -> b and MutateTransitive(b) => Mutate(a)
1566 + *
1567 + * Substituting:
1568 + * Capture contextvar -> function and MutateTransitive(function) => Mutate(contextvar)
1569 + *
1570 + * Note that if the type of the context variables are frozen, global, or primitive, the
1571 + * Capture will either get pruned or downgraded to an ImmutableCapture.
1572 + */
1573 + effects.push({
1574 + kind: 'CreateFunction',
1575 + into: lvalue,
1576 + function: value,
1577 + captures: value.loweredFunc.func.context.filter(
1578 + operand => operand.effect === Effect.Capture,
1579 + ),
1580 + });
1581 + break;
1582 + }
1583 + case 'GetIterator': {
1584 + effects.push({
1585 + kind: 'Create',
1586 + into: lvalue,
1587 + value: ValueKind.Mutable,
1588 + reason: ValueReason.Other,
1589 + });
1590 + if (
1591 + isArrayType(value.collection.identifier) ||
1592 + isMapType(value.collection.identifier) ||
1593 + isSetType(value.collection.identifier)
1594 + ) {
1595 + /*
1596 + * Builtin collections are known to return a fresh iterator on each call,
1597 + * so the iterator does not alias the collection
1598 + */
1599 + effects.push({
1600 + kind: 'Capture',
1601 + from: value.collection,
1602 + into: lvalue,
1603 + });
1604 + } else {
1605 + /*
1606 + * Otherwise, the object may return itself as the iterator, so we have to
1607 + * assume that the result directly aliases the collection. Further, the
1608 + * method to get the iterator could potentially mutate the collection
1609 + */
1610 + effects.push({kind: 'Alias', from: value.collection, into: lvalue});
1611 + effects.push({
1612 + kind: 'MutateTransitiveConditionally',
1613 + value: value.collection,
1614 + });
1615 + }
1616 + break;
1617 + }
1618 + case 'IteratorNext': {
1619 + /*
1620 + * Technically advancing an iterator will always mutate it (for any reasonable implementation)
1621 + * But because we create an alias from the collection to the iterator if we don't know the type,
1622 + * then it's possible the iterator is aliased to a frozen value and we wouldn't want to error.
1623 + * so we mark this as conditional mutation to allow iterating frozen values.
1624 + */
1625 + effects.push({kind: 'MutateConditionally', value: value.iterator});
1626 + // Extracts part of the original collection into the result
1627 + effects.push({
1628 + kind: 'CreateFrom',
1629 + from: value.collection,
1630 + into: lvalue,
1631 + });
1632 + break;
1633 + }
1634 + case 'NextPropertyOf': {
1635 + effects.push({
1636 + kind: 'Create',
1637 + into: lvalue,
1638 + value: ValueKind.Primitive,
1639 + reason: ValueReason.Other,
1640 + });
1641 + break;
1642 + }
1643 + case 'JsxExpression':
1644 + case 'JsxFragment': {
1645 + effects.push({
1646 + kind: 'Create',
1647 + into: lvalue,
1648 + value: ValueKind.Frozen,
1649 + reason: ValueReason.JsxCaptured,
1650 + });
1651 + for (const operand of eachInstructionValueOperand(value)) {
1652 + effects.push({
1653 + kind: 'Freeze',
1654 + value: operand,
1655 + reason: ValueReason.JsxCaptured,
1656 + });
1657 + effects.push({
1658 + kind: 'Capture',
1659 + from: operand,
1660 + into: lvalue,
1661 + });
1662 + }
1663 + if (value.kind === 'JsxExpression') {
1664 + if (value.tag.kind === 'Identifier') {
1665 + // Tags are render function, by definition they're called during render
1666 + effects.push({
1667 + kind: 'Render',
1668 + place: value.tag,
1669 + });
1670 + }
1671 + if (value.children != null) {
1672 + // Children are typically called during render, not used as an event/effect callback
1673 + for (const child of value.children) {
1674 + effects.push({
1675 + kind: 'Render',
1676 + place: child,
1677 + });
1678 + }
1679 + }
1680 + }
1681 + break;
1682 + }
1683 + case 'DeclareLocal': {
1684 + // TODO check this
1685 + effects.push({
1686 + kind: 'Create',
1687 + into: value.lvalue.place,
1688 + // TODO: what kind here???
1689 + value: ValueKind.Primitive,
1690 + reason: ValueReason.Other,
1691 + });
1692 + effects.push({
1693 + kind: 'Create',
1694 + into: lvalue,
1695 + // TODO: what kind here???
1696 + value: ValueKind.Primitive,
1697 + reason: ValueReason.Other,
1698 + });
1699 + break;
1700 + }
1701 + case 'Destructure': {
1702 + for (const patternLValue of eachInstructionValueLValue(value)) {
1703 + if (isPrimitiveType(patternLValue.identifier)) {
1704 + effects.push({
1705 + kind: 'Create',
1706 + into: patternLValue,
1707 + value: ValueKind.Primitive,
1708 + reason: ValueReason.Other,
1709 + });
1710 + } else {
1711 + effects.push({
1712 + kind: 'CreateFrom',
1713 + from: value.value,
1714 + into: patternLValue,
1715 + });
1716 + }
1717 + }
1718 + effects.push({kind: 'Assign', from: value.value, into: lvalue});
1719 + break;
1720 + }
1721 + case 'LoadContext': {
1722 + /*
1723 + * Context variables are like mutable boxes. Loading from one
1724 + * is equivalent to a PropertyLoad from the box, so we model it
1725 + * with the same effect we use there (CreateFrom)
1726 + */
1727 + effects.push({kind: 'CreateFrom', from: value.place, into: lvalue});
1728 + break;
1729 + }
1730 + case 'DeclareContext': {
1731 + // Context variables are conceptually like mutable boxes
1732 + const kind = value.lvalue.kind;
1733 + if (
1734 + !context.hoistedContextDeclarations.has(
1735 + value.lvalue.place.identifier.declarationId,
1736 + ) ||
1737 + kind === InstructionKind.HoistedConst ||
1738 + kind === InstructionKind.HoistedFunction ||
1739 + kind === InstructionKind.HoistedLet
1740 + ) {
1741 + /**
1742 + * If this context variable is not hoisted, or this is the declaration doing the hoisting,
1743 + * then we create the box.
1744 + */
1745 + effects.push({
1746 + kind: 'Create',
1747 + into: value.lvalue.place,
1748 + value: ValueKind.Mutable,
1749 + reason: ValueReason.Other,
1750 + });
1751 + } else {
1752 + /**
1753 + * Otherwise this may be a "declare", but there was a previous DeclareContext that
1754 + * hoisted this variable, and we're mutating it here.
1755 + */
1756 + effects.push({kind: 'Mutate', value: value.lvalue.place});
1757 + }
1758 + effects.push({
1759 + kind: 'Create',
1760 + into: lvalue,
1761 + // The result can't be referenced so this value doesn't matter
1762 + value: ValueKind.Primitive,
1763 + reason: ValueReason.Other,
1764 + });
1765 + break;
1766 + }
1767 + case 'StoreContext': {
1768 + /*
1769 + * Context variables are like mutable boxes, so semantically
1770 + * we're either creating (let/const) or mutating (reassign) a box,
1771 + * and then capturing the value into it.
1772 + */
1773 + if (
1774 + value.lvalue.kind === InstructionKind.Reassign ||
1775 + context.hoistedContextDeclarations.has(
1776 + value.lvalue.place.identifier.declarationId,
1777 + )
1778 + ) {
1779 + effects.push({kind: 'Mutate', value: value.lvalue.place});
1780 + } else {
1781 + effects.push({
1782 + kind: 'Create',
1783 + into: value.lvalue.place,
1784 + value: ValueKind.Mutable,
1785 + reason: ValueReason.Other,
1786 + });
1787 + }
1788 + effects.push({
1789 + kind: 'Capture',
1790 + from: value.value,
1791 + into: value.lvalue.place,
1792 + });
1793 + effects.push({kind: 'Assign', from: value.value, into: lvalue});
1794 + break;
1795 + }
1796 + case 'LoadLocal': {
1797 + effects.push({kind: 'Assign', from: value.place, into: lvalue});
1798 + break;
1799 + }
1800 + case 'StoreLocal': {
1801 + effects.push({
1802 + kind: 'Assign',
1803 + from: value.value,
1804 + into: value.lvalue.place,
1805 + });
1806 + effects.push({kind: 'Assign', from: value.value, into: lvalue});
1807 + break;
1808 + }
1809 + case 'PostfixUpdate':
1810 + case 'PrefixUpdate': {
1811 + effects.push({
1812 + kind: 'Create',
1813 + into: lvalue,
1814 + value: ValueKind.Primitive,
1815 + reason: ValueReason.Other,
1816 + });
1817 + effects.push({
1818 + kind: 'Create',
1819 + into: value.lvalue,
1820 + value: ValueKind.Primitive,
1821 + reason: ValueReason.Other,
1822 + });
1823 + break;
1824 + }
1825 + case 'StoreGlobal': {
1826 + effects.push({
1827 + kind: 'MutateGlobal',
1828 + place: value.value,
1829 + error: {
1830 + reason:
1831 + 'Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
1832 + loc: instr.loc,
1833 + suggestions: null,
1834 + severity: ErrorSeverity.InvalidReact,
1835 + },
1836 + });
1837 + effects.push({kind: 'Assign', from: value.value, into: lvalue});
1838 + break;
1839 + }
1840 + case 'TypeCastExpression': {
1841 + effects.push({kind: 'Assign', from: value.value, into: lvalue});
1842 + break;
1843 + }
1844 + case 'LoadGlobal': {
1845 + effects.push({
1846 + kind: 'Create',
1847 + into: lvalue,
1848 + value: ValueKind.Global,
1849 + reason: ValueReason.Global,
1850 + });
1851 + break;
1852 + }
1853 + case 'StartMemoize':
1854 + case 'FinishMemoize': {
1855 + if (env.config.enablePreserveExistingMemoizationGuarantees) {
1856 + for (const operand of eachInstructionValueOperand(value)) {
1857 + effects.push({
1858 + kind: 'Freeze',
1859 + value: operand,
1860 + reason: ValueReason.Other,
1861 + });
1862 + }
1863 + }
1864 + effects.push({
1865 + kind: 'Create',
1866 + into: lvalue,
1867 + value: ValueKind.Primitive,
1868 + reason: ValueReason.Other,
1869 + });
1870 + break;
1871 + }
1872 + case 'TaggedTemplateExpression':
1873 + case 'BinaryExpression':
1874 + case 'Debugger':
1875 + case 'JSXText':
1876 + case 'MetaProperty':
1877 + case 'Primitive':
1878 + case 'RegExpLiteral':
1879 + case 'TemplateLiteral':
1880 + case 'UnaryExpression':
1881 + case 'UnsupportedNode': {
1882 + effects.push({
1883 + kind: 'Create',
1884 + into: lvalue,
1885 + value: ValueKind.Primitive,
1886 + reason: ValueReason.Other,
1887 + });
1888 + break;
1889 + }
1890 + }
1891 + return {
1892 + effects,
1893 + };
1894 +}
1895 +
1896 +/**
1897 + * Creates a set of aliasing effects given a legacy FunctionSignature. This makes all of the
1898 + * old implicit behaviors from the signatures and InferReferenceEffects explicit, see comments
1899 + * in the body for details.
1900 + *
1901 + * The goal of this method is to make it easier to migrate incrementally to the new system,
1902 + * so we don't have to immediately write new signatures for all the methods to get expected
1903 + * compilation output.
1904 + */
1905 +function computeEffectsForLegacySignature(
1906 + state: InferenceState,
1907 + signature: FunctionSignature,
1908 + lvalue: Place,
1909 + receiver: Place,
1910 + args: Array<Place | SpreadPattern | Hole>,
1911 + loc: SourceLocation,
1912 +): Array<AliasingEffect> {
1913 + const returnValueReason = signature.returnValueReason ?? ValueReason.Other;
1914 + const effects: Array<AliasingEffect> = [];
1915 + effects.push({
1916 + kind: 'Create',
1917 + into: lvalue,
1918 + value: signature.returnValueKind,
1919 + reason: returnValueReason,
1920 + });
1921 + if (signature.impure && state.env.config.validateNoImpureFunctionsInRender) {
1922 + effects.push({
1923 + kind: 'Impure',
1924 + place: receiver,
1925 + error: {
1926 + reason:
1927 + 'Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent)',
1928 + description:
1929 + signature.canonicalName != null
1930 + ? `\`${signature.canonicalName}\` is an impure function whose results may change on every call`
1931 + : null,
1932 + severity: ErrorSeverity.InvalidReact,
1933 + loc,
1934 + suggestions: null,
1935 + },
1936 + });
1937 + }
1938 + const stores: Array<Place> = [];
1939 + const captures: Array<Place> = [];
1940 + function visit(place: Place, effect: Effect): void {
1941 + switch (effect) {
1942 + case Effect.Store: {
1943 + effects.push({
1944 + kind: 'Mutate',
1945 + value: place,
1946 + });
1947 + stores.push(place);
1948 + break;
1949 + }
1950 + case Effect.Capture: {
1951 + captures.push(place);
1952 + break;
1953 + }
1954 + case Effect.ConditionallyMutate: {
1955 + effects.push({
1956 + kind: 'MutateTransitiveConditionally',
1957 + value: place,
1958 + });
1959 + break;
1960 + }
1961 + case Effect.ConditionallyMutateIterator: {
1962 + if (
1963 + isArrayType(place.identifier) ||
1964 + isSetType(place.identifier) ||
1965 + isMapType(place.identifier)
1966 + ) {
1967 + effects.push({
1968 + kind: 'Capture',
1969 + from: place,
1970 + into: lvalue,
1971 + });
1972 + } else {
1973 + effects.push({
1974 + kind: 'Capture',
1975 + from: place,
1976 + into: lvalue,
1977 + });
1978 + captures.push(place);
1979 + effects.push({
1980 + kind: 'MutateTransitiveConditionally',
1981 + value: place,
1982 + });
1983 + }
1984 + break;
1985 + }
1986 + case Effect.Freeze: {
1987 + effects.push({
1988 + kind: 'Freeze',
1989 + value: place,
1990 + reason: returnValueReason,
1991 + });
1992 + break;
1993 + }
1994 + case Effect.Mutate: {
1995 + effects.push({kind: 'MutateTransitive', value: place});
1996 + break;
1997 + }
1998 + case Effect.Read: {
1999 + effects.push({
2000 + kind: 'ImmutableCapture',
2001 + from: place,
2002 + into: lvalue,
2003 + });
2004 + break;
2005 + }
2006 + }
2007 + }
2008 +
2009 + if (
2010 + signature.mutableOnlyIfOperandsAreMutable &&
2011 + areArgumentsImmutableAndNonMutating(state, args)
2012 + ) {
2013 + effects.push({
2014 + kind: 'Alias',
2015 + from: receiver,
2016 + into: lvalue,
2017 + });
2018 + for (const arg of args) {
2019 + if (arg.kind === 'Hole') {
2020 + continue;
2021 + }
2022 + const place = arg.kind === 'Identifier' ? arg : arg.place;
2023 + effects.push({
2024 + kind: 'ImmutableCapture',
2025 + from: place,
2026 + into: lvalue,
2027 + });
2028 + }
2029 + return effects;
2030 + }
2031 +
2032 + if (signature.calleeEffect !== Effect.Capture) {
2033 + /*
2034 + * InferReferenceEffects and FunctionSignature have an implicit assumption that the receiver
2035 + * is captured into the return value. Consider for example the signature for Array.proto.pop:
2036 + * the calleeEffect is Store, since it's a known mutation but non-transitive. But the return
2037 + * of the pop() captures from the receiver! This isn't specified explicitly. So we add this
2038 + * here, and rely on applySignature() to downgrade this to ImmutableCapture (or prune) if
2039 + * the type doesn't actually need to be captured based on the input and return type.
2040 + */
2041 + effects.push({
2042 + kind: 'Alias',
2043 + from: receiver,
2044 + into: lvalue,
2045 + });
2046 + }
2047 + visit(receiver, signature.calleeEffect);
2048 + for (let i = 0; i < args.length; i++) {
2049 + const arg = args[i];
2050 + if (arg.kind === 'Hole') {
2051 + continue;
2052 + }
2053 + const place = arg.kind === 'Identifier' ? arg : arg.place;
2054 + const signatureEffect =
2055 + arg.kind === 'Identifier' && i < signature.positionalParams.length
2056 + ? signature.positionalParams[i]!
2057 + : (signature.restParam ?? Effect.ConditionallyMutate);
2058 + const effect = getArgumentEffect(signatureEffect, arg);
2059 +
2060 + visit(place, effect);
2061 + }
2062 + if (captures.length !== 0) {
2063 + if (stores.length === 0) {
2064 + // If no stores, then capture into the return value
2065 + for (const capture of captures) {
2066 + effects.push({kind: 'Alias', from: capture, into: lvalue});
2067 + }
2068 + } else {
2069 + // Else capture into the stores
2070 + for (const capture of captures) {
2071 + for (const store of stores) {
2072 + effects.push({kind: 'Capture', from: capture, into: store});
2073 + }
2074 + }
2075 + }
2076 + }
2077 + return effects;
2078 +}
2079 +
2080 +/**
2081 + * Returns true if all of the arguments are both non-mutable (immutable or frozen)
2082 + * _and_ are not functions which might mutate their arguments. Note that function
2083 + * expressions count as frozen so long as they do not mutate free variables: this
2084 + * function checks that such functions also don't mutate their inputs.
2085 + */
2086 +function areArgumentsImmutableAndNonMutating(
2087 + state: InferenceState,
2088 + args: Array<Place | SpreadPattern | Hole>,
2089 +): boolean {
2090 + for (const arg of args) {
2091 + if (arg.kind === 'Hole') {
2092 + continue;
2093 + }
2094 + if (arg.kind === 'Identifier' && arg.identifier.type.kind === 'Function') {
2095 + const fnShape = state.env.getFunctionSignature(arg.identifier.type);
2096 + if (fnShape != null) {
2097 + return (
2098 + !fnShape.positionalParams.some(isKnownMutableEffect) &&
2099 + (fnShape.restParam == null ||
2100 + !isKnownMutableEffect(fnShape.restParam))
2101 + );
2102 + }
2103 + }
2104 + const place = arg.kind === 'Identifier' ? arg : arg.place;
2105 +
2106 + const kind = state.kind(place).kind;
2107 + switch (kind) {
2108 + case ValueKind.Primitive:
2109 + case ValueKind.Frozen: {
2110 + /*
2111 + * Only immutable values, or frozen lambdas are allowed.
2112 + * A lambda may appear frozen even if it may mutate its inputs,
2113 + * so we have a second check even for frozen value types
2114 + */
2115 + break;
2116 + }
2117 + default: {
2118 + /**
2119 + * Globals, module locals, and other locally defined functions may
2120 + * mutate their arguments.
2121 + */
2122 + return false;
2123 + }
2124 + }
2125 + const values = state.values(place);
2126 + for (const value of values) {
2127 + if (
2128 + value.kind === 'FunctionExpression' &&
2129 + value.loweredFunc.func.params.some(param => {
2130 + const place = param.kind === 'Identifier' ? param : param.place;
2131 + const range = place.identifier.mutableRange;
2132 + return range.end > range.start + 1;
2133 + })
2134 + ) {
2135 + // This is a function which may mutate its inputs
2136 + return false;
2137 + }
2138 + }
2139 + }
2140 + return true;
2141 +}
2142 +
2143 +function computeEffectsForSignature(
2144 + env: Environment,
2145 + signature: AliasingSignature,
2146 + lvalue: Place,
2147 + receiver: Place,
2148 + args: Array<Place | SpreadPattern | Hole>,
2149 + // Used for signatures constructed dynamically which reference context variables
2150 + context: Array<Place> = [],
2151 + loc: SourceLocation,
2152 +): Array<AliasingEffect> | null {
2153 + if (
2154 + // Not enough args
2155 + signature.params.length > args.length ||
2156 + // Too many args and there is no rest param to hold them
2157 + (args.length > signature.params.length && signature.rest == null)
2158 + ) {
2159 + if (DEBUG) {
2160 + if (signature.params.length > args.length) {
2161 + console.log(
2162 + `not enough args: ${args.length} args for ${signature.params.length} params`,
2163 + );
2164 + } else {
2165 + console.log(
2166 + `too many args: ${args.length} args for ${signature.params.length} params, with no rest param`,
2167 + );
2168 + }
2169 + }
2170 + return null;
2171 + }
2172 + // Build substitutions
2173 + const substitutions: Map<IdentifierId, Array<Place>> = new Map();
2174 + substitutions.set(signature.receiver, [receiver]);
2175 + substitutions.set(signature.returns, [lvalue]);
2176 + const params = signature.params;
2177 + for (let i = 0; i < args.length; i++) {
2178 + const arg = args[i];
2179 + if (arg.kind === 'Hole') {
2180 + continue;
2181 + } else if (params == null || i >= params.length || arg.kind === 'Spread') {
2182 + if (signature.rest == null) {
2183 + if (DEBUG) {
2184 + console.log(`no rest value to hold param`);
2185 + }
2186 + return null;
2187 + }
2188 + const place = arg.kind === 'Identifier' ? arg : arg.place;
2189 + getOrInsertWith(substitutions, signature.rest, () => []).push(place);
2190 + } else {
2191 + const param = params[i];
2192 + substitutions.set(param, [arg]);
2193 + }
2194 + }
2195 +
2196 + /*
2197 + * Signatures constructed dynamically from function expressions will reference values
2198 + * other than their receiver/args/etc. We populate the substitution table with these
2199 + * values so that we can still exit for unpopulated substitutions
2200 + */
2201 + for (const operand of context) {
2202 + substitutions.set(operand.identifier.id, [operand]);
2203 + }
2204 +
2205 + const effects: Array<AliasingEffect> = [];
2206 + for (const signatureTemporary of signature.temporaries) {
2207 + const temp = createTemporaryPlace(env, receiver.loc);
2208 + substitutions.set(signatureTemporary.identifier.id, [temp]);
2209 + }
2210 +
2211 + // Apply substitutions
2212 + for (const effect of signature.effects) {
2213 + switch (effect.kind) {
2214 + case 'Assign':
2215 + case 'ImmutableCapture':
2216 + case 'Alias':
2217 + case 'CreateFrom':
2218 + case 'Capture': {
2219 + const from = substitutions.get(effect.from.identifier.id) ?? [];
2220 + const to = substitutions.get(effect.into.identifier.id) ?? [];
2221 + for (const fromId of from) {
2222 + for (const toId of to) {
2223 + effects.push({
2224 + kind: effect.kind,
2225 + from: fromId,
2226 + into: toId,
2227 + });
2228 + }
2229 + }
2230 + break;
2231 + }
2232 + case 'Impure':
2233 + case 'MutateFrozen':
2234 + case 'MutateGlobal': {
2235 + const values = substitutions.get(effect.place.identifier.id) ?? [];
2236 + for (const value of values) {
2237 + effects.push({kind: effect.kind, place: value, error: effect.error});
2238 + }
2239 + break;
2240 + }
2241 + case 'Render': {
2242 + const values = substitutions.get(effect.place.identifier.id) ?? [];
2243 + for (const value of values) {
2244 + effects.push({kind: effect.kind, place: value});
2245 + }
2246 + break;
2247 + }
2248 + case 'Mutate':
2249 + case 'MutateTransitive':
2250 + case 'MutateTransitiveConditionally':
2251 + case 'MutateConditionally': {
2252 + const values = substitutions.get(effect.value.identifier.id) ?? [];
2253 + for (const id of values) {
2254 + effects.push({kind: effect.kind, value: id});
2255 + }
2256 + break;
2257 + }
2258 + case 'Freeze': {
2259 + const values = substitutions.get(effect.value.identifier.id) ?? [];
2260 + for (const value of values) {
2261 + effects.push({kind: 'Freeze', value, reason: effect.reason});
2262 + }
2263 + break;
2264 + }
2265 + case 'Create': {
2266 + const into = substitutions.get(effect.into.identifier.id) ?? [];
2267 + for (const value of into) {
2268 + effects.push({
2269 + kind: 'Create',
2270 + into: value,
2271 + value: effect.value,
2272 + reason: effect.reason,
2273 + });
2274 + }
2275 + break;
2276 + }
2277 + case 'Apply': {
2278 + const applyReceiver = substitutions.get(effect.receiver.identifier.id);
2279 + if (applyReceiver == null || applyReceiver.length !== 1) {
2280 + if (DEBUG) {
2281 + console.log(`too many substitutions for receiver`);
2282 + }
2283 + return null;
2284 + }
2285 + const applyFunction = substitutions.get(effect.function.identifier.id);
2286 + if (applyFunction == null || applyFunction.length !== 1) {
2287 + if (DEBUG) {
2288 + console.log(`too many substitutions for function`);
2289 + }
2290 + return null;
2291 + }
2292 + const applyInto = substitutions.get(effect.into.identifier.id);
2293 + if (applyInto == null || applyInto.length !== 1) {
2294 + if (DEBUG) {
2295 + console.log(`too many substitutions for into`);
2296 + }
2297 + return null;
2298 + }
2299 + const applyArgs: Array<Place | SpreadPattern | Hole> = [];
2300 + for (const arg of effect.args) {
2301 + if (arg.kind === 'Hole') {
2302 + applyArgs.push(arg);
2303 + } else if (arg.kind === 'Identifier') {
2304 + const applyArg = substitutions.get(arg.identifier.id);
2305 + if (applyArg == null || applyArg.length !== 1) {
2306 + if (DEBUG) {
2307 + console.log(`too many substitutions for arg`);
2308 + }
2309 + return null;
2310 + }
2311 + applyArgs.push(applyArg[0]);
2312 + } else {
2313 + const applyArg = substitutions.get(arg.place.identifier.id);
2314 + if (applyArg == null || applyArg.length !== 1) {
2315 + if (DEBUG) {
2316 + console.log(`too many substitutions for arg`);
2317 + }
2318 + return null;
2319 + }
2320 + applyArgs.push({kind: 'Spread', place: applyArg[0]});
2321 + }
2322 + }
2323 + effects.push({
2324 + kind: 'Apply',
2325 + mutatesFunction: effect.mutatesFunction,
2326 + receiver: applyReceiver[0],
2327 + args: applyArgs,
2328 + function: applyFunction[0],
2329 + into: applyInto[0],
2330 + signature: effect.signature,
2331 + loc,
2332 + });
2333 + break;
2334 + }
2335 + case 'CreateFunction': {
2336 + CompilerError.throwTodo({
2337 + reason: `Support CreateFrom effects in signatures`,
2338 + loc: receiver.loc,
2339 + });
2340 + }
2341 + default: {
2342 + assertExhaustive(
2343 + effect,
2344 + `Unexpected effect kind '${(effect as any).kind}'`,
2345 + );
2346 + }
2347 + }
2348 + }
2349 + return effects;
2350 +}
2351 +
2352 +function buildSignatureFromFunctionExpression(
2353 + env: Environment,
2354 + fn: FunctionExpression,
2355 +): AliasingSignature {
2356 + let rest: IdentifierId | null = null;
2357 + const params: Array<IdentifierId> = [];
2358 + for (const param of fn.loweredFunc.func.params) {
2359 + if (param.kind === 'Identifier') {
2360 + params.push(param.identifier.id);
2361 + } else {
2362 + rest = param.place.identifier.id;
2363 + }
2364 + }
2365 + return {
2366 + receiver: makeIdentifierId(0),
2367 + params,
2368 + rest: rest ?? createTemporaryPlace(env, fn.loc).identifier.id,
2369 + returns: fn.loweredFunc.func.returns.identifier.id,
2370 + effects: fn.loweredFunc.func.aliasingEffects ?? [],
2371 + temporaries: [],
2372 + };
2373 +}
2374 +
2375 +export type AbstractValue = {
2376 + kind: ValueKind;
2377 + reason: ReadonlySet<ValueReason>;
2378 +};
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingFunctionEffects.ts new
+206
@@ -0,0 +1,206 @@
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 {HIRFunction, IdentifierId, Place, ValueKind, ValueReason} from '../HIR';
9 +import {getOrInsertDefault} from '../Utils/utils';
10 +import {AliasingEffect} from './AliasingEffects';
11 +
12 +/**
13 + * This function tracks data flow within an inner function expression in order to
14 + * compute a set of data-flow aliasing effects describing data flow between the function's
15 + * params, context variables, and return value.
16 + *
17 + * For example, consider the following function expression:
18 + *
19 + * ```
20 + * (x) => { return [x, y] }
21 + * ```
22 + *
23 + * This function captures both param `x` and context variable `y` into the return value.
24 + * Unlike our previous inference which counted this as a mutation of x and y, we want to
25 + * build a signature for the function that describes the data flow. We would infer
26 + * `Capture x -> return, Capture y -> return` effects for this function.
27 + *
28 + * This function *also* propagates more ambient-style effects (MutateFrozen, MutateGlobal, Impure, Render)
29 + * from instructions within the function up to the function itself.
30 + */
31 +export function inferMutationAliasingFunctionEffects(
32 + fn: HIRFunction,
33 +): Array<AliasingEffect> | null {
34 + const effects: Array<AliasingEffect> = [];
35 +
36 + /**
37 + * Map used to identify tracked variables: params, context vars, return value
38 + * This is used to detect mutation/capturing/aliasing of params/context vars
39 + */
40 + const tracked = new Map<IdentifierId, Place>();
41 + tracked.set(fn.returns.identifier.id, fn.returns);
42 + for (const operand of [...fn.context, ...fn.params]) {
43 + const place = operand.kind === 'Identifier' ? operand : operand.place;
44 + tracked.set(place.identifier.id, place);
45 + }
46 +
47 + /**
48 + * Track capturing/aliasing of context vars and params into each other and into the return.
49 + * We don't need to track locals and intermediate values, since we're only concerned with effects
50 + * as they relate to arguments visible outside the function.
51 + *
52 + * For each aliased identifier we track capture/alias/createfrom and then merge this with how
53 + * the value is used. Eg capturing an alias => capture. See joinEffects() helper.
54 + */
55 + type AliasedIdentifier = {
56 + kind: AliasingKind;
57 + place: Place;
58 + };
59 + const dataFlow = new Map<IdentifierId, Array<AliasedIdentifier>>();
60 +
61 + /*
62 + * Check for aliasing of tracked values. Also joins the effects of how the value is
63 + * used (@param kind) with the aliasing type of each value
64 + */
65 + function lookup(
66 + place: Place,
67 + kind: AliasedIdentifier['kind'],
68 + ): Array<AliasedIdentifier> | null {
69 + if (tracked.has(place.identifier.id)) {
70 + return [{kind, place}];
71 + }
72 + return (
73 + dataFlow.get(place.identifier.id)?.map(aliased => ({
74 + kind: joinEffects(aliased.kind, kind),
75 + place: aliased.place,
76 + })) ?? null
77 + );
78 + }
79 +
80 + // todo: fixpoint
81 + for (const block of fn.body.blocks.values()) {
82 + for (const phi of block.phis) {
83 + const operands: Array<AliasedIdentifier> = [];
84 + for (const operand of phi.operands.values()) {
85 + const inputs = lookup(operand, 'Alias');
86 + if (inputs != null) {
87 + operands.push(...inputs);
88 + }
89 + }
90 + if (operands.length !== 0) {
91 + dataFlow.set(phi.place.identifier.id, operands);
92 + }
93 + }
94 + for (const instr of block.instructions) {
95 + if (instr.effects == null) continue;
96 + for (const effect of instr.effects) {
97 + if (
98 + effect.kind === 'Assign' ||
99 + effect.kind === 'Capture' ||
100 + effect.kind === 'Alias' ||
101 + effect.kind === 'CreateFrom'
102 + ) {
103 + const from = lookup(effect.from, effect.kind);
104 + if (from == null) {
105 + continue;
106 + }
107 + const into = lookup(effect.into, 'Alias');
108 + if (into == null) {
109 + getOrInsertDefault(dataFlow, effect.into.identifier.id, []).push(
110 + ...from,
111 + );
112 + } else {
113 + for (const aliased of into) {
114 + getOrInsertDefault(
115 + dataFlow,
116 + aliased.place.identifier.id,
117 + [],
118 + ).push(...from);
119 + }
120 + }
121 + } else if (
122 + effect.kind === 'Create' ||
123 + effect.kind === 'CreateFunction'
124 + ) {
125 + getOrInsertDefault(dataFlow, effect.into.identifier.id, [
126 + {kind: 'Alias', place: effect.into},
127 + ]);
128 + } else if (
129 + effect.kind === 'MutateFrozen' ||
130 + effect.kind === 'MutateGlobal' ||
131 + effect.kind === 'Impure' ||
132 + effect.kind === 'Render'
133 + ) {
134 + effects.push(effect);
135 + }
136 + }
137 + }
138 + if (block.terminal.kind === 'return') {
139 + const from = lookup(block.terminal.value, 'Alias');
140 + if (from != null) {
141 + getOrInsertDefault(dataFlow, fn.returns.identifier.id, []).push(
142 + ...from,
143 + );
144 + }
145 + }
146 + }
147 +
148 + // Create aliasing effects based on observed data flow
149 + let hasReturn = false;
150 + for (const [into, from] of dataFlow) {
151 + const input = tracked.get(into);
152 + if (input == null) {
153 + continue;
154 + }
155 + for (const aliased of from) {
156 + if (
157 + aliased.place.identifier.id === input.identifier.id ||
158 + !tracked.has(aliased.place.identifier.id)
159 + ) {
160 + continue;
161 + }
162 + const effect = {kind: aliased.kind, from: aliased.place, into: input};
163 + effects.push(effect);
164 + if (
165 + into === fn.returns.identifier.id &&
166 + (aliased.kind === 'Assign' || aliased.kind === 'CreateFrom')
167 + ) {
168 + hasReturn = true;
169 + }
170 + }
171 + }
172 + // TODO: more precise return effect inference
173 + if (!hasReturn) {
174 + effects.unshift({
175 + kind: 'Create',
176 + into: fn.returns,
177 + value:
178 + fn.returnType.kind === 'Primitive'
179 + ? ValueKind.Primitive
180 + : ValueKind.Mutable,
181 + reason: ValueReason.KnownReturnSignature,
182 + });
183 + }
184 +
185 + return effects;
186 +}
187 +
188 +export enum MutationKind {
189 + None = 0,
190 + Conditional = 1,
191 + Definite = 2,
192 +}
193 +
194 +type AliasingKind = 'Alias' | 'Capture' | 'CreateFrom' | 'Assign';
195 +function joinEffects(
196 + effect1: AliasingKind,
197 + effect2: AliasingKind,
198 +): AliasingKind {
199 + if (effect1 === 'Capture' || effect2 === 'Capture') {
200 + return 'Capture';
201 + } else if (effect1 === 'Assign' || effect2 === 'Assign') {
202 + return 'Assign';
203 + } else {
204 + return 'Alias';
205 + }
206 +}
compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingRanges.ts new
+737
@@ -0,0 +1,737 @@
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 prettyFormat from 'pretty-format';
9 +import {CompilerError, SourceLocation} from '..';
10 +import {
11 + BlockId,
12 + Effect,
13 + HIRFunction,
14 + Identifier,
15 + IdentifierId,
16 + InstructionId,
17 + makeInstructionId,
18 + Place,
19 +} from '../HIR/HIR';
20 +import {
21 + eachInstructionLValue,
22 + eachInstructionValueOperand,
23 + eachTerminalOperand,
24 +} from '../HIR/visitors';
25 +import {assertExhaustive, getOrInsertWith} from '../Utils/utils';
26 +import {printFunction} from '../HIR';
27 +import {printIdentifier, printPlace} from '../HIR/PrintHIR';
28 +import {MutationKind} from './InferMutationAliasingFunctionEffects';
29 +import {Result} from '../Utils/Result';
30 +
31 +const DEBUG = false;
32 +const VERBOSE = false;
33 +
34 +/**
35 + * Infers mutable ranges for all values in the program, using previously inferred
36 + * mutation/aliasing effects. This pass builds a data flow graph using the effects,
37 + * tracking an abstract notion of "when" each effect occurs relative to the others.
38 + * It then walks each mutation effect against the graph, updating the range of each
39 + * node that would be reachable at the "time" that the effect occurred.
40 + *
41 + * This pass also validates against invalid effects: any function that is reachable
42 + * by being called, or via a Render effect, is validated against mutating globals
43 + * or calling impure code.
44 + *
45 + * Note that this function also populates the outer function's aliasing effects with
46 + * any mutations that apply to its params or context variables. For example, a
47 + * function expression such as the following:
48 + *
49 + * ```
50 + * (x) => { x.y = true }
51 + * ```
52 + *
53 + * Would populate a `Mutate x` aliasing effect on the outer function.
54 + */
55 +export function inferMutationAliasingRanges(
56 + fn: HIRFunction,
57 + {isFunctionExpression}: {isFunctionExpression: boolean},
58 +): Result<void, CompilerError> {
59 + if (VERBOSE) {
60 + console.log();
61 + console.log(printFunction(fn));
62 + }
63 + /**
64 + * Part 1: Infer mutable ranges for values. We build an abstract model of
65 + * values, the alias/capture edges between them, and the set of mutations.
66 + * Edges and mutations are ordered, with mutations processed against the
67 + * abstract model only after it is fully constructed by visiting all blocks
68 + * _and_ connecting phis. Phis are considered ordered at the time of the
69 + * phi node.
70 + *
71 + * This should (may?) mean that mutations are able to see the full state
72 + * of the graph and mark all the appropriate identifiers as mutated at
73 + * the correct point, accounting for both backward and forward edges.
74 + * Ie a mutation of x accounts for both values that flowed into x,
75 + * and values that x flowed into.
76 + */
77 + const state = new AliasingState();
78 + type PendingPhiOperand = {from: Place; into: Place; index: number};
79 + const pendingPhis = new Map<BlockId, Array<PendingPhiOperand>>();
80 + const mutations: Array<{
81 + index: number;
82 + id: InstructionId;
83 + transitive: boolean;
84 + kind: MutationKind;
85 + place: Place;
86 + }> = [];
87 + const renders: Array<{index: number; place: Place}> = [];
88 +
89 + let index = 0;
90 +
91 + const errors = new CompilerError();
92 +
93 + for (const param of [...fn.params, ...fn.context, fn.returns]) {
94 + const place = param.kind === 'Identifier' ? param : param.place;
95 + state.create(place, {kind: 'Object'});
96 + }
97 + const seenBlocks = new Set<BlockId>();
98 + for (const block of fn.body.blocks.values()) {
99 + for (const phi of block.phis) {
100 + state.create(phi.place, {kind: 'Phi'});
101 + for (const [pred, operand] of phi.operands) {
102 + if (!seenBlocks.has(pred)) {
103 + // NOTE: annotation required to actually typecheck and not silently infer `any`
104 + const blockPhis = getOrInsertWith<BlockId, Array<PendingPhiOperand>>(
105 + pendingPhis,
106 + pred,
107 + () => [],
108 + );
109 + blockPhis.push({from: operand, into: phi.place, index: index++});
110 + } else {
111 + state.assign(index++, operand, phi.place);
112 + }
113 + }
114 + }
115 + seenBlocks.add(block.id);
116 +
117 + for (const instr of block.instructions) {
118 + if (
119 + instr.value.kind === 'FunctionExpression' ||
120 + instr.value.kind === 'ObjectMethod'
121 + ) {
122 + state.create(instr.lvalue, {
123 + kind: 'Function',
124 + function: instr.value.loweredFunc.func,
125 + });
126 + } else {
127 + for (const lvalue of eachInstructionLValue(instr)) {
128 + state.create(lvalue, {kind: 'Object'});
129 + }
130 + }
131 +
132 + if (instr.effects == null) continue;
133 + for (const effect of instr.effects) {
134 + if (effect.kind === 'Create') {
135 + state.create(effect.into, {kind: 'Object'});
136 + } else if (effect.kind === 'CreateFunction') {
137 + state.create(effect.into, {
138 + kind: 'Function',
139 + function: effect.function.loweredFunc.func,
140 + });
141 + } else if (effect.kind === 'CreateFrom') {
142 + state.createFrom(index++, effect.from, effect.into);
143 + } else if (effect.kind === 'Assign') {
144 + if (!state.nodes.has(effect.into.identifier)) {
145 + state.create(effect.into, {kind: 'Object'});
146 + }
147 + state.assign(index++, effect.from, effect.into);
148 + } else if (effect.kind === 'Alias') {
149 + state.assign(index++, effect.from, effect.into);
150 + } else if (effect.kind === 'Capture') {
151 + state.capture(index++, effect.from, effect.into);
152 + } else if (
153 + effect.kind === 'MutateTransitive' ||
154 + effect.kind === 'MutateTransitiveConditionally'
155 + ) {
156 + mutations.push({
157 + index: index++,
158 + id: instr.id,
159 + transitive: true,
160 + kind:
161 + effect.kind === 'MutateTransitive'
162 + ? MutationKind.Definite
163 + : MutationKind.Conditional,
164 + place: effect.value,
165 + });
166 + } else if (
167 + effect.kind === 'Mutate' ||
168 + effect.kind === 'MutateConditionally'
169 + ) {
170 + mutations.push({
171 + index: index++,
172 + id: instr.id,
173 + transitive: false,
174 + kind:
175 + effect.kind === 'Mutate'
176 + ? MutationKind.Definite
177 + : MutationKind.Conditional,
178 + place: effect.value,
179 + });
180 + } else if (
181 + effect.kind === 'MutateFrozen' ||
182 + effect.kind === 'MutateGlobal' ||
183 + effect.kind === 'Impure'
184 + ) {
185 + errors.push(effect.error);
186 + } else if (effect.kind === 'Render') {
187 + renders.push({index: index++, place: effect.place});
188 + }
189 + }
190 + }
191 + const blockPhis = pendingPhis.get(block.id);
192 + if (blockPhis != null) {
193 + for (const {from, into, index} of blockPhis) {
194 + state.assign(index, from, into);
195 + }
196 + }
197 + if (block.terminal.kind === 'return') {
198 + state.assign(index++, block.terminal.value, fn.returns);
199 + }
200 +
201 + if (
202 + (block.terminal.kind === 'maybe-throw' ||
203 + block.terminal.kind === 'return') &&
204 + block.terminal.effects != null
205 + ) {
206 + for (const effect of block.terminal.effects) {
207 + if (effect.kind === 'Alias') {
208 + state.assign(index++, effect.from, effect.into);
209 + } else {
210 + CompilerError.invariant(effect.kind === 'Freeze', {
211 + reason: `Unexpected '${effect.kind}' effect for MaybeThrow terminal`,
212 + loc: block.terminal.loc,
213 + });
214 + }
215 + }
216 + }
217 + }
218 +
219 + if (VERBOSE) {
220 + console.log(state.debug());
221 + console.log(pretty(mutations));
222 + }
223 + for (const mutation of mutations) {
224 + state.mutate(
225 + mutation.index,
226 + mutation.place.identifier,
227 + makeInstructionId(mutation.id + 1),
228 + mutation.transitive,
229 + mutation.kind,
230 + mutation.place.loc,
231 + errors,
232 + );
233 + }
234 + for (const render of renders) {
235 + state.render(render.index, render.place.identifier, errors);
236 + }
237 + if (DEBUG) {
238 + console.log(pretty([...state.nodes.keys()]));
239 + }
240 + fn.aliasingEffects ??= [];
241 + for (const param of [...fn.context, ...fn.params]) {
242 + const place = param.kind === 'Identifier' ? param : param.place;
243 + const node = state.nodes.get(place.identifier);
244 + if (node == null) {
245 + continue;
246 + }
247 + let mutated = false;
248 + if (node.local != null) {
249 + if (node.local.kind === MutationKind.Conditional) {
250 + mutated = true;
251 + fn.aliasingEffects.push({
252 + kind: 'MutateConditionally',
253 + value: {...place, loc: node.local.loc},
254 + });
255 + } else if (node.local.kind === MutationKind.Definite) {
256 + mutated = true;
257 + fn.aliasingEffects.push({
258 + kind: 'Mutate',
259 + value: {...place, loc: node.local.loc},
260 + });
261 + }
262 + }
263 + if (node.transitive != null) {
264 + if (node.transitive.kind === MutationKind.Conditional) {
265 + mutated = true;
266 + fn.aliasingEffects.push({
267 + kind: 'MutateTransitiveConditionally',
268 + value: {...place, loc: node.transitive.loc},
269 + });
270 + } else if (node.transitive.kind === MutationKind.Definite) {
271 + mutated = true;
272 + fn.aliasingEffects.push({
273 + kind: 'MutateTransitive',
274 + value: {...place, loc: node.transitive.loc},
275 + });
276 + }
277 + }
278 + if (mutated) {
279 + place.effect = Effect.Capture;
280 + }
281 + }
282 +
283 + /**
284 + * Part 2
285 + * Add legacy operand-specific effects based on instruction effects and mutable ranges.
286 + * Also fixes up operand mutable ranges, making sure that start is non-zero if the value
287 + * is mutated (depended on by later passes like InferReactiveScopeVariables which uses this
288 + * to filter spurious mutations of globals, which we now guard against more precisely)
289 + */
290 + for (const block of fn.body.blocks.values()) {
291 + for (const phi of block.phis) {
292 + // TODO: we don't actually set these effects today!
293 + phi.place.effect = Effect.Store;
294 + const isPhiMutatedAfterCreation: boolean =
295 + phi.place.identifier.mutableRange.end >
296 + (block.instructions.at(0)?.id ?? block.terminal.id);
297 + for (const operand of phi.operands.values()) {
298 + operand.effect = isPhiMutatedAfterCreation
299 + ? Effect.Capture
300 + : Effect.Read;
301 + }
302 + if (
303 + isPhiMutatedAfterCreation &&
304 + phi.place.identifier.mutableRange.start === 0
305 + ) {
306 + /*
307 + * TODO: ideally we'd construct a precise start range, but what really
308 + * matters is that the phi's range appears mutable (end > start + 1)
309 + * so we just set the start to the previous instruction before this block
310 + */
311 + const firstInstructionIdOfBlock =
312 + block.instructions.at(0)?.id ?? block.terminal.id;
313 + phi.place.identifier.mutableRange.start = makeInstructionId(
314 + firstInstructionIdOfBlock - 1,
315 + );
316 + }
317 + }
318 + for (const instr of block.instructions) {
319 + for (const lvalue of eachInstructionLValue(instr)) {
320 + lvalue.effect = Effect.ConditionallyMutate;
321 + if (lvalue.identifier.mutableRange.start === 0) {
322 + lvalue.identifier.mutableRange.start = instr.id;
323 + }
324 + if (lvalue.identifier.mutableRange.end === 0) {
325 + lvalue.identifier.mutableRange.end = makeInstructionId(
326 + Math.max(instr.id + 1, lvalue.identifier.mutableRange.end),
327 + );
328 + }
329 + }
330 + for (const operand of eachInstructionValueOperand(instr.value)) {
331 + operand.effect = Effect.Read;
332 + }
333 + if (instr.effects == null) {
334 + continue;
335 + }
336 + const operandEffects = new Map<IdentifierId, Effect>();
337 + for (const effect of instr.effects) {
338 + switch (effect.kind) {
339 + case 'Assign':
340 + case 'Alias':
341 + case 'Capture':
342 + case 'CreateFrom': {
343 + const isMutatedOrReassigned =
344 + effect.into.identifier.mutableRange.end > instr.id;
345 + if (isMutatedOrReassigned) {
346 + operandEffects.set(effect.from.identifier.id, Effect.Capture);
347 + operandEffects.set(effect.into.identifier.id, Effect.Store);
348 + } else {
349 + operandEffects.set(effect.from.identifier.id, Effect.Read);
350 + operandEffects.set(effect.into.identifier.id, Effect.Store);
351 + }
352 + break;
353 + }
354 + case 'CreateFunction':
355 + case 'Create': {
356 + break;
357 + }
358 + case 'Mutate': {
359 + operandEffects.set(effect.value.identifier.id, Effect.Store);
360 + break;
361 + }
362 + case 'Apply': {
363 + CompilerError.invariant(false, {
364 + reason: `[AnalyzeFunctions] Expected Apply effects to be replaced with more precise effects`,
365 + loc: effect.function.loc,
366 + });
367 + }
368 + case 'MutateTransitive':
369 + case 'MutateConditionally':
370 + case 'MutateTransitiveConditionally': {
371 + operandEffects.set(
372 + effect.value.identifier.id,
373 + Effect.ConditionallyMutate,
374 + );
375 + break;
376 + }
377 + case 'Freeze': {
378 + operandEffects.set(effect.value.identifier.id, Effect.Freeze);
379 + break;
380 + }
381 + case 'ImmutableCapture': {
382 + // no-op, Read is the default
383 + break;
384 + }
385 + case 'Impure':
386 + case 'Render':
387 + case 'MutateFrozen':
388 + case 'MutateGlobal': {
389 + // no-op
390 + break;
391 + }
392 + default: {
393 + assertExhaustive(
394 + effect,
395 + `Unexpected effect kind ${(effect as any).kind}`,
396 + );
397 + }
398 + }
399 + }
400 + for (const lvalue of eachInstructionLValue(instr)) {
401 + const effect =
402 + operandEffects.get(lvalue.identifier.id) ??
403 + Effect.ConditionallyMutate;
404 + lvalue.effect = effect;
405 + }
406 + for (const operand of eachInstructionValueOperand(instr.value)) {
407 + if (
408 + operand.identifier.mutableRange.end > instr.id &&
409 + operand.identifier.mutableRange.start === 0
410 + ) {
411 + operand.identifier.mutableRange.start = instr.id;
412 + }
413 + const effect = operandEffects.get(operand.identifier.id) ?? Effect.Read;
414 + operand.effect = effect;
415 + }
416 +
417 + /**
418 + * This case is targeted at hoisted functions like:
419 + *
420 + * ```
421 + * x();
422 + * function x() { ... }
423 + * ```
424 + *
425 + * Which turns into:
426 + *
427 + * t0 = DeclareContext HoistedFunction x
428 + * t1 = LoadContext x
429 + * t2 = CallExpression t1 ( )
430 + * t3 = FunctionExpression ...
431 + * t4 = StoreContext Function x = t3
432 + *
433 + * If the function had captured mutable values, it would already have its
434 + * range extended to include the StoreContext. But if the function doesn't
435 + * capture any mutable values its range won't have been extended yet. We
436 + * want to ensure that the value is memoized along with the context variable,
437 + * not independently of it (bc of the way we do codegen for hoisted functions).
438 + * So here we check for StoreContext rvalues and if they haven't already had
439 + * their range extended to at least this instruction, we extend it.
440 + */
441 + if (
442 + instr.value.kind === 'StoreContext' &&
443 + instr.value.value.identifier.mutableRange.end <= instr.id
444 + ) {
445 + instr.value.value.identifier.mutableRange.end = makeInstructionId(
446 + instr.id + 1,
447 + );
448 + }
449 + }
450 + if (block.terminal.kind === 'return') {
451 + block.terminal.value.effect = isFunctionExpression
452 + ? Effect.Read
453 + : Effect.Freeze;
454 + } else {
455 + for (const operand of eachTerminalOperand(block.terminal)) {
456 + operand.effect = Effect.Read;
457 + }
458 + }
459 + }
460 +
461 + if (VERBOSE) {
462 + console.log(printFunction(fn));
463 + }
464 + return errors.asResult();
465 +}
466 +
467 +function appendFunctionErrors(errors: CompilerError, fn: HIRFunction): void {
468 + for (const effect of fn.aliasingEffects ?? []) {
469 + switch (effect.kind) {
470 + case 'Impure':
471 + case 'MutateFrozen':
472 + case 'MutateGlobal': {
473 + errors.push(effect.error);
474 + break;
475 + }
476 + }
477 + }
478 +}
479 +
480 +type Node = {
481 + id: Identifier;
482 + createdFrom: Map<Identifier, number>;
483 + captures: Map<Identifier, number>;
484 + aliases: Map<Identifier, number>;
485 + edges: Array<{index: number; node: Identifier; kind: 'capture' | 'alias'}>;
486 + transitive: {kind: MutationKind; loc: SourceLocation} | null;
487 + local: {kind: MutationKind; loc: SourceLocation} | null;
488 + value:
489 + | {kind: 'Object'}
490 + | {kind: 'Phi'}
491 + | {kind: 'Function'; function: HIRFunction};
492 +};
493 +class AliasingState {
494 + nodes: Map<Identifier, Node> = new Map();
495 +
496 + create(place: Place, value: Node['value']): void {
497 + this.nodes.set(place.identifier, {
498 + id: place.identifier,
499 + createdFrom: new Map(),
500 + captures: new Map(),
501 + aliases: new Map(),
502 + edges: [],
503 + transitive: null,
504 + local: null,
505 + value,
506 + });
507 + }
508 +
509 + createFrom(index: number, from: Place, into: Place): void {
510 + this.create(into, {kind: 'Object'});
511 + const fromNode = this.nodes.get(from.identifier);
512 + const toNode = this.nodes.get(into.identifier);
513 + if (fromNode == null || toNode == null) {
514 + if (VERBOSE) {
515 + console.log(
516 + `skip: createFrom ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
517 + );
518 + }
519 + return;
520 + }
521 + fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
522 + if (!toNode.createdFrom.has(from.identifier)) {
523 + toNode.createdFrom.set(from.identifier, index);
524 + }
525 + }
526 +
527 + capture(index: number, from: Place, into: Place): void {
528 + const fromNode = this.nodes.get(from.identifier);
529 + const toNode = this.nodes.get(into.identifier);
530 + if (fromNode == null || toNode == null) {
531 + if (VERBOSE) {
532 + console.log(
533 + `skip: capture ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
534 + );
535 + }
536 + return;
537 + }
538 + fromNode.edges.push({index, node: into.identifier, kind: 'capture'});
539 + if (!toNode.captures.has(from.identifier)) {
540 + toNode.captures.set(from.identifier, index);
541 + }
542 + }
543 +
544 + assign(index: number, from: Place, into: Place): void {
545 + const fromNode = this.nodes.get(from.identifier);
546 + const toNode = this.nodes.get(into.identifier);
547 + if (fromNode == null || toNode == null) {
548 + if (VERBOSE) {
549 + console.log(
550 + `skip: assign ${printPlace(from)}${!!fromNode} -> ${printPlace(into)}${!!toNode}`,
551 + );
552 + }
553 + return;
554 + }
555 + fromNode.edges.push({index, node: into.identifier, kind: 'alias'});
556 + if (!toNode.aliases.has(from.identifier)) {
557 + toNode.aliases.set(from.identifier, index);
558 + }
559 + }
560 +
561 + render(index: number, start: Identifier, errors: CompilerError): void {
562 + const seen = new Set<Identifier>();
563 + const queue: Array<Identifier> = [start];
564 + while (queue.length !== 0) {
565 + const current = queue.pop()!;
566 + if (seen.has(current)) {
567 + continue;
568 + }
569 + seen.add(current);
570 + const node = this.nodes.get(current);
571 + if (node == null || node.transitive != null || node.local != null) {
572 + continue;
573 + }
574 + if (node.value.kind === 'Function') {
575 + appendFunctionErrors(errors, node.value.function);
576 + }
577 + for (const [alias, when] of node.createdFrom) {
578 + if (when >= index) {
579 + continue;
580 + }
581 + queue.push(alias);
582 + }
583 + for (const [alias, when] of node.aliases) {
584 + if (when >= index) {
585 + continue;
586 + }
587 + queue.push(alias);
588 + }
589 + for (const [capture, when] of node.captures) {
590 + if (when >= index) {
591 + continue;
592 + }
593 + queue.push(capture);
594 + }
595 + }
596 + }
597 +
598 + mutate(
599 + index: number,
600 + start: Identifier,
601 + end: InstructionId,
602 + transitive: boolean,
603 + kind: MutationKind,
604 + loc: SourceLocation,
605 + errors: CompilerError,
606 + ): void {
607 + if (DEBUG) {
608 + console.log(
609 + `mutate ix=${index} start=$${start.id} end=[${end}]${transitive ? ' transitive' : ''} kind=${kind}`,
610 + );
611 + }
612 + const seen = new Set<Identifier>();
613 + const queue: Array<{
614 + place: Identifier;
615 + transitive: boolean;
616 + direction: 'backwards' | 'forwards';
617 + }> = [{place: start, transitive, direction: 'backwards'}];
618 + while (queue.length !== 0) {
619 + const {place: current, transitive, direction} = queue.pop()!;
620 + if (seen.has(current)) {
621 + continue;
622 + }
623 + seen.add(current);
624 + const node = this.nodes.get(current);
625 + if (node == null) {
626 + if (DEBUG) {
627 + console.log(
628 + `no node! ${printIdentifier(start)} for identifier ${printIdentifier(current)}`,
629 + );
630 + }
631 + continue;
632 + }
633 + if (DEBUG) {
634 + console.log(
635 + ` mutate $${node.id.id} transitive=${transitive} direction=${direction}`,
636 + );
637 + }
638 + node.id.mutableRange.end = makeInstructionId(
639 + Math.max(node.id.mutableRange.end, end),
640 + );
641 + if (
642 + node.value.kind === 'Function' &&
643 + node.transitive == null &&
644 + node.local == null
645 + ) {
646 + appendFunctionErrors(errors, node.value.function);
647 + }
648 + if (transitive) {
649 + if (node.transitive == null || node.transitive.kind < kind) {
650 + node.transitive = {kind, loc};
651 + }
652 + } else {
653 + if (node.local == null || node.local.kind < kind) {
654 + node.local = {kind, loc};
655 + }
656 + }
657 + /**
658 + * all mutations affect "forward" edges by the rules:
659 + * - Capture a -> b, mutate(a) => mutate(b)
660 + * - Alias a -> b, mutate(a) => mutate(b)
661 + */
662 + for (const edge of node.edges) {
663 + if (edge.index >= index) {
664 + break;
665 + }
666 + queue.push({place: edge.node, transitive, direction: 'forwards'});
667 + }
668 + for (const [alias, when] of node.createdFrom) {
669 + if (when >= index) {
670 + continue;
671 + }
672 + queue.push({place: alias, transitive: true, direction: 'backwards'});
673 + }
674 + if (direction === 'backwards' || node.value.kind !== 'Phi') {
675 + /**
676 + * all mutations affect backward alias edges by the rules:
677 + * - Alias a -> b, mutate(b) => mutate(a)
678 + * - Alias a -> b, mutateTransitive(b) => mutate(a)
679 + *
680 + * However, if we reached a phi because one of its inputs was mutated
681 + * (and we're advancing "forwards" through that node's edges), then
682 + * we know we've already processed the mutation at its source. The
683 + * phi's other inputs can't be affected.
684 + */
685 + for (const [alias, when] of node.aliases) {
686 + if (when >= index) {
687 + continue;
688 + }
689 + queue.push({place: alias, transitive, direction: 'backwards'});
690 + }
691 + }
692 + /**
693 + * but only transitive mutations affect captures
694 + */
695 + if (transitive) {
696 + for (const [capture, when] of node.captures) {
697 + if (when >= index) {
698 + continue;
699 + }
700 + queue.push({place: capture, transitive, direction: 'backwards'});
701 + }
702 + }
703 + }
704 + if (DEBUG) {
705 + const nodes = new Map();
706 + for (const id of seen) {
707 + const node = this.nodes.get(id);
708 + nodes.set(id.id, node);
709 + }
710 + console.log(pretty(nodes));
711 + }
712 + }
713 +
714 + debug(): string {
715 + return pretty(this.nodes);
716 + }
717 +}
718 +
719 +export function pretty(v: any): string {
720 + return prettyFormat(v, {
721 + plugins: [
722 + {
723 + test: v =>
724 + v !== null && typeof v === 'object' && v.kind === 'Identifier',
725 + serialize: v => printPlace(v),
726 + },
727 + {
728 + test: v =>
729 + v !== null &&
730 + typeof v === 'object' &&
731 + typeof v.declarationId === 'number',
732 + serialize: v =>
733 + `${printIdentifier(v)}:${v.mutableRange.start}:${v.mutableRange.end}`,
734 + },
735 + ],
736 + });
737 +}
compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
+6 -18
@@ -48,7 +48,7 @@ import {
48 eachTerminalOperand,
49 eachTerminalSuccessor,
50 } from '../HIR/visitors';
51 -import {assertExhaustive} from '../Utils/utils';
51 +import {assertExhaustive, Set_isSuperset} from '../Utils/utils';
52 import {
53 inferTerminalFunctionEffects,
54 inferInstructionFunctionEffects,
@@ -779,7 +779,7 @@ function inferParam(
779 * │ Mutable │───┘
780 * └──────────────────────────┘
781 */
782 -function mergeValues(a: ValueKind, b: ValueKind): ValueKind {
782 +export function mergeValueKinds(a: ValueKind, b: ValueKind): ValueKind {
783 if (a === b) {
784 return a;
785 } else if (a === ValueKind.MaybeFrozen || b === ValueKind.MaybeFrozen) {
@@ -821,28 +821,16 @@ function mergeValues(a: ValueKind, b: ValueKind): ValueKind {
821 }
822 }
823
824 -/**
825 - * @returns `true` if `a` is a superset of `b`.
826 - */
827 -function isSuperset<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean {
828 - for (const v of b) {
829 - if (!a.has(v)) {
830 - return false;
831 - }
832 - }
833 - return true;
834 -}
835 -
824 function mergeAbstractValues(
825 a: AbstractValue,
826 b: AbstractValue,
827 ): AbstractValue {
840 - const kind = mergeValues(a.kind, b.kind);
828 + const kind = mergeValueKinds(a.kind, b.kind);
829 if (
830 kind === a.kind &&
831 kind === b.kind &&
844 - isSuperset(a.reason, b.reason) &&
845 - isSuperset(a.context, b.context)
832 + Set_isSuperset(a.reason, b.reason) &&
833 + Set_isSuperset(a.context, b.context)
834 ) {
835 return a;
836 }
@@ -1989,7 +1977,7 @@ function areArgumentsImmutableAndNonMutating(
1977 return true;
1978 }
1979
1992 -function getArgumentEffect(
1980 +export function getArgumentEffect(
1981 signatureEffect: Effect | null,
1982 arg: Place | SpreadPattern,
1983 ): Effect {
compiler/packages/babel-plugin-react-compiler/src/Inference/InlineImmediatelyInvokedFunctionExpressions.ts
+2
@@ -242,6 +242,7 @@ function rewriteBlock(
242 type: null,
243 loc: terminal.loc,
244 },
245 + effects: null,
246 });
247 block.terminal = {
248 kind: 'goto',
@@ -270,5 +271,6 @@ function declareTemporary(
271 type: null,
272 loc: result.loc,
273 },
274 + effects: null,
275 });
276 }
compiler/packages/babel-plugin-react-compiler/src/Optimization/InlineJsxTransform.ts
+14
@@ -151,6 +151,7 @@ export function inlineJsxTransform(
151 type: null,
152 loc: instr.value.loc,
153 },
154 + effects: null,
155 loc: instr.loc,
156 };
157 currentBlockInstructions.push(varInstruction);
@@ -167,6 +168,7 @@ export function inlineJsxTransform(
168 },
169 loc: instr.value.loc,
170 },
171 + effects: null,
172 loc: instr.loc,
173 };
174 currentBlockInstructions.push(devGlobalInstruction);
@@ -220,6 +222,7 @@ export function inlineJsxTransform(
222 type: null,
223 loc: instr.value.loc,
224 },
225 + effects: null,
226 loc: instr.loc,
227 };
228 thenBlockInstructions.push(reassignElseInstruction);
@@ -292,6 +295,7 @@ export function inlineJsxTransform(
295 ],
296 loc: instr.value.loc,
297 },
298 + effects: null,
299 loc: instr.loc,
300 };
301 elseBlockInstructions.push(reactElementInstruction);
@@ -309,6 +313,7 @@ export function inlineJsxTransform(
313 type: null,
314 loc: instr.value.loc,
315 },
316 + effects: null,
317 loc: instr.loc,
318 };
319 elseBlockInstructions.push(reassignConditionalInstruction);
@@ -436,6 +441,7 @@ function createSymbolProperty(
441 binding: {kind: 'Global', name: 'Symbol'},
442 loc: instr.value.loc,
443 },
444 + effects: null,
445 loc: instr.loc,
446 };
447 nextInstructions.push(symbolInstruction);
@@ -450,6 +456,7 @@ function createSymbolProperty(
456 property: makePropertyLiteral('for'),
457 loc: instr.value.loc,
458 },
459 + effects: null,
460 loc: instr.loc,
461 };
462 nextInstructions.push(symbolForInstruction);
@@ -463,6 +470,7 @@ function createSymbolProperty(
470 value: symbolName,
471 loc: instr.value.loc,
472 },
473 + effects: null,
474 loc: instr.loc,
475 };
476 nextInstructions.push(symbolValueInstruction);
@@ -478,6 +486,7 @@ function createSymbolProperty(
486 args: [symbolValueInstruction.lvalue],
487 loc: instr.value.loc,
488 },
489 + effects: null,
490 loc: instr.loc,
491 };
492 const $$typeofProperty: ObjectProperty = {
@@ -508,6 +517,7 @@ function createTagProperty(
517 value: componentTag.name,
518 loc: instr.value.loc,
519 },
520 + effects: null,
521 loc: instr.loc,
522 };
523 tagProperty = {
@@ -634,6 +644,7 @@ function createPropsProperties(
644 elements: [...children],
645 loc: instr.value.loc,
646 },
647 + effects: null,
648 loc: instr.loc,
649 };
650 nextInstructions.push(childrenPropInstruction);
@@ -657,6 +668,7 @@ function createPropsProperties(
668 value: null,
669 loc: instr.value.loc,
670 },
671 + effects: null,
672 loc: instr.loc,
673 };
674 refProperty = {
@@ -678,6 +690,7 @@ function createPropsProperties(
690 value: null,
691 loc: instr.value.loc,
692 },
693 + effects: null,
694 loc: instr.loc,
695 };
696 keyProperty = {
@@ -711,6 +724,7 @@ function createPropsProperties(
724 properties: props,
725 loc: instr.value.loc,
726 },
727 + effects: null,
728 loc: instr.loc,
729 };
730 propsProperty = {
compiler/packages/babel-plugin-react-compiler/src/Optimization/LowerContextAccess.ts
+7
@@ -146,6 +146,7 @@ function emitLoadLoweredContextCallee(
146 id: makeInstructionId(0),
147 loc: GeneratedSource,
148 lvalue: createTemporaryPlace(env, GeneratedSource),
149 + effects: null,
150 value: loadGlobal,
151 };
152 }
@@ -192,6 +193,7 @@ function emitPropertyLoad(
193 lvalue: object,
194 value: loadObj,
195 id: makeInstructionId(0),
196 + effects: null,
197 loc: GeneratedSource,
198 };
199
@@ -206,6 +208,7 @@ function emitPropertyLoad(
208 lvalue: element,
209 value: loadProp,
210 id: makeInstructionId(0),
211 + effects: null,
212 loc: GeneratedSource,
213 };
214 return {
@@ -237,6 +240,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
240 kind: 'return',
241 loc: GeneratedSource,
242 value: arrayInstr.lvalue,
243 + effects: null,
244 },
245 preds: new Set(),
246 phis: new Set(),
@@ -250,6 +254,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
254 params: [obj],
255 returnTypeAnnotation: null,
256 returnType: makeType(),
257 + returns: createTemporaryPlace(env, GeneratedSource),
258 context: [],
259 effects: null,
260 body: {
@@ -278,6 +283,7 @@ function emitSelectorFn(env: Environment, keys: Array<string>): Instruction {
283 loc: GeneratedSource,
284 },
285 lvalue: createTemporaryPlace(env, GeneratedSource),
286 + effects: null,
287 loc: GeneratedSource,
288 };
289 return fnInstr;
@@ -294,6 +300,7 @@ function emitArrayInstr(elements: Array<Place>, env: Environment): Instruction {
300 id: makeInstructionId(0),
301 value: array,
302 lvalue: arrayLvalue,
303 + effects: null,
304 loc: GeneratedSource,
305 };
306 return arrayInstr;
compiler/packages/babel-plugin-react-compiler/src/Optimization/OutlineJsx.ts
+5
@@ -297,6 +297,7 @@ function emitOutlinedJsx(
297 },
298 loc: GeneratedSource,
299 },
300 + effects: null,
301 };
302 promoteTemporaryJsxTag(loadJsx.lvalue.identifier);
303 const jsxExpr: Instruction = {
@@ -312,6 +313,7 @@ function emitOutlinedJsx(
313 openingLoc: GeneratedSource,
314 closingLoc: GeneratedSource,
315 },
316 + effects: null,
317 };
318
319 return [loadJsx, jsxExpr];
@@ -353,6 +355,7 @@ function emitOutlinedFn(
355 kind: 'return',
356 loc: GeneratedSource,
357 value: instructions.at(-1)!.lvalue,
358 + effects: null,
359 },
360 preds: new Set(),
361 phis: new Set(),
@@ -366,6 +369,7 @@ function emitOutlinedFn(
369 params: [propsObj],
370 returnTypeAnnotation: null,
371 returnType: makeType(),
372 + returns: createTemporaryPlace(env, GeneratedSource),
373 context: [],
374 effects: null,
375 body: {
@@ -517,6 +521,7 @@ function emitDestructureProps(
521 loc: GeneratedSource,
522 value: propsObj,
523 },
524 + effects: null,
525 };
526 return destructurePropsInstr;
527 }
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts
+2 -2
@@ -44,7 +44,7 @@ import {
44 getHookKind,
45 makeIdentifierName,
46 } from '../HIR/HIR';
47 -import {printIdentifier, printPlace} from '../HIR/PrintHIR';
47 +import {printIdentifier, printInstruction, printPlace} from '../HIR/PrintHIR';
48 import {eachPatternOperand} from '../HIR/visitors';
49 import {Err, Ok, Result} from '../Utils/Result';
50 import {GuardKind} from '../Utils/RuntimeDiagnosticConstants';
@@ -1310,7 +1310,7 @@ function codegenInstructionNullable(
1310 });
1311 CompilerError.invariant(value?.type === 'FunctionExpression', {
1312 reason: 'Expected a function as a function declaration value',
1313 - description: null,
1313 + description: `Got ${value == null ? String(value) : value.type} at ${printInstruction(instr)}`,
1314 loc: instr.value.loc,
1315 suggestions: null,
1316 });
compiler/packages/babel-plugin-react-compiler/src/Transform/TransformFire.ts
+4
@@ -436,6 +436,7 @@ function makeLoadUseFireInstruction(
436 value: instrValue,
437 lvalue: {...useFirePlace},
438 loc: GeneratedSource,
439 + effects: null,
440 };
441 }
442
@@ -460,6 +461,7 @@ function makeLoadFireCalleeInstruction(
461 },
462 lvalue: {...loadedFireCallee},
463 loc: GeneratedSource,
464 + effects: null,
465 };
466 }
467
@@ -483,6 +485,7 @@ function makeCallUseFireInstruction(
485 value: useFireCall,
486 lvalue: {...useFireCallResultPlace},
487 loc: GeneratedSource,
488 + effects: null,
489 };
490 }
491
@@ -511,6 +514,7 @@ function makeStoreUseFireInstruction(
514 },
515 lvalue: fireFunctionBindingLValuePlace,
516 loc: GeneratedSource,
517 + effects: null,
518 };
519 }
520
compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts
+15
@@ -121,6 +121,21 @@ export function Set_intersect<T>(sets: Array<ReadonlySet<T>>): Set<T> {
121 return result;
122 }
123
124 +/**
125 + * @returns `true` if `a` is a superset of `b`.
126 + */
127 +export function Set_isSuperset<T>(
128 + a: ReadonlySet<T>,
129 + b: ReadonlySet<T>,
130 +): boolean {
131 + for (const v of b) {
132 + if (!a.has(v)) {
133 + return false;
134 + }
135 + }
136 + return true;
137 +}
138 +
139 export function Iterable_some<T>(
140 iter: Iterable<T>,
141 pred: (item: T) => boolean,
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoFreezingKnownMutableFunctions.ts
+50 -2
@@ -58,8 +58,7 @@ export function validateNoFreezingKnownMutableFunctions(
58 const effect = contextMutationEffects.get(operand.identifier.id);
59 if (effect != null) {
60 errors.push({
61 - reason: `This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update`,
62 - description: `Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables`,
61 + reason: `This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead`,
62 loc: operand.loc,
63 severity: ErrorSeverity.InvalidReact,
64 });
@@ -112,6 +111,55 @@ export function validateNoFreezingKnownMutableFunctions(
111 );
112 if (knownMutation && knownMutation.kind === 'ContextMutation') {
113 contextMutationEffects.set(lvalue.identifier.id, knownMutation);
114 + } else if (
115 + fn.env.config.enableNewMutationAliasingModel &&
116 + value.loweredFunc.func.aliasingEffects != null
117 + ) {
118 + const context = new Set(
119 + value.loweredFunc.func.context.map(p => p.identifier.id),
120 + );
121 + effects: for (const effect of value.loweredFunc.func
122 + .aliasingEffects) {
123 + switch (effect.kind) {
124 + case 'Mutate':
125 + case 'MutateTransitive': {
126 + const knownMutation = contextMutationEffects.get(
127 + effect.value.identifier.id,
128 + );
129 + if (knownMutation != null) {
130 + contextMutationEffects.set(
131 + lvalue.identifier.id,
132 + knownMutation,
133 + );
134 + } else if (
135 + context.has(effect.value.identifier.id) &&
136 + !isRefOrRefLikeMutableType(effect.value.identifier.type)
137 + ) {
138 + contextMutationEffects.set(lvalue.identifier.id, {
139 + kind: 'ContextMutation',
140 + effect: Effect.Mutate,
141 + loc: effect.value.loc,
142 + places: new Set([effect.value]),
143 + });
144 + break effects;
145 + }
146 + break;
147 + }
148 + case 'MutateConditionally':
149 + case 'MutateTransitiveConditionally': {
150 + const knownMutation = contextMutationEffects.get(
151 + effect.value.identifier.id,
152 + );
153 + if (knownMutation != null) {
154 + contextMutationEffects.set(
155 + lvalue.identifier.id,
156 + knownMutation,
157 + );
158 + }
159 + break;
160 + }
161 + }
162 + }
163 }
164 break;
165 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.expect.md
+1 -1
@@ -2,7 +2,7 @@
2 ## Input
3
4 ```javascript
5 -// @flow @enableTransitivelyFreezeFunctionExpressions:false
5 +// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
6 import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
7
8 /**
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-aliased-mutate.js
+1 -1
@@ -1,4 +1,4 @@
1 -// @flow @enableTransitivelyFreezeFunctionExpressions:false
1 +// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
2 import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
3
4 /**
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.expect.md
+1 -1
@@ -2,7 +2,7 @@
2 ## Input
3
4 ```javascript
5 -// @flow @enableTransitivelyFreezeFunctionExpressions:false
5 +// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
6 import {setPropertyByKey, Stringify} from 'shared-runtime';
7
8 /**
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-aliased-capture-mutate.js
+1 -1
@@ -1,4 +1,4 @@
1 -// @flow @enableTransitivelyFreezeFunctionExpressions:false
1 +// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel:false
2 import {setPropertyByKey, Stringify} from 'shared-runtime';
3
4 /**
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.expect.md
+2 -1
@@ -2,6 +2,7 @@
2 ## Input
3
4 ```javascript
5 +// @enableNewMutationAliasingModel:false
6 import {makeArray, mutate} from 'shared-runtime';
7
8 /**
@@ -56,7 +57,7 @@ export const FIXTURE_ENTRYPOINT = {
57 ## Code
58
59 ```javascript
59 -import { c as _c } from "react/compiler-runtime";
60 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
61 import { makeArray, mutate } from "shared-runtime";
62
63 /**
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-capturing-func-maybealias-captured-mutate.ts
+1
@@ -1,3 +1,4 @@
1 +// @enableNewMutationAliasingModel:false
2 import {makeArray, mutate} from 'shared-runtime';
3
4 /**
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.expect.md
+2 -1
@@ -2,6 +2,7 @@
2 ## Input
3
4 ```javascript
5 +// @enableNewMutationAliasingModel:false
6 import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
7
8 /**
@@ -38,7 +39,7 @@ export const FIXTURE_ENTRYPOINT = {
39 ## Code
40
41 ```javascript
41 -import { c as _c } from "react/compiler-runtime";
42 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
43 import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime";
44
45 /**
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.tsx
+1
@@ -1,3 +1,4 @@
1 +// @enableNewMutationAliasingModel:false
2 import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
3
4 /**
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.expect.md
+2 -1
@@ -2,6 +2,7 @@
2 ## Input
3
4 ```javascript
5 +// @enableNewMutationAliasingModel:false
6 import {identity, mutate} from 'shared-runtime';
7
8 /**
@@ -39,7 +40,7 @@ export const FIXTURE_ENTRYPOINT = {
40 ## Code
41
42 ```javascript
42 -import { c as _c } from "react/compiler-runtime";
43 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
44 import { identity, mutate } from "shared-runtime";
45
46 /**
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.js
+1
@@ -1,3 +1,4 @@
1 +// @enableNewMutationAliasingModel:false
2 import {identity, mutate} from 'shared-runtime';
3
4 /**
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-separate-memoization-due-to-callback-capturing.expect.md new
+138
@@ -0,0 +1,138 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel:false
6 +import {ValidateMemoization} from 'shared-runtime';
7 +
8 +const Codes = {
9 + en: {name: 'English'},
10 + ja: {name: 'Japanese'},
11 + ko: {name: 'Korean'},
12 + zh: {name: 'Chinese'},
13 +};
14 +
15 +function Component(a) {
16 + let keys;
17 + if (a) {
18 + keys = Object.keys(Codes);
19 + } else {
20 + return null;
21 + }
22 + const options = keys.map(code => {
23 + const country = Codes[code];
24 + return {
25 + name: country.name,
26 + code,
27 + };
28 + });
29 + return (
30 + <>
31 + <ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
32 + <ValidateMemoization
33 + inputs={[]}
34 + output={options}
35 + onlyCheckCompiled={true}
36 + />
37 + </>
38 + );
39 +}
40 +
41 +export const FIXTURE_ENTRYPOINT = {
42 + fn: Component,
43 + params: [{a: false}],
44 + sequentialRenders: [
45 + {a: false},
46 + {a: true},
47 + {a: true},
48 + {a: false},
49 + {a: true},
50 + {a: false},
51 + ],
52 +};
53 +
54 +```
55 +
56 +## Code
57 +
58 +```javascript
59 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel:false
60 +import { ValidateMemoization } from "shared-runtime";
61 +
62 +const Codes = {
63 + en: { name: "English" },
64 + ja: { name: "Japanese" },
65 + ko: { name: "Korean" },
66 + zh: { name: "Chinese" },
67 +};
68 +
69 +function Component(a) {
70 + const $ = _c(4);
71 + let keys;
72 + if (a) {
73 + let t0;
74 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
75 + t0 = Object.keys(Codes);
76 + $[0] = t0;
77 + } else {
78 + t0 = $[0];
79 + }
80 + keys = t0;
81 + } else {
82 + return null;
83 + }
84 + let t0;
85 + if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
86 + t0 = keys.map(_temp);
87 + $[1] = t0;
88 + } else {
89 + t0 = $[1];
90 + }
91 + const options = t0;
92 + let t1;
93 + if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
94 + t1 = (
95 + <ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
96 + );
97 + $[2] = t1;
98 + } else {
99 + t1 = $[2];
100 + }
101 + let t2;
102 + if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
103 + t2 = (
104 + <>
105 + {t1}
106 + <ValidateMemoization
107 + inputs={[]}
108 + output={options}
109 + onlyCheckCompiled={true}
110 + />
111 + </>
112 + );
113 + $[3] = t2;
114 + } else {
115 + t2 = $[3];
116 + }
117 + return t2;
118 +}
119 +function _temp(code) {
120 + const country = Codes[code];
121 + return { name: country.name, code };
122 +}
123 +
124 +export const FIXTURE_ENTRYPOINT = {
125 + fn: Component,
126 + params: [{ a: false }],
127 + sequentialRenders: [
128 + { a: false },
129 + { a: true },
130 + { a: true },
131 + { a: false },
132 + { a: true },
133 + { a: false },
134 + ],
135 +};
136 +
137 +```
138 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-separate-memoization-due-to-callback-capturing.js new
+48
@@ -0,0 +1,48 @@
1 +// @enableNewMutationAliasingModel:false
2 +import {ValidateMemoization} from 'shared-runtime';
3 +
4 +const Codes = {
5 + en: {name: 'English'},
6 + ja: {name: 'Japanese'},
7 + ko: {name: 'Korean'},
8 + zh: {name: 'Chinese'},
9 +};
10 +
11 +function Component(a) {
12 + let keys;
13 + if (a) {
14 + keys = Object.keys(Codes);
15 + } else {
16 + return null;
17 + }
18 + const options = keys.map(code => {
19 + const country = Codes[code];
20 + return {
21 + name: country.name,
22 + code,
23 + };
24 + });
25 + return (
26 + <>
27 + <ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
28 + <ValidateMemoization
29 + inputs={[]}
30 + output={options}
31 + onlyCheckCompiled={true}
32 + />
33 + </>
34 + );
35 +}
36 +
37 +export const FIXTURE_ENTRYPOINT = {
38 + fn: Component,
39 + params: [{a: false}],
40 + sequentialRenders: [
41 + {a: false},
42 + {a: true},
43 + {a: true},
44 + {a: false},
45 + {a: true},
46 + {a: false},
47 + ],
48 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.expect.md
+8 -7
@@ -2,6 +2,7 @@
2 ## Input
3
4 ```javascript
5 +// @enableNewMutationAliasingModel:false
6 function Component() {
7 const foo = () => {
8 someGlobal = true;
@@ -15,13 +16,13 @@ function Component() {
16 ## Error
17
18 ```
18 - 1 | function Component() {
19 - 2 | const foo = () => {
20 -> 3 | someGlobal = true;
21 - | ^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (3:3)
22 - 4 | };
23 - 5 | return <div {...foo} />;
24 - 6 | }
19 + 2 | function Component() {
20 + 3 | const foo = () => {
21 +> 4 | someGlobal = true;
22 + | ^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (4:4)
23 + 5 | };
24 + 6 | return <div {...foo} />;
25 + 7 | }
26 ```
27
28
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.assign-global-in-jsx-spread-attribute.js
+1
@@ -1,3 +1,4 @@
1 +// @enableNewMutationAliasingModel:false
2 function Component() {
3 const foo = () => {
4 someGlobal = true;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.expect.md new
+58
@@ -0,0 +1,58 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
6 +
7 +import {useCallback, useEffect, useRef} from 'react';
8 +import {useHook} from 'shared-runtime';
9 +
10 +function Component() {
11 + const params = useHook();
12 + const update = useCallback(
13 + partialParams => {
14 + const nextParams = {
15 + ...params,
16 + ...partialParams,
17 + };
18 + nextParams.param = 'value';
19 + console.log(nextParams);
20 + },
21 + [params]
22 + );
23 + const ref = useRef(null);
24 + useEffect(() => {
25 + if (ref.current === null) {
26 + update();
27 + }
28 + }, [update]);
29 +
30 + return 'ok';
31 +}
32 +
33 +```
34 +
35 +
36 +## Error
37 +
38 +```
39 + 18 | );
40 + 19 | const ref = useRef(null);
41 +> 20 | useEffect(() => {
42 + | ^^^^^^^
43 +> 21 | if (ref.current === null) {
44 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
45 +> 22 | update();
46 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
47 +> 23 | }
48 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49 +> 24 | }, [update]);
50 + | ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (20:24)
51 +
52 +InvalidReact: The function modifies a local variable here (14:14)
53 + 25 |
54 + 26 | return 'ok';
55 + 27 | }
56 +```
57 +
58 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-old-inference-false-positive-ref-validation-in-use-effect.js new
+27
@@ -0,0 +1,27 @@
1 +// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel:false
2 +
3 +import {useCallback, useEffect, useRef} from 'react';
4 +import {useHook} from 'shared-runtime';
5 +
6 +function Component() {
7 + const params = useHook();
8 + const update = useCallback(
9 + partialParams => {
10 + const nextParams = {
11 + ...params,
12 + ...partialParams,
13 + };
14 + nextParams.param = 'value';
15 + console.log(nextParams);
16 + },
17 + [params]
18 + );
19 + const ref = useRef(null);
20 + useEffect(() => {
21 + if (ref.current === null) {
22 + update();
23 + }
24 + }, [update]);
25 +
26 + return 'ok';
27 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hoisting-setstate.expect.md renamed
+12 -39
@@ -2,6 +2,7 @@
2 ## Input
3
4 ```javascript
5 +// @enableNewMutationAliasingModel
6 import {useEffect, useState} from 'react';
7 import {Stringify} from 'shared-runtime';
8
@@ -33,45 +34,17 @@ export const FIXTURE_ENTRYPOINT = {
34
35 ```
36
36 -## Code
37
38 -```javascript
39 -import { c as _c } from "react/compiler-runtime";
40 -import { useEffect, useState } from "react";
41 -import { Stringify } from "shared-runtime";
42 -
43 -function Foo() {
44 - const $ = _c(3);
45 - let t0;
46 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
47 - t0 = [];
48 - $[0] = t0;
49 - } else {
50 - t0 = $[0];
51 - }
52 - useEffect(() => setState(2), t0);
53 -
54 - const [state, t1] = useState(0);
55 - const setState = t1;
56 - let t2;
57 - if ($[1] !== state) {
58 - t2 = <Stringify state={state} />;
59 - $[1] = state;
60 - $[2] = t2;
61 - } else {
62 - t2 = $[2];
63 - }
64 - return t2;
65 -}
66 -
67 -export const FIXTURE_ENTRYPOINT = {
68 - fn: Foo,
69 - params: [{}],
70 - sequentialRenders: [{}, {}],
71 -};
38 +## Error
39
40 ```
74 -
75 -### Eval output
76 -(kind: ok) <div>{"state":2}</div>
77 -<div>{"state":2}</div>
\ No newline at end of file
41 + 19 | useEffect(() => setState(2), []);
42 + 20 |
43 +> 21 | const [state, setState] = useState(0);
44 + | ^^^^^^^^ InvalidReact: Updating a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the mutation before calling useEffect(). Found mutation of `setState` (21:21)
45 + 22 | return <Stringify state={state} />;
46 + 23 | }
47 + 24 |
48 +```
49 +
50 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hoisting-setstate.js renamed
+1
@@ -1,3 +1,4 @@
1 +// @enableNewMutationAliasingModel
2 import {useEffect, useState} from 'react';
3 import {Stringify} from 'shared-runtime';
4
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-hook-function-argument-mutates-local-variable.expect.md
+1 -1
@@ -24,7 +24,7 @@ function useFoo() {
24 > 6 | cache.set('key', 'value');
25 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
26 > 7 | });
27 - | ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (5:7)
27 + | ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (5:7)
28
29 InvalidReact: The function modifies a local variable here (6:6)
30 8 | }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-captures-context-variable.expect.md new
+62
@@ -0,0 +1,62 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +import {Stringify, useIdentity} from 'shared-runtime';
7 +
8 +function Component({prop1, prop2}) {
9 + 'use memo';
10 +
11 + const data = useIdentity(
12 + new Map([
13 + [0, 'value0'],
14 + [1, 'value1'],
15 + ])
16 + );
17 + let i = 0;
18 + const items = [];
19 + items.push(
20 + <Stringify
21 + key={i}
22 + onClick={() => data.get(i) + prop1}
23 + shouldInvokeFns={true}
24 + />
25 + );
26 + i = i + 1;
27 + items.push(
28 + <Stringify
29 + key={i}
30 + onClick={() => data.get(i) + prop2}
31 + shouldInvokeFns={true}
32 + />
33 + );
34 + return <>{items}</>;
35 +}
36 +
37 +export const FIXTURE_ENTRYPOINT = {
38 + fn: Component,
39 + params: [{prop1: 'prop1', prop2: 'prop2'}],
40 + sequentialRenders: [
41 + {prop1: 'prop1', prop2: 'prop2'},
42 + {prop1: 'prop1', prop2: 'prop2'},
43 + {prop1: 'changed', prop2: 'prop2'},
44 + ],
45 +};
46 +
47 +```
48 +
49 +
50 +## Error
51 +
52 +```
53 + 20 | />
54 + 21 | );
55 +> 22 | i = i + 1;
56 + | ^ InvalidReact: Updating a value used previously in JSX is not allowed. Consider moving the mutation before the JSX. Found mutation of `i` (22:22)
57 + 23 | items.push(
58 + 24 | <Stringify
59 + 25 | key={i}
60 +```
61 +
62 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-jsx-captures-context-variable.js renamed
+1
@@ -1,3 +1,4 @@
1 +// @enableNewMutationAliasingModel
2 import {Stringify, useIdentity} from 'shared-runtime';
3
4 function Component({prop1, prop2}) {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-pass-mutable-function-as-prop.expect.md
+1 -1
@@ -20,7 +20,7 @@ function Component() {
20 5 | cache.set('key', 'value');
21 6 | };
22 > 7 | return <Foo fn={fn} />;
23 - | ^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:7)
23 + | ^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (7:7)
24
25 InvalidReact: The function modifies a local variable here (5:5)
26 8 | }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-return-mutable-function-from-hook.expect.md
+1 -1
@@ -26,7 +26,7 @@ function useFoo() {
26 > 8 | cache.set('key', 'value');
27 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28 > 9 | };
29 - | ^^^^ InvalidReact: This argument is a function which modifies local variables when called, which can bypass memoization and cause the UI not to update. Functions that are returned from hooks, passed as arguments to hooks, or passed as props to components may not mutate local variables (7:9)
29 + | ^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (7:9)
30
31 InvalidReact: The function modifies a local variable here (8:8)
32 10 | }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.expect.md new
+92
@@ -0,0 +1,92 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @flow @enableNewMutationAliasingModel
6 +/**
7 + * This hook returns a function that when called with an input object,
8 + * will return the result of mapping that input with the supplied map
9 + * function. Results are cached, so if the same input is passed again,
10 + * the same output object will be returned.
11 + *
12 + * Note that this technically violates the rules of React and is unsafe:
13 + * hooks must return immutable objects and be pure, and a function which
14 + * captures and mutates a value when called is inherently not pure.
15 + *
16 + * However, in this case it is technically safe _if_ the mapping function
17 + * is pure *and* the resulting objects are never modified. This is because
18 + * the function only caches: the result of `returnedFunction(someInput)`
19 + * strictly depends on `returnedFunction` and `someInput`, and cannot
20 + * otherwise change over time.
21 + */
22 +hook useMemoMap<TInput: interface {}, TOutput>(
23 + map: TInput => TOutput
24 +): TInput => TOutput {
25 + return useMemo(() => {
26 + // The original issue is that `cache` was not memoized together with the returned
27 + // function. This was because neither appears to ever be mutated — the function
28 + // is known to mutate `cache` but the function isn't called.
29 + //
30 + // The fix is to detect cases like this — functions that are mutable but not called -
31 + // and ensure that their mutable captures are aliased together into the same scope.
32 + const cache = new WeakMap<TInput, TOutput>();
33 + return input => {
34 + let output = cache.get(input);
35 + if (output == null) {
36 + output = map(input);
37 + cache.set(input, output);
38 + }
39 + return output;
40 + };
41 + }, [map]);
42 +}
43 +
44 +```
45 +
46 +
47 +## Error
48 +
49 +```
50 + 19 | map: TInput => TOutput
51 + 20 | ): TInput => TOutput {
52 +> 21 | return useMemo(() => {
53 + | ^^^^^^^^^^^^^^^
54 +> 22 | // The original issue is that `cache` was not memoized together with the returned
55 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
56 +> 23 | // function. This was because neither appears to ever be mutated — the function
57 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58 +> 24 | // is known to mutate `cache` but the function isn't called.
59 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
60 +> 25 | //
61 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62 +> 26 | // The fix is to detect cases like this — functions that are mutable but not called -
63 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
64 +> 27 | // and ensure that their mutable captures are aliased together into the same scope.
65 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66 +> 28 | const cache = new WeakMap<TInput, TOutput>();
67 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
68 +> 29 | return input => {
69 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
70 +> 30 | let output = cache.get(input);
71 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
72 +> 31 | if (output == null) {
73 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
74 +> 32 | output = map(input);
75 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
76 +> 33 | cache.set(input, output);
77 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
78 +> 34 | }
79 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80 +> 35 | return output;
81 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
82 +> 36 | };
83 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
84 +> 37 | }, [map]);
85 + | ^^^^^^^^^^^^ InvalidReact: This argument is a function which may reassign or mutate local variables after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead (21:37)
86 +
87 +InvalidReact: The function modifies a local variable here (33:33)
88 + 38 | }
89 + 39 |
90 +```
91 +
92 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.js renamed
+1 -1
@@ -1,4 +1,4 @@
1 -// @flow
1 +// @flow @enableNewMutationAliasingModel
2 /**
3 * This hook returns a function that when called with an input object,
4 * will return the result of mapping that input with the supplied map
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.mutable-range-shared-inner-outer-function.expect.md
+1 -1
@@ -18,7 +18,7 @@ function Component(props) {
18 a.property = true;
19 b.push(false);
20 };
21 - return <div onClick={f()} />;
21 + return <div onClick={f} />;
22 }
23
24 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.mutable-range-shared-inner-outer-function.js
+1 -1
@@ -14,7 +14,7 @@ function Component(props) {
14 a.property = true;
15 b.push(false);
16 };
17 - return <div onClick={f()} />;
17 + return <div onClick={f} />;
18 }
19
20 export const FIXTURE_ENTRYPOINT = {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.expect.md
+8 -7
@@ -2,6 +2,7 @@
2 ## Input
3
4 ```javascript
5 +// @enableNewMutationAliasingModel:false
6 function Foo() {
7 const x = () => {
8 window.href = 'foo';
@@ -21,13 +22,13 @@ export const FIXTURE_ENTRYPOINT = {
22 ## Error
23
24 ```
24 - 1 | function Foo() {
25 - 2 | const x = () => {
26 -> 3 | window.href = 'foo';
27 - | ^^^^^^ InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (3:3)
28 - 4 | };
29 - 5 | const y = {x};
30 - 6 | return <Bar y={y} />;
25 + 2 | function Foo() {
26 + 3 | const x = () => {
27 +> 4 | window.href = 'foo';
28 + | ^^^^^^ InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (4:4)
29 + 5 | };
30 + 6 | const y = {x};
31 + 7 | return <Bar y={y} />;
32 ```
33
34
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.object-capture-global-mutation.js
+1
@@ -1,3 +1,4 @@
1 +// @enableNewMutationAliasingModel:false
2 function Foo() {
3 const x = () => {
4 window.href = 'foo';
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md
+1 -1
@@ -22,7 +22,7 @@ function Component(props) {
22 7 | return hasErrors;
23 8 | }
24 > 9 | return hasErrors();
25 - | ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$14 (9:9)
25 + | ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$15 (9:9)
26 10 | }
27 11 |
28 ```
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-captures-context-variable.expect.md deleted
-129
@@ -1,129 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -import {Stringify, useIdentity} from 'shared-runtime';
6 -
7 -function Component({prop1, prop2}) {
8 - 'use memo';
9 -
10 - const data = useIdentity(
11 - new Map([
12 - [0, 'value0'],
13 - [1, 'value1'],
14 - ])
15 - );
16 - let i = 0;
17 - const items = [];
18 - items.push(
19 - <Stringify
20 - key={i}
21 - onClick={() => data.get(i) + prop1}
22 - shouldInvokeFns={true}
23 - />
24 - );
25 - i = i + 1;
26 - items.push(
27 - <Stringify
28 - key={i}
29 - onClick={() => data.get(i) + prop2}
30 - shouldInvokeFns={true}
31 - />
32 - );
33 - return <>{items}</>;
34 -}
35 -
36 -export const FIXTURE_ENTRYPOINT = {
37 - fn: Component,
38 - params: [{prop1: 'prop1', prop2: 'prop2'}],
39 - sequentialRenders: [
40 - {prop1: 'prop1', prop2: 'prop2'},
41 - {prop1: 'prop1', prop2: 'prop2'},
42 - {prop1: 'changed', prop2: 'prop2'},
43 - ],
44 -};
45 -
46 -```
47 -
48 -## Code
49 -
50 -```javascript
51 -import { c as _c } from "react/compiler-runtime";
52 -import { Stringify, useIdentity } from "shared-runtime";
53 -
54 -function Component(t0) {
55 - "use memo";
56 - const $ = _c(12);
57 - const { prop1, prop2 } = t0;
58 - let t1;
59 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
60 - t1 = new Map([
61 - [0, "value0"],
62 - [1, "value1"],
63 - ]);
64 - $[0] = t1;
65 - } else {
66 - t1 = $[0];
67 - }
68 - const data = useIdentity(t1);
69 - let t2;
70 - if ($[1] !== data || $[2] !== prop1 || $[3] !== prop2) {
71 - let i = 0;
72 - const items = [];
73 - items.push(
74 - <Stringify
75 - key={i}
76 - onClick={() => data.get(i) + prop1}
77 - shouldInvokeFns={true}
78 - />,
79 - );
80 - i = i + 1;
81 -
82 - const t3 = i;
83 - let t4;
84 - if ($[5] !== data || $[6] !== i || $[7] !== prop2) {
85 - t4 = () => data.get(i) + prop2;
86 - $[5] = data;
87 - $[6] = i;
88 - $[7] = prop2;
89 - $[8] = t4;
90 - } else {
91 - t4 = $[8];
92 - }
93 - let t5;
94 - if ($[9] !== t3 || $[10] !== t4) {
95 - t5 = <Stringify key={t3} onClick={t4} shouldInvokeFns={true} />;
96 - $[9] = t3;
97 - $[10] = t4;
98 - $[11] = t5;
99 - } else {
100 - t5 = $[11];
101 - }
102 - items.push(t5);
103 - t2 = <>{items}</>;
104 - $[1] = data;
105 - $[2] = prop1;
106 - $[3] = prop2;
107 - $[4] = t2;
108 - } else {
109 - t2 = $[4];
110 - }
111 - return t2;
112 -}
113 -
114 -export const FIXTURE_ENTRYPOINT = {
115 - fn: Component,
116 - params: [{ prop1: "prop1", prop2: "prop2" }],
117 - sequentialRenders: [
118 - { prop1: "prop1", prop2: "prop2" },
119 - { prop1: "prop1", prop2: "prop2" },
120 - { prop1: "changed", prop2: "prop2" },
121 - ],
122 -};
123 -
124 -```
125 -
126 -### Eval output
127 -(kind: ok) <div>{"onClick":{"kind":"Function","result":"value1prop1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
128 -<div>{"onClick":{"kind":"Function","result":"value1prop1"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
129 -<div>{"onClick":{"kind":"Function","result":"value1changed"},"shouldInvokeFns":true}</div><div>{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-filter.expect.md new
+93
@@ -0,0 +1,93 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +function Component({value}) {
7 + const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
8 + useIdentity(null);
9 + const derived = arr.filter(Boolean);
10 + return (
11 + <Stringify>
12 + {derived.at(0)}
13 + {derived.at(-1)}
14 + </Stringify>
15 + );
16 +}
17 +
18 +```
19 +
20 +## Code
21 +
22 +```javascript
23 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
24 +function Component(t0) {
25 + const $ = _c(13);
26 + const { value } = t0;
27 + let t1;
28 + let t2;
29 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
30 + t1 = { value: "foo" };
31 + t2 = { value: "bar" };
32 + $[0] = t1;
33 + $[1] = t2;
34 + } else {
35 + t1 = $[0];
36 + t2 = $[1];
37 + }
38 + let t3;
39 + if ($[2] !== value) {
40 + t3 = [t1, t2, { value }];
41 + $[2] = value;
42 + $[3] = t3;
43 + } else {
44 + t3 = $[3];
45 + }
46 + const arr = t3;
47 + useIdentity(null);
48 + let t4;
49 + if ($[4] !== arr) {
50 + t4 = arr.filter(Boolean);
51 + $[4] = arr;
52 + $[5] = t4;
53 + } else {
54 + t4 = $[5];
55 + }
56 + const derived = t4;
57 + let t5;
58 + if ($[6] !== derived) {
59 + t5 = derived.at(0);
60 + $[6] = derived;
61 + $[7] = t5;
62 + } else {
63 + t5 = $[7];
64 + }
65 + let t6;
66 + if ($[8] !== derived) {
67 + t6 = derived.at(-1);
68 + $[8] = derived;
69 + $[9] = t6;
70 + } else {
71 + t6 = $[9];
72 + }
73 + let t7;
74 + if ($[10] !== t5 || $[11] !== t6) {
75 + t7 = (
76 + <Stringify>
77 + {t5}
78 + {t6}
79 + </Stringify>
80 + );
81 + $[10] = t5;
82 + $[11] = t6;
83 + $[12] = t7;
84 + } else {
85 + t7 = $[12];
86 + }
87 + return t7;
88 +}
89 +
90 +```
91 +
92 +### Eval output
93 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-filter.js new
+12
@@ -0,0 +1,12 @@
1 +// @enableNewMutationAliasingModel
2 +function Component({value}) {
3 + const arr = [{value: 'foo'}, {value: 'bar'}, {value}];
4 + useIdentity(null);
5 + const derived = arr.filter(Boolean);
6 + return (
7 + <Stringify>
8 + {derived.at(0)}
9 + {derived.at(-1)}
10 + </Stringify>
11 + );
12 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-captures-receiver-noAlias.expect.md new
+71
@@ -0,0 +1,71 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +function Component(props) {
7 + // This item is part of the receiver, should be memoized
8 + const item = {a: props.a};
9 + const items = [item];
10 + const mapped = items.map(item => item);
11 + // mapped[0].a = null;
12 + return mapped;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Component,
17 + params: [{a: {id: 42}}],
18 + isComponent: false,
19 +};
20 +
21 +```
22 +
23 +## Code
24 +
25 +```javascript
26 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
27 +function Component(props) {
28 + const $ = _c(6);
29 + let t0;
30 + if ($[0] !== props.a) {
31 + t0 = { a: props.a };
32 + $[0] = props.a;
33 + $[1] = t0;
34 + } else {
35 + t0 = $[1];
36 + }
37 + const item = t0;
38 + let t1;
39 + if ($[2] !== item) {
40 + t1 = [item];
41 + $[2] = item;
42 + $[3] = t1;
43 + } else {
44 + t1 = $[3];
45 + }
46 + const items = t1;
47 + let t2;
48 + if ($[4] !== items) {
49 + t2 = items.map(_temp);
50 + $[4] = items;
51 + $[5] = t2;
52 + } else {
53 + t2 = $[5];
54 + }
55 + const mapped = t2;
56 + return mapped;
57 +}
58 +function _temp(item_0) {
59 + return item_0;
60 +}
61 +
62 +export const FIXTURE_ENTRYPOINT = {
63 + fn: Component,
64 + params: [{ a: { id: 42 } }],
65 + isComponent: false,
66 +};
67 +
68 +```
69 +
70 +### Eval output
71 +(kind: ok) [{"a":{"id":42}}]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-captures-receiver-noAlias.js new
+15
@@ -0,0 +1,15 @@
1 +// @enableNewMutationAliasingModel
2 +function Component(props) {
3 + // This item is part of the receiver, should be memoized
4 + const item = {a: props.a};
5 + const items = [item];
6 + const mapped = items.map(item => item);
7 + // mapped[0].a = null;
8 + return mapped;
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: Component,
13 + params: [{a: {id: 42}}],
14 + isComponent: false,
15 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-push.expect.md new
+57
@@ -0,0 +1,57 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +function Component({a, b, c}) {
7 + const x = [];
8 + x.push(a);
9 + const merged = {b}; // could be mutated by mutate(x) below
10 + x.push(merged);
11 + mutate(x);
12 + const independent = {c}; // can't be later mutated
13 + x.push(independent);
14 + return <Foo value={x} />;
15 +}
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
23 +function Component(t0) {
24 + const $ = _c(6);
25 + const { a, b, c } = t0;
26 + let t1;
27 + if ($[0] !== a || $[1] !== b || $[2] !== c) {
28 + const x = [];
29 + x.push(a);
30 + const merged = { b };
31 + x.push(merged);
32 + mutate(x);
33 + let t2;
34 + if ($[4] !== c) {
35 + t2 = { c };
36 + $[4] = c;
37 + $[5] = t2;
38 + } else {
39 + t2 = $[5];
40 + }
41 + const independent = t2;
42 + x.push(independent);
43 + t1 = <Foo value={x} />;
44 + $[0] = a;
45 + $[1] = b;
46 + $[2] = c;
47 + $[3] = t1;
48 + } else {
49 + t1 = $[3];
50 + }
51 + return t1;
52 +}
53 +
54 +```
55 +
56 +### Eval output
57 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-push.js new
+11
@@ -0,0 +1,11 @@
1 +// @enableNewMutationAliasingModel
2 +function Component({a, b, c}) {
3 + const x = [];
4 + x.push(a);
5 + const merged = {b}; // could be mutated by mutate(x) below
6 + x.push(merged);
7 + mutate(x);
8 + const independent = {c}; // can't be later mutated
9 + x.push(independent);
10 + return <Foo value={x} />;
11 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/basic-mutation-via-function-expression.expect.md new
+49
@@ -0,0 +1,49 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +function Component({a, b}) {
7 + const x = {a};
8 + const y = [b];
9 + const f = () => {
10 + y.x = x;
11 + mutate(y);
12 + };
13 + f();
14 + return <div>{x}</div>;
15 +}
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
23 +function Component(t0) {
24 + const $ = _c(3);
25 + const { a, b } = t0;
26 + let t1;
27 + if ($[0] !== a || $[1] !== b) {
28 + const x = { a };
29 + const y = [b];
30 + const f = () => {
31 + y.x = x;
32 + mutate(y);
33 + };
34 +
35 + f();
36 + t1 = <div>{x}</div>;
37 + $[0] = a;
38 + $[1] = b;
39 + $[2] = t1;
40 + } else {
41 + t1 = $[2];
42 + }
43 + return t1;
44 +}
45 +
46 +```
47 +
48 +### Eval output
49 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/basic-mutation-via-function-expression.js new
+11
@@ -0,0 +1,11 @@
1 +// @enableNewMutationAliasingModel
2 +function Component({a, b}) {
3 + const x = {a};
4 + const y = [b];
5 + const f = () => {
6 + y.x = x;
7 + mutate(y);
8 + };
9 + f();
10 + return <div>{x}</div>;
11 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/basic-mutation.expect.md new
+42
@@ -0,0 +1,42 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +function Component({a, b}) {
7 + const x = {a};
8 + const y = [b];
9 + y.x = x;
10 + mutate(y);
11 + return <div>{x}</div>;
12 +}
13 +
14 +```
15 +
16 +## Code
17 +
18 +```javascript
19 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
20 +function Component(t0) {
21 + const $ = _c(3);
22 + const { a, b } = t0;
23 + let t1;
24 + if ($[0] !== a || $[1] !== b) {
25 + const x = { a };
26 + const y = [b];
27 + y.x = x;
28 + mutate(y);
29 + t1 = <div>{x}</div>;
30 + $[0] = a;
31 + $[1] = b;
32 + $[2] = t1;
33 + } else {
34 + t1 = $[2];
35 + }
36 + return t1;
37 +}
38 +
39 +```
40 +
41 +### Eval output
42 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/basic-mutation.js new
+8
@@ -0,0 +1,8 @@
1 +// @enableNewMutationAliasingModel
2 +function Component({a, b}) {
3 + const x = {a};
4 + const y = [b];
5 + y.x = x;
6 + mutate(y);
7 + return <div>{x}</div>;
8 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-backedge-phi-with-later-mutation.expect.md new
+102
@@ -0,0 +1,102 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +import {arrayPush, Stringify} from 'shared-runtime';
7 +
8 +function Component({prop1, prop2}) {
9 + 'use memo';
10 +
11 + let x = [{value: prop1}];
12 + let z;
13 + while (x.length < 2) {
14 + // there's a phi here for x (value before the loop and the reassignment later)
15 +
16 + // this mutation occurs before the reassigned value
17 + arrayPush(x, {value: prop2});
18 +
19 + if (x[0].value === prop1) {
20 + x = [{value: prop2}];
21 + const y = x;
22 + z = y[0];
23 + }
24 + }
25 + z.other = true;
26 + return <Stringify z={z} />;
27 +}
28 +
29 +export const FIXTURE_ENTRYPOINT = {
30 + fn: Component,
31 + params: [{prop1: 0, prop2: 'a'}],
32 + sequentialRenders: [
33 + {prop1: 0, prop2: 'a'},
34 + {prop1: 1, prop2: 'a'},
35 + {prop1: 1, prop2: 'b'},
36 + {prop1: 0, prop2: 'b'},
37 + {prop1: 0, prop2: 'a'},
38 + ],
39 +};
40 +
41 +```
42 +
43 +## Code
44 +
45 +```javascript
46 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
47 +import { arrayPush, Stringify } from "shared-runtime";
48 +
49 +function Component(t0) {
50 + "use memo";
51 + const $ = _c(5);
52 + const { prop1, prop2 } = t0;
53 + let z;
54 + if ($[0] !== prop1 || $[1] !== prop2) {
55 + let x = [{ value: prop1 }];
56 + while (x.length < 2) {
57 + arrayPush(x, { value: prop2 });
58 + if (x[0].value === prop1) {
59 + x = [{ value: prop2 }];
60 + const y = x;
61 + z = y[0];
62 + }
63 + }
64 +
65 + z.other = true;
66 + $[0] = prop1;
67 + $[1] = prop2;
68 + $[2] = z;
69 + } else {
70 + z = $[2];
71 + }
72 + let t1;
73 + if ($[3] !== z) {
74 + t1 = <Stringify z={z} />;
75 + $[3] = z;
76 + $[4] = t1;
77 + } else {
78 + t1 = $[4];
79 + }
80 + return t1;
81 +}
82 +
83 +export const FIXTURE_ENTRYPOINT = {
84 + fn: Component,
85 + params: [{ prop1: 0, prop2: "a" }],
86 + sequentialRenders: [
87 + { prop1: 0, prop2: "a" },
88 + { prop1: 1, prop2: "a" },
89 + { prop1: 1, prop2: "b" },
90 + { prop1: 0, prop2: "b" },
91 + { prop1: 0, prop2: "a" },
92 + ],
93 +};
94 +
95 +```
96 +
97 +### Eval output
98 +(kind: ok) <div>{"z":{"value":"a","other":true}}</div>
99 +<div>{"z":{"value":"a","other":true}}</div>
100 +<div>{"z":{"value":"b","other":true}}</div>
101 +<div>{"z":{"value":"b","other":true}}</div>
102 +<div>{"z":{"value":"a","other":true}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capture-backedge-phi-with-later-mutation.js new
+35
@@ -0,0 +1,35 @@
1 +// @enableNewMutationAliasingModel
2 +import {arrayPush, Stringify} from 'shared-runtime';
3 +
4 +function Component({prop1, prop2}) {
5 + 'use memo';
6 +
7 + let x = [{value: prop1}];
8 + let z;
9 + while (x.length < 2) {
10 + // there's a phi here for x (value before the loop and the reassignment later)
11 +
12 + // this mutation occurs before the reassigned value
13 + arrayPush(x, {value: prop2});
14 +
15 + if (x[0].value === prop1) {
16 + x = [{value: prop2}];
17 + const y = x;
18 + z = y[0];
19 + }
20 + }
21 + z.other = true;
22 + return <Stringify z={z} />;
23 +}
24 +
25 +export const FIXTURE_ENTRYPOINT = {
26 + fn: Component,
27 + params: [{prop1: 0, prop2: 'a'}],
28 + sequentialRenders: [
29 + {prop1: 0, prop2: 'a'},
30 + {prop1: 1, prop2: 'a'},
31 + {prop1: 1, prop2: 'b'},
32 + {prop1: 0, prop2: 'b'},
33 + {prop1: 0, prop2: 'a'},
34 + ],
35 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-reassign-local-variable-in-jsx-callback.expect.md new
+53
@@ -0,0 +1,53 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component() {
6 + let local;
7 +
8 + const reassignLocal = newValue => {
9 + local = newValue;
10 + };
11 +
12 + const onClick = newValue => {
13 + reassignLocal('hello');
14 +
15 + if (local === newValue) {
16 + // Without React Compiler, `reassignLocal` is freshly created
17 + // on each render, capturing a binding to the latest `local`,
18 + // such that invoking reassignLocal will reassign the same
19 + // binding that we are observing in the if condition, and
20 + // we reach this branch
21 + console.log('`local` was updated!');
22 + } else {
23 + // With React Compiler enabled, `reassignLocal` is only created
24 + // once, capturing a binding to `local` in that render pass.
25 + // Therefore, calling `reassignLocal` will reassign the wrong
26 + // version of `local`, and not update the binding we are checking
27 + // in the if condition.
28 + //
29 + // To protect against this, we disallow reassigning locals from
30 + // functions that escape
31 + throw new Error('`local` not updated!');
32 + }
33 + };
34 +
35 + return <button onClick={onClick}>Submit</button>;
36 +}
37 +
38 +```
39 +
40 +
41 +## Error
42 +
43 +```
44 + 3 |
45 + 4 | const reassignLocal = newValue => {
46 +> 5 | local = newValue;
47 + | ^^^^^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `local` cannot be reassigned after render (5:5)
48 + 6 | };
49 + 7 |
50 + 8 | const onClick = newValue => {
51 +```
52 +
53 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-reassign-local-variable-in-jsx-callback.js new
+32
@@ -0,0 +1,32 @@
1 +function Component() {
2 + let local;
3 +
4 + const reassignLocal = newValue => {
5 + local = newValue;
6 + };
7 +
8 + const onClick = newValue => {
9 + reassignLocal('hello');
10 +
11 + if (local === newValue) {
12 + // Without React Compiler, `reassignLocal` is freshly created
13 + // on each render, capturing a binding to the latest `local`,
14 + // such that invoking reassignLocal will reassign the same
15 + // binding that we are observing in the if condition, and
16 + // we reach this branch
17 + console.log('`local` was updated!');
18 + } else {
19 + // With React Compiler enabled, `reassignLocal` is only created
20 + // once, capturing a binding to `local` in that render pass.
21 + // Therefore, calling `reassignLocal` will reassign the wrong
22 + // version of `local`, and not update the binding we are checking
23 + // in the if condition.
24 + //
25 + // To protect against this, we disallow reassigning locals from
26 + // functions that escape
27 + throw new Error('`local` not updated!');
28 + }
29 + };
30 +
31 + return <button onClick={onClick}>Submit</button>;
32 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-useCallback-captures-reassigned-context.expect.md new
+43
@@ -0,0 +1,43 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel
6 +import {useCallback} from 'react';
7 +import {makeArray} from 'shared-runtime';
8 +
9 +// This case is already unsound in source, so we can safely bailout
10 +function Foo(props) {
11 + let x = [];
12 + x.push(props);
13 +
14 + // makeArray() is captured, but depsList contains [props]
15 + const cb = useCallback(() => [x], [x]);
16 +
17 + x = makeArray();
18 +
19 + return cb;
20 +}
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: Foo,
23 + params: [{}],
24 +};
25 +
26 +```
27 +
28 +
29 +## Error
30 +
31 +```
32 + 9 |
33 + 10 | // makeArray() is captured, but depsList contains [props]
34 +> 11 | const cb = useCallback(() => [x], [x]);
35 + | ^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This dependency may be mutated later, which could cause the value to change unexpectedly (11:11)
36 +
37 +CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output. (11:11)
38 + 12 |
39 + 13 | x = makeArray();
40 + 14 |
41 +```
42 +
43 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-useCallback-captures-reassigned-context.js new
+20
@@ -0,0 +1,20 @@
1 +// @validatePreserveExistingMemoizationGuarantees @enableNewMutationAliasingModel
2 +import {useCallback} from 'react';
3 +import {makeArray} from 'shared-runtime';
4 +
5 +// This case is already unsound in source, so we can safely bailout
6 +function Foo(props) {
7 + let x = [];
8 + x.push(props);
9 +
10 + // makeArray() is captured, but depsList contains [props]
11 + const cb = useCallback(() => [x], [x]);
12 +
13 + x = makeArray();
14 +
15 + return cb;
16 +}
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Foo,
19 + params: [{}],
20 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-frozen-value.expect.md new
+28
@@ -0,0 +1,28 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +function Component({a, b}) {
7 + const x = {a};
8 + useFreeze(x);
9 + x.y = true;
10 + return <div>error</div>;
11 +}
12 +
13 +```
14 +
15 +
16 +## Error
17 +
18 +```
19 + 3 | const x = {a};
20 + 4 | useFreeze(x);
21 +> 5 | x.y = true;
22 + | ^ InvalidReact: This mutates a variable that React considers immutable (5:5)
23 + 6 | return <div>error</div>;
24 + 7 | }
25 + 8 |
26 +```
27 +
28 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-frozen-value.js new
+7
@@ -0,0 +1,7 @@
1 +// @enableNewMutationAliasingModel
2 +function Component({a, b}) {
3 + const x = {a};
4 + useFreeze(x);
5 + x.y = true;
6 + return <div>error</div>;
7 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/iife-return-modified-later-phi.expect.md new
+58
@@ -0,0 +1,58 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + const items = (() => {
7 + if (props.cond) {
8 + return [];
9 + } else {
10 + return null;
11 + }
12 + })();
13 + items?.push(props.a);
14 + return items;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{a: {}}],
20 +};
21 +
22 +```
23 +
24 +## Code
25 +
26 +```javascript
27 +import { c as _c } from "react/compiler-runtime";
28 +function Component(props) {
29 + const $ = _c(3);
30 + let items;
31 + if ($[0] !== props.a || $[1] !== props.cond) {
32 + let t0;
33 + if (props.cond) {
34 + t0 = [];
35 + } else {
36 + t0 = null;
37 + }
38 + items = t0;
39 +
40 + items?.push(props.a);
41 + $[0] = props.a;
42 + $[1] = props.cond;
43 + $[2] = items;
44 + } else {
45 + items = $[2];
46 + }
47 + return items;
48 +}
49 +
50 +export const FIXTURE_ENTRYPOINT = {
51 + fn: Component,
52 + params: [{ a: {} }],
53 +};
54 +
55 +```
56 +
57 +### Eval output
58 +(kind: ok) null
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/iife-return-modified-later-phi.js new
+16
@@ -0,0 +1,16 @@
1 +function Component(props) {
2 + const items = (() => {
3 + if (props.cond) {
4 + return [];
5 + } else {
6 + return null;
7 + }
8 + })();
9 + items?.push(props.a);
10 + return items;
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Component,
15 + params: [{a: {}}],
16 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections-2.expect.md new
+67
@@ -0,0 +1,67 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +import {Stringify} from 'shared-runtime';
7 +
8 +function Component({a, b}) {
9 + const x = {a, b};
10 + const f = () => {
11 + const y = [x];
12 + return y[0];
13 + };
14 + const x0 = f();
15 + const z = [x0];
16 + const x1 = z[0];
17 + x1.key = 'value';
18 + return <Stringify x={x} />;
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: Component,
23 + params: [{a: 0, b: 1}],
24 +};
25 +
26 +```
27 +
28 +## Code
29 +
30 +```javascript
31 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
32 +import { Stringify } from "shared-runtime";
33 +
34 +function Component(t0) {
35 + const $ = _c(3);
36 + const { a, b } = t0;
37 + let t1;
38 + if ($[0] !== a || $[1] !== b) {
39 + const x = { a, b };
40 + const f = () => {
41 + const y = [x];
42 + return y[0];
43 + };
44 +
45 + const x0 = f();
46 + const z = [x0];
47 + const x1 = z[0];
48 + x1.key = "value";
49 + t1 = <Stringify x={x} />;
50 + $[0] = a;
51 + $[1] = b;
52 + $[2] = t1;
53 + } else {
54 + t1 = $[2];
55 + }
56 + return t1;
57 +}
58 +
59 +export const FIXTURE_ENTRYPOINT = {
60 + fn: Component,
61 + params: [{ a: 0, b: 1 }],
62 +};
63 +
64 +```
65 +
66 +### Eval output
67 +(kind: ok) <div>{"x":{"a":0,"b":1,"key":"value"}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections-2.js new
+20
@@ -0,0 +1,20 @@
1 +// @enableNewMutationAliasingModel
2 +import {Stringify} from 'shared-runtime';
3 +
4 +function Component({a, b}) {
5 + const x = {a, b};
6 + const f = () => {
7 + const y = [x];
8 + return y[0];
9 + };
10 + const x0 = f();
11 + const z = [x0];
12 + const x1 = z[0];
13 + x1.key = 'value';
14 + return <Stringify x={x} />;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{a: 0, b: 1}],
20 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections.expect.md new
+67
@@ -0,0 +1,67 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +import {Stringify} from 'shared-runtime';
7 +
8 +function Component({a, b}) {
9 + const x = {a, b};
10 + const y = [x];
11 + const f = () => {
12 + const x0 = y[0];
13 + return [x0];
14 + };
15 + const z = f();
16 + const x1 = z[0];
17 + x1.key = 'value';
18 + return <Stringify x={x} />;
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: Component,
23 + params: [{a: 0, b: 1}],
24 +};
25 +
26 +```
27 +
28 +## Code
29 +
30 +```javascript
31 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
32 +import { Stringify } from "shared-runtime";
33 +
34 +function Component(t0) {
35 + const $ = _c(3);
36 + const { a, b } = t0;
37 + let t1;
38 + if ($[0] !== a || $[1] !== b) {
39 + const x = { a, b };
40 + const y = [x];
41 + const f = () => {
42 + const x0 = y[0];
43 + return [x0];
44 + };
45 +
46 + const z = f();
47 + const x1 = z[0];
48 + x1.key = "value";
49 + t1 = <Stringify x={x} />;
50 + $[0] = a;
51 + $[1] = b;
52 + $[2] = t1;
53 + } else {
54 + t1 = $[2];
55 + }
56 + return t1;
57 +}
58 +
59 +export const FIXTURE_ENTRYPOINT = {
60 + fn: Component,
61 + params: [{ a: 0, b: 1 }],
62 +};
63 +
64 +```
65 +
66 +### Eval output
67 +(kind: ok) <div>{"x":{"a":0,"b":1,"key":"value"}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-function-call-indirections.js new
+20
@@ -0,0 +1,20 @@
1 +// @enableNewMutationAliasingModel
2 +import {Stringify} from 'shared-runtime';
3 +
4 +function Component({a, b}) {
5 + const x = {a, b};
6 + const y = [x];
7 + const f = () => {
8 + const x0 = y[0];
9 + return [x0];
10 + };
11 + const z = f();
12 + const x1 = z[0];
13 + x1.key = 'value';
14 + return <Stringify x={x} />;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{a: 0, b: 1}],
20 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-indirections.expect.md new
+60
@@ -0,0 +1,60 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +import {Stringify} from 'shared-runtime';
7 +
8 +function Component({a, b}) {
9 + const x = {a, b};
10 + const y = [x];
11 + const x0 = y[0];
12 + const z = [x0];
13 + const x1 = z[0];
14 + x1.key = 'value';
15 + return <Stringify x={x} />;
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Component,
20 + params: [{a: 0, b: 1}],
21 +};
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
29 +import { Stringify } from "shared-runtime";
30 +
31 +function Component(t0) {
32 + const $ = _c(3);
33 + const { a, b } = t0;
34 + let t1;
35 + if ($[0] !== a || $[1] !== b) {
36 + const x = { a, b };
37 + const y = [x];
38 + const x0 = y[0];
39 + const z = [x0];
40 + const x1 = z[0];
41 + x1.key = "value";
42 + t1 = <Stringify x={x} />;
43 + $[0] = a;
44 + $[1] = b;
45 + $[2] = t1;
46 + } else {
47 + t1 = $[2];
48 + }
49 + return t1;
50 +}
51 +
52 +export const FIXTURE_ENTRYPOINT = {
53 + fn: Component,
54 + params: [{ a: 0, b: 1 }],
55 +};
56 +
57 +```
58 +
59 +### Eval output
60 +(kind: ok) <div>{"x":{"a":0,"b":1,"key":"value"}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-boxing-unboxing-indirections.js new
+17
@@ -0,0 +1,17 @@
1 +// @enableNewMutationAliasingModel
2 +import {Stringify} from 'shared-runtime';
3 +
4 +function Component({a, b}) {
5 + const x = {a, b};
6 + const y = [x];
7 + const x0 = y[0];
8 + const z = [x0];
9 + const x1 = z[0];
10 + x1.key = 'value';
11 + return <Stringify x={x} />;
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: Component,
16 + params: [{a: 0, b: 1}],
17 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-propertyload.expect.md new
+39
@@ -0,0 +1,39 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +function Component({a, b}) {
7 + const x = {};
8 + const y = {x};
9 + const z = y.x;
10 + z.true = false;
11 + return <div>{z}</div>;
12 +}
13 +
14 +```
15 +
16 +## Code
17 +
18 +```javascript
19 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
20 +function Component(t0) {
21 + const $ = _c(1);
22 + let t1;
23 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
24 + const x = {};
25 + const y = { x };
26 + const z = y.x;
27 + z.true = false;
28 + t1 = <div>{z}</div>;
29 + $[0] = t1;
30 + } else {
31 + t1 = $[0];
32 + }
33 + return t1;
34 +}
35 +
36 +```
37 +
38 +### Eval output
39 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-through-propertyload.js new
+8
@@ -0,0 +1,8 @@
1 +// @enableNewMutationAliasingModel
2 +function Component({a, b}) {
3 + const x = {};
4 + const y = {x};
5 + const z = y.x;
6 + z.true = false;
7 + return <div>{z}</div>;
8 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/nullable-objects-assume-invoked-direct-call.expect.md new
+75
@@ -0,0 +1,75 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +import {useState} from 'react';
7 +import {useIdentity} from 'shared-runtime';
8 +
9 +function useMakeCallback({obj}: {obj: {value: number}}) {
10 + const [state, setState] = useState(0);
11 + const cb = () => {
12 + if (obj.value !== state) setState(obj.value);
13 + };
14 + useIdentity();
15 + cb();
16 + return [cb];
17 +}
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: useMakeCallback,
20 + params: [{obj: {value: 1}}],
21 + sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}],
22 +};
23 +
24 +```
25 +
26 +## Code
27 +
28 +```javascript
29 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
30 +import { useState } from "react";
31 +import { useIdentity } from "shared-runtime";
32 +
33 +function useMakeCallback(t0) {
34 + const $ = _c(5);
35 + const { obj } = t0;
36 + const [state, setState] = useState(0);
37 + let t1;
38 + if ($[0] !== obj.value || $[1] !== state) {
39 + t1 = () => {
40 + if (obj.value !== state) {
41 + setState(obj.value);
42 + }
43 + };
44 + $[0] = obj.value;
45 + $[1] = state;
46 + $[2] = t1;
47 + } else {
48 + t1 = $[2];
49 + }
50 + const cb = t1;
51 +
52 + useIdentity();
53 + cb();
54 + let t2;
55 + if ($[3] !== cb) {
56 + t2 = [cb];
57 + $[3] = cb;
58 + $[4] = t2;
59 + } else {
60 + t2 = $[4];
61 + }
62 + return t2;
63 +}
64 +
65 +export const FIXTURE_ENTRYPOINT = {
66 + fn: useMakeCallback,
67 + params: [{ obj: { value: 1 } }],
68 + sequentialRenders: [{ obj: { value: 1 } }, { obj: { value: 2 } }],
69 +};
70 +
71 +```
72 +
73 +### Eval output
74 +(kind: ok) ["[[ function params=0 ]]"]
75 +["[[ function params=0 ]]"]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/nullable-objects-assume-invoked-direct-call.js new
+18
@@ -0,0 +1,18 @@
1 +// @enableNewMutationAliasingModel
2 +import {useState} from 'react';
3 +import {useIdentity} from 'shared-runtime';
4 +
5 +function useMakeCallback({obj}: {obj: {value: number}}) {
6 + const [state, setState] = useState(0);
7 + const cb = () => {
8 + if (obj.value !== state) setState(obj.value);
9 + };
10 + useIdentity();
11 + cb();
12 + return [cb];
13 +}
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: useMakeCallback,
16 + params: [{obj: {value: 1}}],
17 + sequentialRenders: [{obj: {value: 1}}, {obj: {value: 2}}],
18 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/potential-mutation-in-function-expression.expect.md new
+64
@@ -0,0 +1,64 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +function Component({a, b, c}) {
7 + const x = [a, b];
8 + const f = () => {
9 + maybeMutate(x);
10 + // different dependency to force this not to merge with x's scope
11 + console.log(c);
12 + };
13 + return <Foo onClick={f} value={x} />;
14 +}
15 +
16 +```
17 +
18 +## Code
19 +
20 +```javascript
21 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
22 +function Component(t0) {
23 + const $ = _c(9);
24 + const { a, b, c } = t0;
25 + let t1;
26 + if ($[0] !== a || $[1] !== b) {
27 + t1 = [a, b];
28 + $[0] = a;
29 + $[1] = b;
30 + $[2] = t1;
31 + } else {
32 + t1 = $[2];
33 + }
34 + const x = t1;
35 + let t2;
36 + if ($[3] !== c || $[4] !== x) {
37 + t2 = () => {
38 + maybeMutate(x);
39 +
40 + console.log(c);
41 + };
42 + $[3] = c;
43 + $[4] = x;
44 + $[5] = t2;
45 + } else {
46 + t2 = $[5];
47 + }
48 + const f = t2;
49 + let t3;
50 + if ($[6] !== f || $[7] !== x) {
51 + t3 = <Foo onClick={f} value={x} />;
52 + $[6] = f;
53 + $[7] = x;
54 + $[8] = t3;
55 + } else {
56 + t3 = $[8];
57 + }
58 + return t3;
59 +}
60 +
61 +```
62 +
63 +### Eval output
64 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/potential-mutation-in-function-expression.js new
+10
@@ -0,0 +1,10 @@
1 +// @enableNewMutationAliasingModel
2 +function Component({a, b, c}) {
3 + const x = [a, b];
4 + const f = () => {
5 + maybeMutate(x);
6 + // different dependency to force this not to merge with x's scope
7 + console.log(c);
8 + };
9 + return <Foo onClick={f} value={x} />;
10 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-ref.expect.md new
+54
@@ -0,0 +1,54 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +function ReactiveRefInEffect(props) {
7 + const ref1 = useRef('initial value');
8 + const ref2 = useRef('initial value');
9 + let ref;
10 + if (props.foo) {
11 + ref = ref1;
12 + } else {
13 + ref = ref2;
14 + }
15 + useEffect(() => print(ref));
16 +}
17 +
18 +```
19 +
20 +## Code
21 +
22 +```javascript
23 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
24 +function ReactiveRefInEffect(props) {
25 + const $ = _c(4);
26 + const ref1 = useRef("initial value");
27 + const ref2 = useRef("initial value");
28 + let ref;
29 + if ($[0] !== props.foo) {
30 + if (props.foo) {
31 + ref = ref1;
32 + } else {
33 + ref = ref2;
34 + }
35 + $[0] = props.foo;
36 + $[1] = ref;
37 + } else {
38 + ref = $[1];
39 + }
40 + let t0;
41 + if ($[2] !== ref) {
42 + t0 = () => print(ref);
43 + $[2] = ref;
44 + $[3] = t0;
45 + } else {
46 + t0 = $[3];
47 + }
48 + useEffect(t0);
49 +}
50 +
51 +```
52 +
53 +### Eval output
54 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-ref.js new
+12
@@ -0,0 +1,12 @@
1 +// @enableNewMutationAliasingModel
2 +function ReactiveRefInEffect(props) {
3 + const ref1 = useRef('initial value');
4 + const ref2 = useRef('initial value');
5 + let ref;
6 + if (props.foo) {
7 + ref = ref1;
8 + } else {
9 + ref = ref2;
10 + }
11 + useEffect(() => print(ref));
12 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/set-add-mutate.expect.md new
+54
@@ -0,0 +1,54 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +function useHook({el1, el2}) {
7 + const s = new Set();
8 + const arr = makeArray(el1);
9 + s.add(arr);
10 + // Mutate after store
11 + arr.push(el2);
12 +
13 + s.add(makeArray(el2));
14 + return s.size;
15 +}
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
23 +function useHook(t0) {
24 + const $ = _c(5);
25 + const { el1, el2 } = t0;
26 + let s;
27 + if ($[0] !== el1 || $[1] !== el2) {
28 + s = new Set();
29 + const arr = makeArray(el1);
30 + s.add(arr);
31 +
32 + arr.push(el2);
33 + let t1;
34 + if ($[3] !== el2) {
35 + t1 = makeArray(el2);
36 + $[3] = el2;
37 + $[4] = t1;
38 + } else {
39 + t1 = $[4];
40 + }
41 + s.add(t1);
42 + $[0] = el1;
43 + $[1] = el2;
44 + $[2] = s;
45 + } else {
46 + s = $[2];
47 + }
48 + return s.size;
49 +}
50 +
51 +```
52 +
53 +### Eval output
54 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/set-add-mutate.js new
+11
@@ -0,0 +1,11 @@
1 +// @enableNewMutationAliasingModel
2 +function useHook({el1, el2}) {
3 + const s = new Set();
4 + const arr = makeArray(el1);
5 + s.add(arr);
6 + // Mutate after store
7 + arr.push(el2);
8 +
9 + s.add(makeArray(el2));
10 + return s.size;
11 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/ssa-renaming-ternary-destruction.expect.md new
+70
@@ -0,0 +1,70 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enablePropagateDepsInHIR @enableNewMutationAliasingModel
6 +function useFoo(props) {
7 + let x = [];
8 + x.push(props.bar);
9 + // todo: the below should memoize separately from the above
10 + // my guess is that the phi causes the different `x` identifiers
11 + // to get added to an alias group. this is where we need to track
12 + // the actual state of the alias groups at the time of the mutation
13 + props.cond ? (({x} = {x: {}}), ([x] = [[]]), x.push(props.foo)) : null;
14 + return x;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: useFoo,
19 + params: [{cond: false, foo: 2, bar: 55}],
20 + sequentialRenders: [
21 + {cond: false, foo: 2, bar: 55},
22 + {cond: false, foo: 3, bar: 55},
23 + {cond: true, foo: 3, bar: 55},
24 + ],
25 +};
26 +
27 +```
28 +
29 +## Code
30 +
31 +```javascript
32 +import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR @enableNewMutationAliasingModel
33 +function useFoo(props) {
34 + const $ = _c(5);
35 + let x;
36 + if ($[0] !== props.bar) {
37 + x = [];
38 + x.push(props.bar);
39 + $[0] = props.bar;
40 + $[1] = x;
41 + } else {
42 + x = $[1];
43 + }
44 + if ($[2] !== props.cond || $[3] !== props.foo) {
45 + props.cond ? (([x] = [[]]), x.push(props.foo)) : null;
46 + $[2] = props.cond;
47 + $[3] = props.foo;
48 + $[4] = x;
49 + } else {
50 + x = $[4];
51 + }
52 + return x;
53 +}
54 +
55 +export const FIXTURE_ENTRYPOINT = {
56 + fn: useFoo,
57 + params: [{ cond: false, foo: 2, bar: 55 }],
58 + sequentialRenders: [
59 + { cond: false, foo: 2, bar: 55 },
60 + { cond: false, foo: 3, bar: 55 },
61 + { cond: true, foo: 3, bar: 55 },
62 + ],
63 +};
64 +
65 +```
66 +
67 +### Eval output
68 +(kind: ok) [55]
69 +[55]
70 +[3]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/ssa-renaming-ternary-destruction.js new
+21
@@ -0,0 +1,21 @@
1 +// @enablePropagateDepsInHIR @enableNewMutationAliasingModel
2 +function useFoo(props) {
3 + let x = [];
4 + x.push(props.bar);
5 + // todo: the below should memoize separately from the above
6 + // my guess is that the phi causes the different `x` identifiers
7 + // to get added to an alias group. this is where we need to track
8 + // the actual state of the alias groups at the time of the mutation
9 + props.cond ? (({x} = {x: {}}), ([x] = [[]]), x.push(props.foo)) : null;
10 + return x;
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: useFoo,
15 + params: [{cond: false, foo: 2, bar: 55}],
16 + sequentialRenders: [
17 + {cond: false, foo: 2, bar: 55},
18 + {cond: false, foo: 3, bar: 55},
19 + {cond: true, foo: 3, bar: 55},
20 + ],
21 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitive-mutation-before-capturing-value-created-earlier.expect.md new
+50
@@ -0,0 +1,50 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +function Component({a, b}) {
7 + const x = [a];
8 + const y = {b};
9 + mutate(y);
10 + y.x = x;
11 + return <div>{y}</div>;
12 +}
13 +
14 +```
15 +
16 +## Code
17 +
18 +```javascript
19 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
20 +function Component(t0) {
21 + const $ = _c(5);
22 + const { a, b } = t0;
23 + let t1;
24 + if ($[0] !== a) {
25 + t1 = [a];
26 + $[0] = a;
27 + $[1] = t1;
28 + } else {
29 + t1 = $[1];
30 + }
31 + const x = t1;
32 + let t2;
33 + if ($[2] !== b || $[3] !== x) {
34 + const y = { b };
35 + mutate(y);
36 + y.x = x;
37 + t2 = <div>{y}</div>;
38 + $[2] = b;
39 + $[3] = x;
40 + $[4] = t2;
41 + } else {
42 + t2 = $[4];
43 + }
44 + return t2;
45 +}
46 +
47 +```
48 +
49 +### Eval output
50 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/transitive-mutation-before-capturing-value-created-earlier.js new
+8
@@ -0,0 +1,8 @@
1 +// @enableNewMutationAliasingModel
2 +function Component({a, b}) {
3 + const x = [a];
4 + const y = {b};
5 + mutate(y);
6 + y.x = x;
7 + return <div>{y}</div>;
8 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-access-assignment.expect.md new
+83
@@ -0,0 +1,83 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component({a, b, c}) {
6 + // This is an object version of array-access-assignment.js
7 + // Meant to confirm that object expressions and PropertyStore/PropertyLoad with strings
8 + // works equivalently to array expressions and property accesses with numeric indices
9 + const x = {zero: a};
10 + const y = {zero: null, one: b};
11 + const z = {zero: {}, one: {}, two: {zero: c}};
12 + x.zero = y.one;
13 + z.zero.zero = x.zero;
14 + return {zero: x, one: z};
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{a: 1, b: 20, c: 300}],
20 + sequentialRenders: [
21 + {a: 2, b: 20, c: 300},
22 + {a: 3, b: 20, c: 300},
23 + {a: 3, b: 21, c: 300},
24 + {a: 3, b: 22, c: 300},
25 + {a: 3, b: 22, c: 301},
26 + ],
27 +};
28 +
29 +```
30 +
31 +## Code
32 +
33 +```javascript
34 +import { c as _c } from "react/compiler-runtime";
35 +function Component(t0) {
36 + const $ = _c(6);
37 + const { a, b, c } = t0;
38 + let t1;
39 + if ($[0] !== a || $[1] !== b || $[2] !== c) {
40 + const x = { zero: a };
41 + let t2;
42 + if ($[4] !== b) {
43 + t2 = { zero: null, one: b };
44 + $[4] = b;
45 + $[5] = t2;
46 + } else {
47 + t2 = $[5];
48 + }
49 + const y = t2;
50 + const z = { zero: {}, one: {}, two: { zero: c } };
51 + x.zero = y.one;
52 + z.zero.zero = x.zero;
53 + t1 = { zero: x, one: z };
54 + $[0] = a;
55 + $[1] = b;
56 + $[2] = c;
57 + $[3] = t1;
58 + } else {
59 + t1 = $[3];
60 + }
61 + return t1;
62 +}
63 +
64 +export const FIXTURE_ENTRYPOINT = {
65 + fn: Component,
66 + params: [{ a: 1, b: 20, c: 300 }],
67 + sequentialRenders: [
68 + { a: 2, b: 20, c: 300 },
69 + { a: 3, b: 20, c: 300 },
70 + { a: 3, b: 21, c: 300 },
71 + { a: 3, b: 22, c: 300 },
72 + { a: 3, b: 22, c: 301 },
73 + ],
74 +};
75 +
76 +```
77 +
78 +### Eval output
79 +(kind: ok) {"zero":{"zero":20},"one":{"zero":{"zero":20},"one":{},"two":{"zero":300}}}
80 +{"zero":{"zero":20},"one":{"zero":{"zero":20},"one":{},"two":{"zero":300}}}
81 +{"zero":{"zero":21},"one":{"zero":{"zero":21},"one":{},"two":{"zero":300}}}
82 +{"zero":{"zero":22},"one":{"zero":{"zero":22},"one":{},"two":{"zero":300}}}
83 +{"zero":{"zero":22},"one":{"zero":{"zero":22},"one":{},"two":{"zero":301}}}
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-access-assignment.js new
+23
@@ -0,0 +1,23 @@
1 +function Component({a, b, c}) {
2 + // This is an object version of array-access-assignment.js
3 + // Meant to confirm that object expressions and PropertyStore/PropertyLoad with strings
4 + // works equivalently to array expressions and property accesses with numeric indices
5 + const x = {zero: a};
6 + const y = {zero: null, one: b};
7 + const z = {zero: {}, one: {}, two: {zero: c}};
8 + x.zero = y.one;
9 + z.zero.zero = x.zero;
10 + return {zero: x, one: z};
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Component,
15 + params: [{a: 1, b: 20, c: 300}],
16 + sequentialRenders: [
17 + {a: 2, b: 20, c: 300},
18 + {a: 3, b: 20, c: 300},
19 + {a: 3, b: 21, c: 300},
20 + {a: 3, b: 22, c: 300},
21 + {a: 3, b: 22, c: 301},
22 + ],
23 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-aliased-capture-aliased-mutate.expect.md new
+104
@@ -0,0 +1,104 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel
6 +import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
7 +
8 +/**
9 + * Repro of a bug fixed in the new aliasing model.
10 + *
11 + * 1. `InferMutableRanges` derives the mutable range of identifiers and their
12 + * aliases from `LoadLocal`, `PropertyLoad`, etc
13 + * - After this pass, y's mutable range only extends to `arrayPush(x, y)`
14 + * - We avoid assigning mutable ranges to loads after y's mutable range, as
15 + * these are working with an immutable value. As a result, `LoadLocal y` and
16 + * `PropertyLoad y` do not get mutable ranges
17 + * 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
18 + * as according to the 'co-mutation' of different values
19 + * - Here, we infer that
20 + * - `arrayPush(y, x)` might alias `x` and `y` to each other
21 + * - `setPropertyKey(x, ...)` may mutate both `x` and `y`
22 + * - This pass correctly extends the mutable range of `y`
23 + * - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
24 + * PropertyLoads still don't have a mutable range
25 + *
26 + * Note that the this bug is an edge case. Compiler output is only invalid for:
27 + * - function expressions with
28 + * `enableTransitivelyFreezeFunctionExpressions:false`
29 + * - functions that throw and get retried without clearing the memocache
30 + *
31 + * Found differences in evaluator results
32 + * Non-forget (expected):
33 + * (kind: ok)
34 + * <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
35 + * <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
36 + * Forget:
37 + * (kind: ok)
38 + * <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
39 + * <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
40 + */
41 +function useFoo({a, b}: {a: number, b: number}) {
42 + const x = [];
43 + const y = {value: a};
44 +
45 + arrayPush(x, y); // x and y co-mutate
46 + const y_alias = y;
47 + const cb = () => y_alias.value;
48 + setPropertyByKey(x[0], 'value', b); // might overwrite y.value
49 + return <Stringify cb={cb} shouldInvokeFns={true} />;
50 +}
51 +
52 +export const FIXTURE_ENTRYPOINT = {
53 + fn: useFoo,
54 + params: [{a: 2, b: 10}],
55 + sequentialRenders: [
56 + {a: 2, b: 10},
57 + {a: 2, b: 11},
58 + ],
59 +};
60 +
61 +```
62 +
63 +## Code
64 +
65 +```javascript
66 +import { c as _c } from "react/compiler-runtime";
67 +import { arrayPush, setPropertyByKey, Stringify } from "shared-runtime";
68 +
69 +function useFoo(t0) {
70 + const $ = _c(3);
71 + const { a, b } = t0;
72 + let t1;
73 + if ($[0] !== a || $[1] !== b) {
74 + const x = [];
75 + const y = { value: a };
76 +
77 + arrayPush(x, y);
78 + const y_alias = y;
79 + const cb = () => y_alias.value;
80 + setPropertyByKey(x[0], "value", b);
81 + t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
82 + $[0] = a;
83 + $[1] = b;
84 + $[2] = t1;
85 + } else {
86 + t1 = $[2];
87 + }
88 + return t1;
89 +}
90 +
91 +export const FIXTURE_ENTRYPOINT = {
92 + fn: useFoo,
93 + params: [{ a: 2, b: 10 }],
94 + sequentialRenders: [
95 + { a: 2, b: 10 },
96 + { a: 2, b: 11 },
97 + ],
98 +};
99 +
100 +```
101 +
102 +### Eval output
103 +(kind: ok) <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
104 +<div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-aliased-capture-aliased-mutate.js new
+55
@@ -0,0 +1,55 @@
1 +// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel
2 +import {arrayPush, setPropertyByKey, Stringify} from 'shared-runtime';
3 +
4 +/**
5 + * Repro of a bug fixed in the new aliasing model.
6 + *
7 + * 1. `InferMutableRanges` derives the mutable range of identifiers and their
8 + * aliases from `LoadLocal`, `PropertyLoad`, etc
9 + * - After this pass, y's mutable range only extends to `arrayPush(x, y)`
10 + * - We avoid assigning mutable ranges to loads after y's mutable range, as
11 + * these are working with an immutable value. As a result, `LoadLocal y` and
12 + * `PropertyLoad y` do not get mutable ranges
13 + * 2. `InferReactiveScopeVariables` extends mutable ranges and creates scopes,
14 + * as according to the 'co-mutation' of different values
15 + * - Here, we infer that
16 + * - `arrayPush(y, x)` might alias `x` and `y` to each other
17 + * - `setPropertyKey(x, ...)` may mutate both `x` and `y`
18 + * - This pass correctly extends the mutable range of `y`
19 + * - Since we didn't run `InferMutableRange` logic again, the LoadLocal /
20 + * PropertyLoads still don't have a mutable range
21 + *
22 + * Note that the this bug is an edge case. Compiler output is only invalid for:
23 + * - function expressions with
24 + * `enableTransitivelyFreezeFunctionExpressions:false`
25 + * - functions that throw and get retried without clearing the memocache
26 + *
27 + * Found differences in evaluator results
28 + * Non-forget (expected):
29 + * (kind: ok)
30 + * <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
31 + * <div>{"cb":{"kind":"Function","result":11},"shouldInvokeFns":true}</div>
32 + * Forget:
33 + * (kind: ok)
34 + * <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
35 + * <div>{"cb":{"kind":"Function","result":10},"shouldInvokeFns":true}</div>
36 + */
37 +function useFoo({a, b}: {a: number, b: number}) {
38 + const x = [];
39 + const y = {value: a};
40 +
41 + arrayPush(x, y); // x and y co-mutate
42 + const y_alias = y;
43 + const cb = () => y_alias.value;
44 + setPropertyByKey(x[0], 'value', b); // might overwrite y.value
45 + return <Stringify cb={cb} shouldInvokeFns={true} />;
46 +}
47 +
48 +export const FIXTURE_ENTRYPOINT = {
49 + fn: useFoo,
50 + params: [{a: 2, b: 10}],
51 + sequentialRenders: [
52 + {a: 2, b: 10},
53 + {a: 2, b: 11},
54 + ],
55 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-aliased-capture-mutate.expect.md new
+84
@@ -0,0 +1,84 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel
6 +import {setPropertyByKey, Stringify} from 'shared-runtime';
7 +
8 +/**
9 + * Variation of bug in `bug-aliased-capture-aliased-mutate`.
10 + * Fixed in the new inference model.
11 + *
12 + * Found differences in evaluator results
13 + * Non-forget (expected):
14 + * (kind: ok)
15 + * <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
16 + * <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
17 + * Forget:
18 + * (kind: ok)
19 + * <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
20 + * <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
21 + */
22 +
23 +function useFoo({a}: {a: number, b: number}) {
24 + const arr = [];
25 + const obj = {value: a};
26 +
27 + setPropertyByKey(obj, 'arr', arr);
28 + const obj_alias = obj;
29 + const cb = () => obj_alias.arr.length;
30 + for (let i = 0; i < a; i++) {
31 + arr.push(i);
32 + }
33 + return <Stringify cb={cb} shouldInvokeFns={true} />;
34 +}
35 +
36 +export const FIXTURE_ENTRYPOINT = {
37 + fn: useFoo,
38 + params: [{a: 2}],
39 + sequentialRenders: [{a: 2}, {a: 3}],
40 +};
41 +
42 +```
43 +
44 +## Code
45 +
46 +```javascript
47 +import { c as _c } from "react/compiler-runtime";
48 +import { setPropertyByKey, Stringify } from "shared-runtime";
49 +
50 +function useFoo(t0) {
51 + const $ = _c(2);
52 + const { a } = t0;
53 + let t1;
54 + if ($[0] !== a) {
55 + const arr = [];
56 + const obj = { value: a };
57 +
58 + setPropertyByKey(obj, "arr", arr);
59 + const obj_alias = obj;
60 + const cb = () => obj_alias.arr.length;
61 + for (let i = 0; i < a; i++) {
62 + arr.push(i);
63 + }
64 +
65 + t1 = <Stringify cb={cb} shouldInvokeFns={true} />;
66 + $[0] = a;
67 + $[1] = t1;
68 + } else {
69 + t1 = $[1];
70 + }
71 + return t1;
72 +}
73 +
74 +export const FIXTURE_ENTRYPOINT = {
75 + fn: useFoo,
76 + params: [{ a: 2 }],
77 + sequentialRenders: [{ a: 2 }, { a: 3 }],
78 +};
79 +
80 +```
81 +
82 +### Eval output
83 +(kind: ok) <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
84 +<div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-aliased-capture-mutate.js new
+36
@@ -0,0 +1,36 @@
1 +// @flow @enableTransitivelyFreezeFunctionExpressions:false @enableNewMutationAliasingModel
2 +import {setPropertyByKey, Stringify} from 'shared-runtime';
3 +
4 +/**
5 + * Variation of bug in `bug-aliased-capture-aliased-mutate`.
6 + * Fixed in the new inference model.
7 + *
8 + * Found differences in evaluator results
9 + * Non-forget (expected):
10 + * (kind: ok)
11 + * <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
12 + * <div>{"cb":{"kind":"Function","result":3},"shouldInvokeFns":true}</div>
13 + * Forget:
14 + * (kind: ok)
15 + * <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
16 + * <div>{"cb":{"kind":"Function","result":2},"shouldInvokeFns":true}</div>
17 + */
18 +
19 +function useFoo({a}: {a: number, b: number}) {
20 + const arr = [];
21 + const obj = {value: a};
22 +
23 + setPropertyByKey(obj, 'arr', arr);
24 + const obj_alias = obj;
25 + const cb = () => obj_alias.arr.length;
26 + for (let i = 0; i < a; i++) {
27 + arr.push(i);
28 + }
29 + return <Stringify cb={cb} shouldInvokeFns={true} />;
30 +}
31 +
32 +export const FIXTURE_ENTRYPOINT = {
33 + fn: useFoo,
34 + params: [{a: 2}],
35 + sequentialRenders: [{a: 2}, {a: 3}],
36 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-capturing-func-maybealias-captured-mutate.expect.md new
+111
@@ -0,0 +1,111 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +import {makeArray, mutate} from 'shared-runtime';
7 +
8 +/**
9 + * Bug repro, fixed in the new mutability/aliasing inference.
10 + *
11 + * Previous issue:
12 + *
13 + * Fork of `capturing-func-alias-captured-mutate`, but instead of directly
14 + * aliasing `y` via `[y]`, we make an opaque call.
15 + *
16 + * Note that the bug here is that we don't infer that `a = makeArray(y)`
17 + * potentially captures a context variable into a local variable. As a result,
18 + * we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're
19 + * currently inferring that this lambda captures `y` (for a potential later
20 + * mutation) and simply reads `x`.
21 + *
22 + * Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not
23 + * used when we analyze CallExpressions.
24 + */
25 +function Component({foo, bar}: {foo: number; bar: number}) {
26 + let x = {foo};
27 + let y: {bar: number; x?: {foo: number}} = {bar};
28 + const f0 = function () {
29 + let a = makeArray(y); // a = [y]
30 + let b = x;
31 + // this writes y.x = x
32 + a[0].x = b;
33 + };
34 + f0();
35 + mutate(y.x);
36 + return y;
37 +}
38 +
39 +export const FIXTURE_ENTRYPOINT = {
40 + fn: Component,
41 + params: [{foo: 3, bar: 4}],
42 + sequentialRenders: [
43 + {foo: 3, bar: 4},
44 + {foo: 3, bar: 5},
45 + ],
46 +};
47 +
48 +```
49 +
50 +## Code
51 +
52 +```javascript
53 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
54 +import { makeArray, mutate } from "shared-runtime";
55 +
56 +/**
57 + * Bug repro, fixed in the new mutability/aliasing inference.
58 + *
59 + * Previous issue:
60 + *
61 + * Fork of `capturing-func-alias-captured-mutate`, but instead of directly
62 + * aliasing `y` via `[y]`, we make an opaque call.
63 + *
64 + * Note that the bug here is that we don't infer that `a = makeArray(y)`
65 + * potentially captures a context variable into a local variable. As a result,
66 + * we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're
67 + * currently inferring that this lambda captures `y` (for a potential later
68 + * mutation) and simply reads `x`.
69 + *
70 + * Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not
71 + * used when we analyze CallExpressions.
72 + */
73 +function Component(t0) {
74 + const $ = _c(3);
75 + const { foo, bar } = t0;
76 + let y;
77 + if ($[0] !== bar || $[1] !== foo) {
78 + const x = { foo };
79 + y = { bar };
80 + const f0 = function () {
81 + const a = makeArray(y);
82 + const b = x;
83 +
84 + a[0].x = b;
85 + };
86 +
87 + f0();
88 + mutate(y.x);
89 + $[0] = bar;
90 + $[1] = foo;
91 + $[2] = y;
92 + } else {
93 + y = $[2];
94 + }
95 + return y;
96 +}
97 +
98 +export const FIXTURE_ENTRYPOINT = {
99 + fn: Component,
100 + params: [{ foo: 3, bar: 4 }],
101 + sequentialRenders: [
102 + { foo: 3, bar: 4 },
103 + { foo: 3, bar: 5 },
104 + ],
105 +};
106 +
107 +```
108 +
109 +### Eval output
110 +(kind: ok) {"bar":4,"x":{"foo":3,"wat0":"joe"}}
111 +{"bar":5,"x":{"foo":3,"wat0":"joe"}}
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-capturing-func-maybealias-captured-mutate.ts new
+42
@@ -0,0 +1,42 @@
1 +// @enableNewMutationAliasingModel
2 +import {makeArray, mutate} from 'shared-runtime';
3 +
4 +/**
5 + * Bug repro, fixed in the new mutability/aliasing inference.
6 + *
7 + * Previous issue:
8 + *
9 + * Fork of `capturing-func-alias-captured-mutate`, but instead of directly
10 + * aliasing `y` via `[y]`, we make an opaque call.
11 + *
12 + * Note that the bug here is that we don't infer that `a = makeArray(y)`
13 + * potentially captures a context variable into a local variable. As a result,
14 + * we don't understand that `a[0].x = b` captures `x` into `y` -- instead, we're
15 + * currently inferring that this lambda captures `y` (for a potential later
16 + * mutation) and simply reads `x`.
17 + *
18 + * Concretely `InferReferenceEffects.hasContextRefOperand` is incorrectly not
19 + * used when we analyze CallExpressions.
20 + */
21 +function Component({foo, bar}: {foo: number; bar: number}) {
22 + let x = {foo};
23 + let y: {bar: number; x?: {foo: number}} = {bar};
24 + const f0 = function () {
25 + let a = makeArray(y); // a = [y]
26 + let b = x;
27 + // this writes y.x = x
28 + a[0].x = b;
29 + };
30 + f0();
31 + mutate(y.x);
32 + return y;
33 +}
34 +
35 +export const FIXTURE_ENTRYPOINT = {
36 + fn: Component,
37 + params: [{foo: 3, bar: 4}],
38 + sequentialRenders: [
39 + {foo: 3, bar: 4},
40 + {foo: 3, bar: 5},
41 + ],
42 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-false-positive-ref-validation-in-use-effect.expect.md new
+88
@@ -0,0 +1,88 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel
6 +import {useCallback, useEffect, useRef} from 'react';
7 +import {useHook} from 'shared-runtime';
8 +
9 +// This was a false positive "can't freeze mutable function" in the old
10 +// inference model, fixed in the new inference model.
11 +function Component() {
12 + const params = useHook();
13 + const update = useCallback(
14 + partialParams => {
15 + const nextParams = {
16 + ...params,
17 + ...partialParams,
18 + };
19 + nextParams.param = 'value';
20 + console.log(nextParams);
21 + },
22 + [params]
23 + );
24 + const ref = useRef(null);
25 + useEffect(() => {
26 + if (ref.current === null) {
27 + update();
28 + }
29 + }, [update]);
30 +
31 + return 'ok';
32 +}
33 +
34 +```
35 +
36 +## Code
37 +
38 +```javascript
39 +import { c as _c } from "react/compiler-runtime"; // @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel
40 +import { useCallback, useEffect, useRef } from "react";
41 +import { useHook } from "shared-runtime";
42 +
43 +// This was a false positive "can't freeze mutable function" in the old
44 +// inference model, fixed in the new inference model.
45 +function Component() {
46 + const $ = _c(5);
47 + const params = useHook();
48 + let t0;
49 + if ($[0] !== params) {
50 + t0 = (partialParams) => {
51 + const nextParams = { ...params, ...partialParams };
52 +
53 + nextParams.param = "value";
54 + console.log(nextParams);
55 + };
56 + $[0] = params;
57 + $[1] = t0;
58 + } else {
59 + t0 = $[1];
60 + }
61 + const update = t0;
62 +
63 + const ref = useRef(null);
64 + let t1;
65 + let t2;
66 + if ($[2] !== update) {
67 + t1 = () => {
68 + if (ref.current === null) {
69 + update();
70 + }
71 + };
72 +
73 + t2 = [update];
74 + $[2] = update;
75 + $[3] = t1;
76 + $[4] = t2;
77 + } else {
78 + t1 = $[3];
79 + t2 = $[4];
80 + }
81 + useEffect(t1, t2);
82 + return "ok";
83 +}
84 +
85 +```
86 +
87 +### Eval output
88 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-false-positive-ref-validation-in-use-effect.js new
+28
@@ -0,0 +1,28 @@
1 +// @validateNoFreezingKnownMutableFunctions @enableNewMutationAliasingModel
2 +import {useCallback, useEffect, useRef} from 'react';
3 +import {useHook} from 'shared-runtime';
4 +
5 +// This was a false positive "can't freeze mutable function" in the old
6 +// inference model, fixed in the new inference model.
7 +function Component() {
8 + const params = useHook();
9 + const update = useCallback(
10 + partialParams => {
11 + const nextParams = {
12 + ...params,
13 + ...partialParams,
14 + };
15 + nextParams.param = 'value';
16 + console.log(nextParams);
17 + },
18 + [params]
19 + );
20 + const ref = useRef(null);
21 + useEffect(() => {
22 + if (ref.current === null) {
23 + update();
24 + }
25 + }, [update]);
26 +
27 + return 'ok';
28 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-invalid-phi-as-dependency.expect.md new
+80
@@ -0,0 +1,80 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
7 +
8 +/**
9 + * Fixture showing an edge case for ReactiveScope variable propagation.
10 + * Fixed in the new inference model
11 + *
12 + * Found differences in evaluator results
13 + * Non-forget (expected):
14 + * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
15 + * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
16 + * Forget:
17 + * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
18 + * [[ (exception in render) Error: invariant broken ]]
19 + *
20 + */
21 +function Component() {
22 + const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
23 + const boxedInner = [obj?.inner];
24 + useIdentity(null);
25 + mutate(obj);
26 + if (boxedInner[0] !== obj?.inner) {
27 + throw new Error('invariant broken');
28 + }
29 + return <Stringify obj={obj} inner={boxedInner} />;
30 +}
31 +
32 +export const FIXTURE_ENTRYPOINT = {
33 + fn: Component,
34 + params: [{arg: 0}],
35 + sequentialRenders: [{arg: 0}, {arg: 1}],
36 +};
37 +
38 +```
39 +
40 +## Code
41 +
42 +```javascript
43 +// @enableNewMutationAliasingModel
44 +import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime";
45 +
46 +/**
47 + * Fixture showing an edge case for ReactiveScope variable propagation.
48 + * Fixed in the new inference model
49 + *
50 + * Found differences in evaluator results
51 + * Non-forget (expected):
52 + * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
53 + * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
54 + * Forget:
55 + * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
56 + * [[ (exception in render) Error: invariant broken ]]
57 + *
58 + */
59 +function Component() {
60 + const obj = CONST_TRUE ? { inner: { value: "hello" } } : null;
61 + const boxedInner = [obj?.inner];
62 + useIdentity(null);
63 + mutate(obj);
64 + if (boxedInner[0] !== obj?.inner) {
65 + throw new Error("invariant broken");
66 + }
67 + return <Stringify obj={obj} inner={boxedInner} />;
68 +}
69 +
70 +export const FIXTURE_ENTRYPOINT = {
71 + fn: Component,
72 + params: [{ arg: 0 }],
73 + sequentialRenders: [{ arg: 0 }, { arg: 1 }],
74 +};
75 +
76 +```
77 +
78 +### Eval output
79 +(kind: ok) <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
80 +<div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-invalid-phi-as-dependency.tsx new
+32
@@ -0,0 +1,32 @@
1 +// @enableNewMutationAliasingModel
2 +import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime';
3 +
4 +/**
5 + * Fixture showing an edge case for ReactiveScope variable propagation.
6 + * Fixed in the new inference model
7 + *
8 + * Found differences in evaluator results
9 + * Non-forget (expected):
10 + * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
11 + * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
12 + * Forget:
13 + * <div>{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}</div>
14 + * [[ (exception in render) Error: invariant broken ]]
15 + *
16 + */
17 +function Component() {
18 + const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null;
19 + const boxedInner = [obj?.inner];
20 + useIdentity(null);
21 + mutate(obj);
22 + if (boxedInner[0] !== obj?.inner) {
23 + throw new Error('invariant broken');
24 + }
25 + return <Stringify obj={obj} inner={boxedInner} />;
26 +}
27 +
28 +export const FIXTURE_ENTRYPOINT = {
29 + fn: Component,
30 + params: [{arg: 0}],
31 + sequentialRenders: [{arg: 0}, {arg: 1}],
32 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.expect.md new
+91
@@ -0,0 +1,91 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +import {identity, mutate} from 'shared-runtime';
7 +
8 +/**
9 + * Fixed in the new inference model.
10 + *
11 + * Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
12 + * with the mutation hoisted to a named variable instead of being directly
13 + * inlined into the Object key.
14 + *
15 + * Found differences in evaluator results
16 + * Non-forget (expected):
17 + * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
18 + * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
19 + * Forget:
20 + * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
21 + * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
22 + */
23 +function Component(props) {
24 + const key = {};
25 + const tmp = (mutate(key), key);
26 + const context = {
27 + // Here, `tmp` is frozen (as it's inferred to be a primitive/string)
28 + [tmp]: identity([props.value]),
29 + };
30 + mutate(key);
31 + return [context, key];
32 +}
33 +
34 +export const FIXTURE_ENTRYPOINT = {
35 + fn: Component,
36 + params: [{value: 42}],
37 + sequentialRenders: [{value: 42}, {value: 42}],
38 +};
39 +
40 +```
41 +
42 +## Code
43 +
44 +```javascript
45 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
46 +import { identity, mutate } from "shared-runtime";
47 +
48 +/**
49 + * Fixed in the new inference model.
50 + *
51 + * Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
52 + * with the mutation hoisted to a named variable instead of being directly
53 + * inlined into the Object key.
54 + *
55 + * Found differences in evaluator results
56 + * Non-forget (expected):
57 + * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
58 + * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
59 + * Forget:
60 + * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
61 + * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
62 + */
63 +function Component(props) {
64 + const $ = _c(2);
65 + let t0;
66 + if ($[0] !== props.value) {
67 + const key = {};
68 + const tmp = (mutate(key), key);
69 + const context = { [tmp]: identity([props.value]) };
70 +
71 + mutate(key);
72 + t0 = [context, key];
73 + $[0] = props.value;
74 + $[1] = t0;
75 + } else {
76 + t0 = $[1];
77 + }
78 + return t0;
79 +}
80 +
81 +export const FIXTURE_ENTRYPOINT = {
82 + fn: Component,
83 + params: [{ value: 42 }],
84 + sequentialRenders: [{ value: 42 }, { value: 42 }],
85 +};
86 +
87 +```
88 +
89 +### Eval output
90 +(kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
91 +[{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-object-expression-computed-key-modified-during-after-construction-hoisted-sequence-expr.js new
+34
@@ -0,0 +1,34 @@
1 +// @enableNewMutationAliasingModel
2 +import {identity, mutate} from 'shared-runtime';
3 +
4 +/**
5 + * Fixed in the new inference model.
6 + *
7 + * Bug: copy of error.todo-object-expression-computed-key-modified-during-after-construction-sequence-expr
8 + * with the mutation hoisted to a named variable instead of being directly
9 + * inlined into the Object key.
10 + *
11 + * Found differences in evaluator results
12 + * Non-forget (expected):
13 + * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
14 + * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
15 + * Forget:
16 + * (kind: ok) [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe"}]
17 + * [{"[object Object]":[42]},{"wat0":"joe","wat1":"joe","wat2":"joe"}]
18 + */
19 +function Component(props) {
20 + const key = {};
21 + const tmp = (mutate(key), key);
22 + const context = {
23 + // Here, `tmp` is frozen (as it's inferred to be a primitive/string)
24 + [tmp]: identity([props.value]),
25 + };
26 + mutate(key);
27 + return [context, key];
28 +}
29 +
30 +export const FIXTURE_ENTRYPOINT = {
31 + fn: Component,
32 + params: [{value: 42}],
33 + sequentialRenders: [{value: 42}, {value: 42}],
34 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-separate-memoization-due-to-callback-capturing.expect.md new
+149
@@ -0,0 +1,149 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableNewMutationAliasingModel
6 +import {ValidateMemoization} from 'shared-runtime';
7 +
8 +const Codes = {
9 + en: {name: 'English'},
10 + ja: {name: 'Japanese'},
11 + ko: {name: 'Korean'},
12 + zh: {name: 'Chinese'},
13 +};
14 +
15 +function Component(a) {
16 + let keys;
17 + if (a) {
18 + keys = Object.keys(Codes);
19 + } else {
20 + return null;
21 + }
22 + const options = keys.map(code => {
23 + // In the old inference model, `keys` was assumed to be mutated bc
24 + // this callback captures its input into its output, and the return
25 + // is treated as a mutation since it's a function expression. The new
26 + // model understands that `code` is captured but not mutated.
27 + const country = Codes[code];
28 + return {
29 + name: country.name,
30 + code,
31 + };
32 + });
33 + return (
34 + <>
35 + <ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
36 + <ValidateMemoization
37 + inputs={[]}
38 + output={options}
39 + onlyCheckCompiled={true}
40 + />
41 + </>
42 + );
43 +}
44 +
45 +export const FIXTURE_ENTRYPOINT = {
46 + fn: Component,
47 + params: [{a: false}],
48 + sequentialRenders: [
49 + {a: false},
50 + {a: true},
51 + {a: true},
52 + {a: false},
53 + {a: true},
54 + {a: false},
55 + ],
56 +};
57 +
58 +```
59 +
60 +## Code
61 +
62 +```javascript
63 +import { c as _c } from "react/compiler-runtime"; // @enableNewMutationAliasingModel
64 +import { ValidateMemoization } from "shared-runtime";
65 +
66 +const Codes = {
67 + en: { name: "English" },
68 + ja: { name: "Japanese" },
69 + ko: { name: "Korean" },
70 + zh: { name: "Chinese" },
71 +};
72 +
73 +function Component(a) {
74 + const $ = _c(4);
75 + let keys;
76 + if (a) {
77 + let t0;
78 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
79 + t0 = Object.keys(Codes);
80 + $[0] = t0;
81 + } else {
82 + t0 = $[0];
83 + }
84 + keys = t0;
85 + } else {
86 + return null;
87 + }
88 + let t0;
89 + if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
90 + t0 = keys.map(_temp);
91 + $[1] = t0;
92 + } else {
93 + t0 = $[1];
94 + }
95 + const options = t0;
96 + let t1;
97 + if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
98 + t1 = (
99 + <ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
100 + );
101 + $[2] = t1;
102 + } else {
103 + t1 = $[2];
104 + }
105 + let t2;
106 + if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
107 + t2 = (
108 + <>
109 + {t1}
110 + <ValidateMemoization
111 + inputs={[]}
112 + output={options}
113 + onlyCheckCompiled={true}
114 + />
115 + </>
116 + );
117 + $[3] = t2;
118 + } else {
119 + t2 = $[3];
120 + }
121 + return t2;
122 +}
123 +function _temp(code) {
124 + const country = Codes[code];
125 + return { name: country.name, code };
126 +}
127 +
128 +export const FIXTURE_ENTRYPOINT = {
129 + fn: Component,
130 + params: [{ a: false }],
131 + sequentialRenders: [
132 + { a: false },
133 + { a: true },
134 + { a: true },
135 + { a: false },
136 + { a: true },
137 + { a: false },
138 + ],
139 +};
140 +
141 +```
142 +
143 +### Eval output
144 +(kind: ok) <div>{"inputs":[],"output":["en","ja","ko","zh"]}</div><div>{"inputs":[],"output":[{"name":"English","code":"en"},{"name":"Japanese","code":"ja"},{"name":"Korean","code":"ko"},{"name":"Chinese","code":"zh"}]}</div>
145 +<div>{"inputs":[],"output":["en","ja","ko","zh"]}</div><div>{"inputs":[],"output":[{"name":"English","code":"en"},{"name":"Japanese","code":"ja"},{"name":"Korean","code":"ko"},{"name":"Chinese","code":"zh"}]}</div>
146 +<div>{"inputs":[],"output":["en","ja","ko","zh"]}</div><div>{"inputs":[],"output":[{"name":"English","code":"en"},{"name":"Japanese","code":"ja"},{"name":"Korean","code":"ko"},{"name":"Chinese","code":"zh"}]}</div>
147 +<div>{"inputs":[],"output":["en","ja","ko","zh"]}</div><div>{"inputs":[],"output":[{"name":"English","code":"en"},{"name":"Japanese","code":"ja"},{"name":"Korean","code":"ko"},{"name":"Chinese","code":"zh"}]}</div>
148 +<div>{"inputs":[],"output":["en","ja","ko","zh"]}</div><div>{"inputs":[],"output":[{"name":"English","code":"en"},{"name":"Japanese","code":"ja"},{"name":"Korean","code":"ko"},{"name":"Chinese","code":"zh"}]}</div>
149 +<div>{"inputs":[],"output":["en","ja","ko","zh"]}</div><div>{"inputs":[],"output":[{"name":"English","code":"en"},{"name":"Japanese","code":"ja"},{"name":"Korean","code":"ko"},{"name":"Chinese","code":"zh"}]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-separate-memoization-due-to-callback-capturing.js new
+52
@@ -0,0 +1,52 @@
1 +// @enableNewMutationAliasingModel
2 +import {ValidateMemoization} from 'shared-runtime';
3 +
4 +const Codes = {
5 + en: {name: 'English'},
6 + ja: {name: 'Japanese'},
7 + ko: {name: 'Korean'},
8 + zh: {name: 'Chinese'},
9 +};
10 +
11 +function Component(a) {
12 + let keys;
13 + if (a) {
14 + keys = Object.keys(Codes);
15 + } else {
16 + return null;
17 + }
18 + const options = keys.map(code => {
19 + // In the old inference model, `keys` was assumed to be mutated bc
20 + // this callback captures its input into its output, and the return
21 + // is treated as a mutation since it's a function expression. The new
22 + // model understands that `code` is captured but not mutated.
23 + const country = Codes[code];
24 + return {
25 + name: country.name,
26 + code,
27 + };
28 + });
29 + return (
30 + <>
31 + <ValidateMemoization inputs={[]} output={keys} onlyCheckCompiled={true} />
32 + <ValidateMemoization
33 + inputs={[]}
34 + output={options}
35 + onlyCheckCompiled={true}
36 + />
37 + </>
38 + );
39 +}
40 +
41 +export const FIXTURE_ENTRYPOINT = {
42 + fn: Component,
43 + params: [{a: false}],
44 + sequentialRenders: [
45 + {a: false},
46 + {a: true},
47 + {a: true},
48 + {a: false},
49 + {a: true},
50 + {a: false},
51 + ],
52 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-uncalled-function-capturing-mutable-values-memoizes-with-captures-values.expect.md deleted
-77
@@ -1,77 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -// @flow
6 -/**
7 - * This hook returns a function that when called with an input object,
8 - * will return the result of mapping that input with the supplied map
9 - * function. Results are cached, so if the same input is passed again,
10 - * the same output object will be returned.
11 - *
12 - * Note that this technically violates the rules of React and is unsafe:
13 - * hooks must return immutable objects and be pure, and a function which
14 - * captures and mutates a value when called is inherently not pure.
15 - *
16 - * However, in this case it is technically safe _if_ the mapping function
17 - * is pure *and* the resulting objects are never modified. This is because
18 - * the function only caches: the result of `returnedFunction(someInput)`
19 - * strictly depends on `returnedFunction` and `someInput`, and cannot
20 - * otherwise change over time.
21 - */
22 -hook useMemoMap<TInput: interface {}, TOutput>(
23 - map: TInput => TOutput
24 -): TInput => TOutput {
25 - return useMemo(() => {
26 - // The original issue is that `cache` was not memoized together with the returned
27 - // function. This was because neither appears to ever be mutated — the function
28 - // is known to mutate `cache` but the function isn't called.
29 - //
30 - // The fix is to detect cases like this — functions that are mutable but not called -
31 - // and ensure that their mutable captures are aliased together into the same scope.
32 - const cache = new WeakMap<TInput, TOutput>();
33 - return input => {
34 - let output = cache.get(input);
35 - if (output == null) {
36 - output = map(input);
37 - cache.set(input, output);
38 - }
39 - return output;
40 - };
41 - }, [map]);
42 -}
43 -
44 -```
45 -
46 -## Code
47 -
48 -```javascript
49 -import { c as _c } from "react/compiler-runtime";
50 -
51 -function useMemoMap(map) {
52 - const $ = _c(2);
53 - let t0;
54 - let t1;
55 - if ($[0] !== map) {
56 - const cache = new WeakMap();
57 - t1 = (input) => {
58 - let output = cache.get(input);
59 - if (output == null) {
60 - output = map(input);
61 - cache.set(input, output);
62 - }
63 - return output;
64 - };
65 - $[0] = map;
66 - $[1] = t1;
67 - } else {
68 - t1 = $[1];
69 - }
70 - t0 = t1;
71 - return t0;
72 -}
73 -
74 -```
75 -
76 -### Eval output
77 -(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/snap/src/SproutTodoFilter.ts
+1
@@ -486,6 +486,7 @@ const skipFilter = new Set([
486 'todo.lower-context-access-array-destructuring',
487 'lower-context-selector-simple',
488 'lower-context-acess-multiple',
489 + 'bug-separate-memoization-due-to-callback-capturing',
490 ]);
491
492 export default skipFilter;