[optim] NewExpressions do not mutate the caller (class type)
``` function Foo() { const MyClass = getClass(); // following line is not expected to change MyClass return new MyClass(); } ```
Mofei Zhang committed
Mar 11, 2024 at 13:03 UTC
2fc910d58025d143562fe0f2287af9d510a91946
3 files changed
+126
-5
compiler/packages/babel-plugin-react-forget/src/Inference/InferReferenceEffects.ts
+34
-5
@@ -37,6 +37,7 @@ import {
37
printSourceLocation,
38
} from "../HIR/PrintHIR";
39
import {
40
+ eachCallArgument,
41
eachInstructionOperand,
42
eachInstructionValueOperand,
43
eachPatternOperand,
@@ -850,15 +851,43 @@ function inferBlock(
851
break;
852
}
853
case "NewExpression": {
854
+ /**
855
+ * For new expressions, we infer a `read` effect on the Class / Function type
856
+ * to avoid extending mutable ranges of locally created classes, e.g.
857
+ * ```js
858
+ * const MyClass = getClass();
859
+ * const value = new MyClass(val1, val2)
860
+ * ^ (read) ^ (conditionally mutate)
861
+ * ```
862
+ *
863
+ * Risks:
864
+ * Classes / functions created during render could technically capture and
865
+ * mutate their enclosing scope, which we currently do not detect.
866
+ */
867
valueKind = {
868
kind: ValueKind.Mutable,
869
reason: new Set([ValueReason.Other]),
870
};
857
- effect = {
858
- kind: Effect.ConditionallyMutate,
859
- reason: ValueReason.Other,
860
- };
861
- break;
871
+ state.reference(
872
+ instrValue.callee,
873
+ functionEffects,
874
+ Effect.Read,
875
+ ValueReason.Other
876
+ );
877
+
878
+ for (const operand of eachCallArgument(instrValue.args)) {
879
+ state.reference(
880
+ operand,
881
+ functionEffects,
882
+ Effect.ConditionallyMutate,
883
+ ValueReason.Other
884
+ );
885
+ }
886
+
887
+ state.initialize(instrValue, valueKind);
888
+ state.define(instr.lvalue, instrValue);
889
+ instr.lvalue.effect = lvalueEffect;
890
+ continue;
891
}
892
case "ObjectExpression": {
893
valueKind = hasContextRefOperand(state, instrValue)
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/new-does-not-mutate-class.expect.md
new
+77
@@ -0,0 +1,77 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { identity } from "shared-runtime";
6
+
7
+class Foo {}
8
+function Component({ val }) {
9
+ const MyClass = identity(Foo);
10
+ const x = [val];
11
+ const y = new MyClass();
12
+
13
+ return [x, y];
14
+}
15
+
16
+export const FIXTURE_ENTRYPOINT = {
17
+ fn: Component,
18
+ params: [{ val: 0 }],
19
+};
20
+
21
+```
22
+
23
+## Code
24
+
25
+```javascript
26
+import { unstable_useMemoCache as useMemoCache } from "react";
27
+import { identity } from "shared-runtime";
28
+
29
+class Foo {}
30
+function Component(t0) {
31
+ const $ = useMemoCache(6);
32
+ const { val } = t0;
33
+ let t1;
34
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
35
+ t1 = identity(Foo);
36
+ $[0] = t1;
37
+ } else {
38
+ t1 = $[0];
39
+ }
40
+ const MyClass = t1;
41
+ let t2;
42
+ if ($[1] !== val) {
43
+ t2 = [val];
44
+ $[1] = val;
45
+ $[2] = t2;
46
+ } else {
47
+ t2 = $[2];
48
+ }
49
+ const x = t2;
50
+ let t3;
51
+ if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
52
+ t3 = new MyClass();
53
+ $[3] = t3;
54
+ } else {
55
+ t3 = $[3];
56
+ }
57
+ const y = t3;
58
+ let t4;
59
+ if ($[4] !== x) {
60
+ t4 = [x, y];
61
+ $[4] = x;
62
+ $[5] = t4;
63
+ } else {
64
+ t4 = $[5];
65
+ }
66
+ return t4;
67
+}
68
+
69
+export const FIXTURE_ENTRYPOINT = {
70
+ fn: Component,
71
+ params: [{ val: 0 }],
72
+};
73
+
74
+```
75
+
76
+### Eval output
77
+(kind: ok) [[0],{}]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/new-does-not-mutate-class.ts
new
+15
@@ -0,0 +1,15 @@
1
+import { identity } from "shared-runtime";
2
+
3
+class Foo {}
4
+function Component({ val }) {
5
+ const MyClass = identity(Foo);
6
+ const x = [val];
7
+ const y = new MyClass();
8
+
9
+ return [x, y];
10
+}
11
+
12
+export const FIXTURE_ENTRYPOINT = {
13
+ fn: Component,
14
+ params: [{ val: 0 }],
15
+};