@samitouri / QOS-React / commits / 2fc910d580

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