Support for (Optional)MemberExpression callee in OptionalCall
Earlier PRs bailed out when the callee of an OptionalCallExpression was a MemberExpression or OptionalMemberExpression (ie for optional method calls). This PRs expands support for optional method calls, including when the receiver, method, or both are optional. Even better, we don't need to add any additional terminals or instruction variants for this case - the one new OptionalCall terminal from earlier in the stack works for all these cases.
Joe Savona committed
Mar 24, 2023 at 14:22 UTC
9ff82c3c100d27629940a27c88a4340ff008c69f
7 files changed
+177
-25
compiler/forget/src/HIR/BuildHIR.ts
+54
-25
@@ -943,19 +943,6 @@ function lowerExpression(
943
const expr = exprPath as NodePath<t.OptionalCallExpression>;
944
const optional = expr.node.optional;
945
const calleePath = expr.get("callee");
946
- if (
947
- calleePath.isMemberExpression() ||
948
- calleePath.isOptionalMemberExpression()
949
- ) {
950
- // collect any errors from the arguments
951
- lowerArguments(builder, expr.get("arguments"));
952
- builder.errors.push({
953
- reason: `(BuildHIR::lowerExpression) Support OptionalCallExpression with ${calleePath.type} callee`,
954
- severity: ErrorSeverity.Todo,
955
- nodePath: calleePath,
956
- });
957
- return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
958
- }
946
const loc = expr.node.loc ?? GeneratedSource;
947
const place = buildTemporaryPlace(builder, loc);
948
const continuationBlock = builder.reserve(builder.currentBlockKind());
@@ -963,24 +950,64 @@ function lowerExpression(
950
// Lower the callee in the current block: the callee is always unconditionally evaluated
951
// The test block's branch will test on this value to determine whether to evaluate the call (consequent)
952
// or evaluate to undefined (alternate)
966
- const callee = lowerExpressionToTemporary(builder, calleePath);
953
+ let callee:
954
+ | { kind: "CallExpression"; callee: Place }
955
+ | { kind: "MethodCall"; receiver: Place; property: Place };
956
+ if (
957
+ calleePath.isMemberExpression() ||
958
+ calleePath.isOptionalMemberExpression()
959
+ ) {
960
+ const memberExpr = lowerMemberExpression(builder, calleePath);
961
+ const propertyPlace = buildTemporaryPlace(builder, GeneratedSource);
962
+ builder.push({
963
+ id: makeInstructionId(0),
964
+ lvalue: { ...propertyPlace },
965
+ value: memberExpr.value,
966
+ loc: GeneratedSource,
967
+ });
968
+ callee = {
969
+ kind: "MethodCall",
970
+ receiver: memberExpr.object,
971
+ property: propertyPlace,
972
+ };
973
+ } else {
974
+ callee = {
975
+ kind: "CallExpression",
976
+ callee: lowerExpressionToTemporary(builder, calleePath),
977
+ };
978
+ }
979
980
// block to evaluate if the callee is non-null/undefined. arguments are lowered in this block to preserve
981
// the semantic of conditional evaluation depending on the callee
982
const consequent = builder.enter("value", () => {
983
const args = lowerArguments(builder, expr.get("arguments"));
984
const temp = buildTemporaryPlace(builder, loc);
973
- builder.push({
974
- id: makeInstructionId(0),
975
- lvalue: { ...temp },
976
- value: {
977
- kind: "CallExpression",
978
- callee: { ...callee },
979
- args,
985
+ if (callee.kind === "CallExpression") {
986
+ builder.push({
987
+ id: makeInstructionId(0),
988
+ lvalue: { ...temp },
989
+ value: {
990
+ kind: "CallExpression",
991
+ callee: { ...callee.callee },
992
+ args,
993
+ loc,
994
+ },
995
loc,
981
- },
982
- loc,
983
- });
996
+ });
997
+ } else {
998
+ builder.push({
999
+ id: makeInstructionId(0),
1000
+ lvalue: { ...temp },
1001
+ value: {
1002
+ kind: "MethodCall",
1003
+ receiver: { ...callee.receiver },
1004
+ property: { ...callee.property },
1005
+ args,
1006
+ loc: exprLoc,
1007
+ },
1008
+ loc,
1009
+ });
1010
+ }
1011
builder.push({
1012
id: makeInstructionId(0),
1013
lvalue: buildTemporaryPlace(builder, loc),
@@ -1033,9 +1060,11 @@ function lowerExpression(
1060
});
1061
1062
const testBlock = builder.enter("value", () => {
1063
+ const testPlace =
1064
+ callee.kind === "CallExpression" ? callee.callee : callee.property;
1065
return {
1066
kind: "branch",
1038
- test: { ...callee },
1067
+ test: { ...testPlace },
1068
consequent,
1069
alternate,
1070
id: makeInstructionId(0),
compiler/forget/src/__tests__/fixtures/compiler/optional-method-call.expect.md
new
+35
@@ -0,0 +1,35 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component(props) {
6
+ const x = makeObject(props);
7
+ const y = makeObject(props);
8
+ const z = x.optionalMethod?.(y.a, props.a, foo(y.b), bar(props.b));
9
+ return z;
10
+}
11
+
12
+```
13
+
14
+## Code
15
+
16
+```javascript
17
+function Component(props) {
18
+ const $ = React.unstable_useMemoCache(2);
19
+ const c_0 = $[0] !== props;
20
+ let t0;
21
+ if (c_0) {
22
+ const x = makeObject(props);
23
+ const y = makeObject(props);
24
+ t0 = x.optionalMethod?.(y.a, props.a, foo(y.b), bar(props.b));
25
+ $[0] = props;
26
+ $[1] = t0;
27
+ } else {
28
+ t0 = $[1];
29
+ }
30
+ const z = t0;
31
+ return z;
32
+}
33
+
34
+```
35
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/optional-method-call.js
new
+6
@@ -0,0 +1,6 @@
1
+function Component(props) {
2
+ const x = makeObject(props);
3
+ const y = makeObject(props);
4
+ const z = x.optionalMethod?.(y.a, props.a, foo(y.b), bar(props.b));
5
+ return z;
6
+}
compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-method-call.expect.md
new
+35
@@ -0,0 +1,35 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component(props) {
6
+ const x = makeOptionalObject(props);
7
+ const y = makeObject(props);
8
+ const z = x?.method(y.a, props.a, foo(y.b), bar(props.b));
9
+ return z;
10
+}
11
+
12
+```
13
+
14
+## Code
15
+
16
+```javascript
17
+function Component(props) {
18
+ const $ = React.unstable_useMemoCache(2);
19
+ const c_0 = $[0] !== props;
20
+ let t0;
21
+ if (c_0) {
22
+ const x = makeOptionalObject(props);
23
+ const y = makeObject(props);
24
+ t0 = x?.method(y.a, props.a, foo(y.b), bar(props.b));
25
+ $[0] = props;
26
+ $[1] = t0;
27
+ } else {
28
+ t0 = $[1];
29
+ }
30
+ const z = t0;
31
+ return z;
32
+}
33
+
34
+```
35
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-method-call.js
new
+6
@@ -0,0 +1,6 @@
1
+function Component(props) {
2
+ const x = makeOptionalObject(props);
3
+ const y = makeObject(props);
4
+ const z = x?.method(y.a, props.a, foo(y.b), bar(props.b));
5
+ return z;
6
+}
compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-optional-method.expect.md
new
+35
@@ -0,0 +1,35 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component(props) {
6
+ const x = makeOptionalObject(props);
7
+ const y = makeObject(props);
8
+ const z = x?.optionalMethod?.(y.a, props.a, foo(y.b), bar(props.b));
9
+ return z;
10
+}
11
+
12
+```
13
+
14
+## Code
15
+
16
+```javascript
17
+function Component(props) {
18
+ const $ = React.unstable_useMemoCache(2);
19
+ const c_0 = $[0] !== props;
20
+ let t0;
21
+ if (c_0) {
22
+ const x = makeOptionalObject(props);
23
+ const y = makeObject(props);
24
+ t0 = x?.optionalMethod?.(y.a, props.a, foo(y.b), bar(props.b));
25
+ $[0] = props;
26
+ $[1] = t0;
27
+ } else {
28
+ t0 = $[1];
29
+ }
30
+ const z = t0;
31
+ return z;
32
+}
33
+
34
+```
35
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-optional-method.js
new
+6
@@ -0,0 +1,6 @@
1
+function Component(props) {
2
+ const x = makeOptionalObject(props);
3
+ const y = makeObject(props);
4
+ const z = x?.optionalMethod?.(y.a, props.a, foo(y.b), bar(props.b));
5
+ return z;
6
+}