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