@samitouri / QOS-React-1 / commits / bde30f0284

[be] Remove ValidateFrozenLambdas

This pass doesn't really make sense in light of `@enableTransitivelyFreezeFunctionExpressions`. The original idea of ValidateFrozenLambdas was that trying to pass a "mutable" lambda to a frozen value was invalid. But since then we've realized that the better heuristic is that freezing a lambda is transitive.

Joe Savona committed Feb 13, 2024 at 16:45 UTC bde30f02840ad49119c2433d54ad352e95f53420
13 files changed +3 -328
compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts
-5
@@ -70,7 +70,6 @@ import {
70 import { assertExhaustive } from "../Utils/utils";
71 import {
72 validateContextVariableLValues,
73 - validateFrozenLambdas,
73 validateHooksUsage,
74 validateMemoizedEffectDependencies,
75 validateNoRefAccessInRender,
@@ -161,10 +160,6 @@ function* runWithEnvironment(
160 inferReferenceEffects(hir);
161 yield log({ kind: "hir", name: "InferReferenceEffects", value: hir });
162
164 - if (env.config.validateFrozenLambdas) {
165 - validateFrozenLambdas(hir);
166 - }
167 -
163 // Note: Has to come after infer reference effects because "dead" code may still affect inference
164 deadCodeElimination(hir);
165 yield log({ kind: "hir", name: "DeadCodeElimination", value: hir });
compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts
-6
@@ -180,12 +180,6 @@ const EnvironmentConfigSchema = z.object({
180 */
181 validateRefAccessDuringRenderFunctionExpressions: z.boolean().default(false),
182
183 - /*
184 - * Validate that mutable lambdas are not passed where a frozen value is expected, since mutable
185 - * lambdas cannot be frozen. The only mutation allowed inside a frozen lambda is of ref values.
186 - */
187 - validateFrozenLambdas: z.boolean().default(false),
188 -
183 /*
184 * Validates that setState is not unconditionally called during render, as it can lead to
185 * infinite loops.
compiler/packages/babel-plugin-react-forget/src/Validation/ValidateFrozenLambdas.ts deleted
-159
@@ -1,159 +0,0 @@
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 - CompilerErrorDetail,
11 - ErrorSeverity,
12 -} from "../CompilerError";
13 -import {
14 - Effect,
15 - FunctionExpression,
16 - HIRFunction,
17 - IdentifierId,
18 - ObjectMethod,
19 - Place,
20 - isRefValueType,
21 - isUseRefType,
22 -} from "../HIR/HIR";
23 -import {
24 - eachInstructionValueOperand,
25 - eachTerminalOperand,
26 -} from "../HIR/visitors";
27 -
28 -/*
29 - * Various APIs in React take ownership of the values passed to them, such that it is invalid
30 - * to subsequently modify those values. Examples include:
31 - * - Passing a value as a prop to JSX. Subsequently mutating this value will result in undefined
32 - * behavior, since the mutation may or may not be observed depending on when the child re-renders.
33 - * In addition, the value may be used as an input to memoization in children, and mutation could
34 - * invalidate that memoization.
35 - * - Passing a value to `useState()`, for the same reason.
36 - * - Passing a value to a hook, for the same reason.
37 - *
38 - * Most "normal" data types (objects, arrays, etc) can be "frozen" when passed to a React API simply
39 - * by not calling any mutating methods on them. However, mutable lambdas are an exception: if a lambda
40 - * has side-effects, there is no way to do something to the lambda that would allow calling it without
41 - * triggering those side effects. The only thing a developer could do is not call the lambda, but developers
42 - * also have no way of knowing that they can't call the lambda.
43 - *
44 - * From a type system perspective, the above APIs that "take ownership" of their values really accept
45 - * *already frozen* values as input. Thus it is invalid to pass a value that cannot be frozen to these APIs,
46 - * and it is therefore invalid to pass a mutable lambda.
47 - *
48 - * This pass validates the above rule. Note that this validation can by bypassed by storing a mutable lambda
49 - * inside some value (eg as an array element or object property). In these cases we trust that the developer
50 - * is not breaking the rules. The goal of this validation is to find cases that are provably wrong and help
51 - * the developer fix the mistake earlier.
52 - */
53 -export function validateFrozenLambdas(fn: HIRFunction): void {
54 - const state = new State();
55 -
56 - const errors = new CompilerError();
57 - for (const [, block] of fn.body.blocks) {
58 - for (const phi of block.phis) {
59 - for (const [, operand] of phi.operands) {
60 - const resolvedId = state.temporaries.get(operand.id) ?? operand.id;
61 - const lambda = state.lambdas.get(resolvedId);
62 - if (lambda !== undefined) {
63 - state.lambdas.set(phi.id.id, lambda);
64 - break;
65 - }
66 - }
67 - }
68 - for (const instr of block.instructions) {
69 - switch (instr.value.kind) {
70 - case "ObjectMethod":
71 - case "FunctionExpression": {
72 - if (
73 - instr.value.loweredFunc.dependencies.some(
74 - (place) =>
75 - place.effect === Effect.Mutate &&
76 - !isRefValueType(place.identifier) &&
77 - !isUseRefType(place.identifier)
78 - )
79 - ) {
80 - state.lambdas.set(instr.lvalue.identifier.id, instr.value);
81 - }
82 - break;
83 - }
84 - case "LoadLocal": {
85 - const resolvedId =
86 - state.temporaries.get(instr.value.place.identifier.id) ??
87 - instr.value.place.identifier.id;
88 - state.temporaries.set(instr.lvalue.identifier.id, resolvedId);
89 - break;
90 - }
91 - case "StoreLocal": {
92 - const resolvedId =
93 - state.temporaries.get(instr.value.value.identifier.id) ??
94 - instr.value.value.identifier.id;
95 - state.temporaries.set(
96 - instr.value.lvalue.place.identifier.id,
97 - resolvedId
98 - );
99 - break;
100 - }
101 - default: {
102 - for (const operand of eachInstructionValueOperand(instr.value)) {
103 - const operandError = validateOperand(operand, state);
104 - if (operandError !== null) {
105 - errors.pushErrorDetail(operandError);
106 - }
107 - }
108 - }
109 - }
110 - }
111 - for (const operand of eachTerminalOperand(block.terminal)) {
112 - const operandError = validateOperand(operand, state);
113 - if (operandError !== null) {
114 - errors.pushErrorDetail(operandError);
115 - }
116 - }
117 - }
118 - if (errors.hasErrors()) {
119 - throw errors;
120 - }
121 -}
122 -
123 -class State {
124 - lambdas: Map<IdentifierId, FunctionExpression | ObjectMethod> = new Map();
125 - temporaries: Map<IdentifierId, IdentifierId> = new Map();
126 -}
127 -
128 -function validateOperand(
129 - operand: Place,
130 - state: State
131 -): CompilerErrorDetail | null {
132 - if (operand.effect === Effect.Freeze) {
133 - const operandId =
134 - state.temporaries.get(operand.identifier.id) ?? operand.identifier.id;
135 - const lambda = state.lambdas.get(operandId);
136 - if (lambda !== undefined) {
137 - /*
138 - * TODO: these seem to always be null, we should try to preserve original
139 - * names from source
140 - * TODO: figure out how to print object methods as they don't have names
141 - */
142 - const description =
143 - lambda.kind === "FunctionExpression" &&
144 - lambda.name !== null &&
145 - operand.identifier.name !== null
146 - ? `\`${lambda.name}\` is a function that may mutate \`${operand.identifier.name}\`. If you must mutate \`${operand.identifier.name}\` try using a React API like useState and use its setter function instead`
147 - : null;
148 - return new CompilerErrorDetail({
149 - description,
150 - loc: typeof operand.loc !== "symbol" ? operand.loc : null,
151 - reason:
152 - "This mutates a variable that is managed by React, where an immutable value or a function was expected",
153 - severity: ErrorSeverity.InvalidReact,
154 - suggestions: null,
155 - });
156 - }
157 - }
158 - return null;
159 -}
compiler/packages/babel-plugin-react-forget/src/Validation/index.ts
-1
@@ -6,7 +6,6 @@
6 */
7
8 export { validateContextVariableLValues } from "./ValidateContextVariableLValues";
9 -export { validateFrozenLambdas } from "./ValidateFrozenLambdas";
9 export { validateHooksUsage } from "./ValidateHooksUsage";
10 export { validateMemoizedEffectDependencies } from "./ValidateMemoizedEffectDependencies";
11 export { validateNoRefAccessInRender } from "./ValidateNoRefAccesInRender";
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-capture-func-passed-to-jsx.expect.md deleted
-27
@@ -1,27 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -// @validateFrozenLambdas
6 -function component(a, b) {
7 - let y = { b };
8 - let z = { a };
9 - let x = function () {
10 - z.a = 2;
11 - y.b;
12 - };
13 - let t = <Foo x={x}></Foo>;
14 - mutate(x); // x should be frozen here
15 - return t;
16 -}
17 -
18 -```
19 -
20 -
21 -## Error
22 -
23 -```
24 -[ReactForget] InvalidReact: This mutates a variable that is managed by React, where an immutable value or a function was expected (9:9)
25 -```
26 -
27 -
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-capture-func-passed-to-jsx.js deleted
-12
@@ -1,12 +0,0 @@
1 -// @validateFrozenLambdas
2 -function component(a, b) {
3 - let y = { b };
4 - let z = { a };
5 - let x = function () {
6 - z.a = 2;
7 - y.b;
8 - };
9 - let t = <Foo x={x}></Foo>;
10 - mutate(x); // x should be frozen here
11 - return t;
12 -}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-freeze-conditionally-mutable-lambda.expect.md deleted
-32
@@ -1,32 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -// @validateFrozenLambdas
6 -function Component(props) {
7 - const x = {};
8 - let fn;
9 - if (props.cond) {
10 - // mutable
11 - fn = () => {
12 - x.value = props.value;
13 - };
14 - } else {
15 - // immutable
16 - fn = () => {
17 - x.value;
18 - };
19 - }
20 - return fn;
21 -}
22 -
23 -```
24 -
25 -
26 -## Error
27 -
28 -```
29 -[ReactForget] InvalidReact: This mutates a variable that is managed by React, where an immutable value or a function was expected (16:16)
30 -```
31 -
32 -
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-freeze-conditionally-mutable-lambda.js deleted
-17
@@ -1,17 +0,0 @@
1 -// @validateFrozenLambdas
2 -function Component(props) {
3 - const x = {};
4 - let fn;
5 - if (props.cond) {
6 - // mutable
7 - fn = () => {
8 - x.value = props.value;
9 - };
10 - } else {
11 - // immutable
12 - fn = () => {
13 - x.value;
14 - };
15 - }
16 - return fn;
17 -}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-freeze-mutable-lambda-mutate-local.expect.md deleted
-25
@@ -1,25 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -// @validateFrozenLambdas
6 -function Component(props) {
7 - const x = {};
8 - const onChange = (e) => {
9 - // INVALID! should use copy-on-write and pass the new value
10 - x.value = e.target.value;
11 - setX(x);
12 - };
13 - return <input value={x.value} onChange={onChange} />;
14 -}
15 -
16 -```
17 -
18 -
19 -## Error
20 -
21 -```
22 -[ReactForget] InvalidReact: This mutates a variable that is managed by React, where an immutable value or a function was expected (9:9)
23 -```
24 -
25 -
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-freeze-mutable-lambda-mutate-local.js deleted
-10
@@ -1,10 +0,0 @@
1 -// @validateFrozenLambdas
2 -function Component(props) {
3 - const x = {};
4 - const onChange = (e) => {
5 - // INVALID! should use copy-on-write and pass the new value
6 - x.value = e.target.value;
7 - setX(x);
8 - };
9 - return <input value={x.value} onChange={onChange} />;
10 -}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-freeze-mutable-lambda-reassign-local.expect.md deleted
-23
@@ -1,23 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -// @validateFrozenLambdas
6 -function Component(props) {
7 - let x = "";
8 - const onChange = (e) => {
9 - x = e.target.value;
10 - };
11 - return <input value={x} onChange={onChange} />;
12 -}
13 -
14 -```
15 -
16 -
17 -## Error
18 -
19 -```
20 -[ReactForget] InvalidReact: This mutates a variable that is managed by React, where an immutable value or a function was expected (7:7)
21 -```
22 -
23 -
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-freeze-mutable-lambda-reassign-local.js deleted
-8
@@ -1,8 +0,0 @@
1 -// @validateFrozenLambdas
2 -function Component(props) {
3 - let x = "";
4 - const onChange = (e) => {
5 - x = e.target.value;
6 - };
7 - return <input value={x} onChange={onChange} />;
8 -}
compiler/packages/babel-plugin-react-forget/src/__tests__/parseConfigPragma-test.ts
+3 -3
@@ -14,16 +14,16 @@ describe("parseConfigPragma()", () => {
14 // Validate defaults first to make sure that the parser is getting the value from the pragma,
15 // and not just missing it and getting the default value
16 expect(defaultConfig.enableForest).toBe(false);
17 - expect(defaultConfig.validateFrozenLambdas).toBe(false);
17 + expect(defaultConfig.validateRefAccessDuringRender).toBe(false);
18 expect(defaultConfig.memoizeJsxElements).toBe(true);
19
20 const config = parseConfigPragma(
21 - "@enableForest @validateFrozenLambdas:true @memoizeJsxElements:false"
21 + "@enableForest @validateRefAccessDuringRender:true @memoizeJsxElements:false"
22 );
23 expect(config).toEqual({
24 ...defaultConfig,
25 enableForest: true,
26 - validateFrozenLambdas: true,
26 + validateRefAccessDuringRender: true,
27 memoizeJsxElements: false,
28 });
29 });