[bugfix] do not hoist computed memberpaths in lambdas
Mofei Zhang committed
Oct 30, 2023 at 12:56 UTC
e2836eec5be07acf5a0cf7af3a230f2526d26031
7 files changed
+154
-51
compiler/packages/babel-plugin-react-forget/src/HIR/BuildHIR.ts
+32
-19
@@ -3530,6 +3530,14 @@ function lowerAssignment(
3530
}
3531
}
3532
3533
+function isValidDependency(path: NodePath<t.MemberExpression>): boolean {
3534
+ const parent: NodePath<t.Node> = path.parentPath;
3535
+ return (
3536
+ !path.node.computed &&
3537
+ !(parent.isCallExpression() && parent.get("callee") === path)
3538
+ );
3539
+}
3540
+
3541
function captureScopes({ from, to }: { from: Scope; to: Scope }): Set<Scope> {
3542
let scopes: Set<Scope> = new Set();
3543
while (from) {
@@ -3579,7 +3587,7 @@ function gatherCapturedDeps(
3587
| NodePath<t.JSXOpeningElement>
3588
): void {
3589
// Base context variable to depend on
3582
- let baseIdentifier: NodePath<t.Identifier | t.JSXIdentifier>;
3590
+ let baseIdentifier: NodePath<t.Identifier> | NodePath<t.JSXIdentifier>;
3591
// Base expression to depend on, which (for now) may contain non side-effectful
3592
// member expressions
3593
let dependency:
@@ -3604,31 +3612,36 @@ function gatherCapturedDeps(
3612
dependency = current;
3613
} else if (path.isMemberExpression()) {
3614
// Calculate baseIdentifier
3607
- let current: NodePath<Expression> = path;
3608
- while (current.isMemberExpression()) {
3609
- current = current.get("object");
3615
+ let currentId: NodePath<Expression> = path;
3616
+ while (currentId.isMemberExpression()) {
3617
+ currentId = currentId.get("object");
3618
}
3611
- if (!current.isIdentifier()) {
3619
+ if (!currentId.isIdentifier()) {
3620
return;
3621
}
3614
- baseIdentifier = current;
3622
+ baseIdentifier = currentId;
3623
3624
// Get the expression to depend on, which may involve PropertyLoads
3625
// for member expressions
3618
- current =
3619
- path.parent.type === "CallExpression" &&
3620
- path.parent.callee === path.node
3621
- ? path.get("object")
3622
- : path;
3623
- while (current.isMemberExpression() && current.node.computed) {
3624
- // computed nodes may contain side-effectful subexpressions
3625
- current = current.get("object");
3626
+ let currentDep:
3627
+ | NodePath<t.MemberExpression>
3628
+ | NodePath<t.Identifier>
3629
+ | NodePath<t.JSXIdentifier> = baseIdentifier;
3630
+
3631
+ while (true) {
3632
+ const nextDep: null | NodePath<t.Node> = currentDep.parentPath;
3633
+ if (
3634
+ nextDep &&
3635
+ nextDep.isMemberExpression() &&
3636
+ isValidDependency(nextDep)
3637
+ ) {
3638
+ currentDep = nextDep;
3639
+ } else {
3640
+ break;
3641
+ }
3642
}
3627
- invariant(
3628
- current.isMemberExpression() || current.isIdentifier(),
3629
- "Internal invariant broken in BuildHIR, unexpected type for capturedDep"
3630
- );
3631
- dependency = current;
3643
+
3644
+ dependency = currentDep;
3645
} else {
3646
baseIdentifier = path;
3647
dependency = path;
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.bug-lambda-array-access-member-expr.expect.md
deleted
-30
@@ -1,30 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-import { invoke } from "shared-runtime";
6
-
7
-function Foo() {
8
- const x = [{ value: 0 }, { value: 1 }, { value: 2 }];
9
- const foo = (param: number) => {
10
- return x[param].value;
11
- };
12
-
13
- return invoke(foo, 1);
14
-}
15
-
16
-export const FIXTURE_ENTRYPONT = {
17
- fn: Foo,
18
- params: [{}],
19
-};
20
-
21
-```
22
-
23
-
24
-## Error
25
-
26
-```
27
-[ReactForget] Todo: EnterSSA: Expected identifier to be defined before being used. Identifier param$10 is undefined (5:5)
28
-```
29
-
30
-
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/lambda-array-access-member-expr-captured.expect.md
new
+52
@@ -0,0 +1,52 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { CONST_NUMBER0, invoke } from "shared-runtime";
6
+
7
+function Foo() {
8
+ const x = [{ value: 0 }, { value: 1 }, { value: 2 }];
9
+ const param = CONST_NUMBER0;
10
+ const foo = () => {
11
+ return x[param].value;
12
+ };
13
+
14
+ return invoke(foo);
15
+}
16
+
17
+export const FIXTURE_ENTRYPOINT = {
18
+ fn: Foo,
19
+ params: [{}],
20
+};
21
+
22
+```
23
+
24
+## Code
25
+
26
+```javascript
27
+import { unstable_useMemoCache as useMemoCache } from "react";
28
+import { CONST_NUMBER0, invoke } from "shared-runtime";
29
+
30
+function Foo() {
31
+ const $ = useMemoCache(1);
32
+ let t0;
33
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
34
+ const x = [{ value: 0 }, { value: 1 }, { value: 2 }];
35
+
36
+ const foo = () => x[CONST_NUMBER0].value;
37
+
38
+ t0 = invoke(foo);
39
+ $[0] = t0;
40
+ } else {
41
+ t0 = $[0];
42
+ }
43
+ return t0;
44
+}
45
+
46
+export const FIXTURE_ENTRYPOINT = {
47
+ fn: Foo,
48
+ params: [{}],
49
+};
50
+
51
+```
52
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/lambda-array-access-member-expr-captured.ts
new
+16
@@ -0,0 +1,16 @@
1
+import { CONST_NUMBER0, invoke } from "shared-runtime";
2
+
3
+function Foo() {
4
+ const x = [{ value: 0 }, { value: 1 }, { value: 2 }];
5
+ const param = CONST_NUMBER0;
6
+ const foo = () => {
7
+ return x[param].value;
8
+ };
9
+
10
+ return invoke(foo);
11
+}
12
+
13
+export const FIXTURE_ENTRYPOINT = {
14
+ fn: Foo,
15
+ params: [{}],
16
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/lambda-array-access-member-expr-param.expect.md
new
+50
@@ -0,0 +1,50 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { invoke } from "shared-runtime";
6
+
7
+function Foo() {
8
+ const x = [{ value: 0 }, { value: 1 }, { value: 2 }];
9
+ const foo = (param: number) => {
10
+ return x[param].value;
11
+ };
12
+
13
+ return invoke(foo, 1);
14
+}
15
+
16
+export const FIXTURE_ENTRYPOINT = {
17
+ fn: Foo,
18
+ params: [{}],
19
+};
20
+
21
+```
22
+
23
+## Code
24
+
25
+```javascript
26
+import { unstable_useMemoCache as useMemoCache } from "react";
27
+import { invoke } from "shared-runtime";
28
+
29
+function Foo() {
30
+ const $ = useMemoCache(1);
31
+ let t0;
32
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
33
+ const x = [{ value: 0 }, { value: 1 }, { value: 2 }];
34
+ const foo = (param) => x[param].value;
35
+
36
+ t0 = invoke(foo, 1);
37
+ $[0] = t0;
38
+ } else {
39
+ t0 = $[0];
40
+ }
41
+ return t0;
42
+}
43
+
44
+export const FIXTURE_ENTRYPOINT = {
45
+ fn: Foo,
46
+ params: [{}],
47
+};
48
+
49
+```
50
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/lambda-array-access-member-expr-param.ts
renamed
+1
-1
@@ -9,7 +9,7 @@ function Foo() {
9
return invoke(foo, 1);
10
}
11
12
-export const FIXTURE_ENTRYPONT = {
12
+export const FIXTURE_ENTRYPOINT = {
13
fn: Foo,
14
params: [{}],
15
};
compiler/packages/sprout/src/runner-evaluator.ts
+3
-1
@@ -99,7 +99,9 @@ export function doEval(source: string): EvaluatorResult {
99
) {
100
return {
101
kind: "UnexpectedError",
102
- value: 'FIXTURE_ENTRYPOINT not exported!',
102
+ value: 'FIXTURE_ENTRYPOINT not exported! Found {'
103
+ + Object.keys(exports).filter(e => e !== 'FIXTURE_ENTRYPOINT').toString()
104
+ + '}',
105
};
106
}
107
const validationError = validateEntrypoint(exports.FIXTURE_ENTRYPOINT);