From 9ff82c3c100d27629940a27c88a4340ff008c69f Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Fri, 24 Mar 2023 14:22:16 -0700 Subject: [PATCH] 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. --- compiler/forget/src/HIR/BuildHIR.ts | 79 +++++++++++++------ .../compiler/optional-method-call.expect.md | 35 ++++++++ .../fixtures/compiler/optional-method-call.js | 6 ++ .../optional-receiver-method-call.expect.md | 35 ++++++++ .../compiler/optional-receiver-method-call.js | 6 ++ ...ptional-receiver-optional-method.expect.md | 35 ++++++++ .../optional-receiver-optional-method.js | 6 ++ 7 files changed, 177 insertions(+), 25 deletions(-) create mode 100644 compiler/forget/src/__tests__/fixtures/compiler/optional-method-call.expect.md create mode 100644 compiler/forget/src/__tests__/fixtures/compiler/optional-method-call.js create mode 100644 compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-method-call.expect.md create mode 100644 compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-method-call.js create mode 100644 compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-optional-method.expect.md create mode 100644 compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-optional-method.js diff --git a/compiler/forget/src/HIR/BuildHIR.ts b/compiler/forget/src/HIR/BuildHIR.ts index 6c5b77a0b5..691ee4d6ca 100644 --- a/compiler/forget/src/HIR/BuildHIR.ts +++ b/compiler/forget/src/HIR/BuildHIR.ts @@ -943,19 +943,6 @@ function lowerExpression( const expr = exprPath as NodePath; const optional = expr.node.optional; const calleePath = expr.get("callee"); - if ( - calleePath.isMemberExpression() || - calleePath.isOptionalMemberExpression() - ) { - // collect any errors from the arguments - lowerArguments(builder, expr.get("arguments")); - builder.errors.push({ - reason: `(BuildHIR::lowerExpression) Support OptionalCallExpression with ${calleePath.type} callee`, - severity: ErrorSeverity.Todo, - nodePath: calleePath, - }); - return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc }; - } const loc = expr.node.loc ?? GeneratedSource; const place = buildTemporaryPlace(builder, loc); const continuationBlock = builder.reserve(builder.currentBlockKind()); @@ -963,24 +950,64 @@ function lowerExpression( // Lower the callee in the current block: the callee is always unconditionally evaluated // The test block's branch will test on this value to determine whether to evaluate the call (consequent) // or evaluate to undefined (alternate) - const callee = lowerExpressionToTemporary(builder, calleePath); + let callee: + | { kind: "CallExpression"; callee: Place } + | { kind: "MethodCall"; receiver: Place; property: Place }; + if ( + calleePath.isMemberExpression() || + calleePath.isOptionalMemberExpression() + ) { + const memberExpr = lowerMemberExpression(builder, calleePath); + const propertyPlace = buildTemporaryPlace(builder, GeneratedSource); + builder.push({ + id: makeInstructionId(0), + lvalue: { ...propertyPlace }, + value: memberExpr.value, + loc: GeneratedSource, + }); + callee = { + kind: "MethodCall", + receiver: memberExpr.object, + property: propertyPlace, + }; + } else { + callee = { + kind: "CallExpression", + callee: lowerExpressionToTemporary(builder, calleePath), + }; + } // block to evaluate if the callee is non-null/undefined. arguments are lowered in this block to preserve // the semantic of conditional evaluation depending on the callee const consequent = builder.enter("value", () => { const args = lowerArguments(builder, expr.get("arguments")); const temp = buildTemporaryPlace(builder, loc); - builder.push({ - id: makeInstructionId(0), - lvalue: { ...temp }, - value: { - kind: "CallExpression", - callee: { ...callee }, - args, + if (callee.kind === "CallExpression") { + builder.push({ + id: makeInstructionId(0), + lvalue: { ...temp }, + value: { + kind: "CallExpression", + callee: { ...callee.callee }, + args, + loc, + }, loc, - }, - loc, - }); + }); + } else { + builder.push({ + id: makeInstructionId(0), + lvalue: { ...temp }, + value: { + kind: "MethodCall", + receiver: { ...callee.receiver }, + property: { ...callee.property }, + args, + loc: exprLoc, + }, + loc, + }); + } builder.push({ id: makeInstructionId(0), lvalue: buildTemporaryPlace(builder, loc), @@ -1033,9 +1060,11 @@ function lowerExpression( }); const testBlock = builder.enter("value", () => { + const testPlace = + callee.kind === "CallExpression" ? callee.callee : callee.property; return { kind: "branch", - test: { ...callee }, + test: { ...testPlace }, consequent, alternate, id: makeInstructionId(0), diff --git a/compiler/forget/src/__tests__/fixtures/compiler/optional-method-call.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/optional-method-call.expect.md new file mode 100644 index 0000000000..7b0c0ec820 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/optional-method-call.expect.md @@ -0,0 +1,35 @@ + +## Input + +```javascript +function Component(props) { + const x = makeObject(props); + const y = makeObject(props); + const z = x.optionalMethod?.(y.a, props.a, foo(y.b), bar(props.b)); + return z; +} + +``` + +## Code + +```javascript +function Component(props) { + const $ = React.unstable_useMemoCache(2); + const c_0 = $[0] !== props; + let t0; + if (c_0) { + const x = makeObject(props); + const y = makeObject(props); + t0 = x.optionalMethod?.(y.a, props.a, foo(y.b), bar(props.b)); + $[0] = props; + $[1] = t0; + } else { + t0 = $[1]; + } + const z = t0; + return z; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/optional-method-call.js b/compiler/forget/src/__tests__/fixtures/compiler/optional-method-call.js new file mode 100644 index 0000000000..25145d61ed --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/optional-method-call.js @@ -0,0 +1,6 @@ +function Component(props) { + const x = makeObject(props); + const y = makeObject(props); + const z = x.optionalMethod?.(y.a, props.a, foo(y.b), bar(props.b)); + return z; +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-method-call.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-method-call.expect.md new file mode 100644 index 0000000000..942abb774e --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-method-call.expect.md @@ -0,0 +1,35 @@ + +## Input + +```javascript +function Component(props) { + const x = makeOptionalObject(props); + const y = makeObject(props); + const z = x?.method(y.a, props.a, foo(y.b), bar(props.b)); + return z; +} + +``` + +## Code + +```javascript +function Component(props) { + const $ = React.unstable_useMemoCache(2); + const c_0 = $[0] !== props; + let t0; + if (c_0) { + const x = makeOptionalObject(props); + const y = makeObject(props); + t0 = x?.method(y.a, props.a, foo(y.b), bar(props.b)); + $[0] = props; + $[1] = t0; + } else { + t0 = $[1]; + } + const z = t0; + return z; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-method-call.js b/compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-method-call.js new file mode 100644 index 0000000000..698437dcd8 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-method-call.js @@ -0,0 +1,6 @@ +function Component(props) { + const x = makeOptionalObject(props); + const y = makeObject(props); + const z = x?.method(y.a, props.a, foo(y.b), bar(props.b)); + return z; +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-optional-method.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-optional-method.expect.md new file mode 100644 index 0000000000..bca8669825 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-optional-method.expect.md @@ -0,0 +1,35 @@ + +## Input + +```javascript +function Component(props) { + const x = makeOptionalObject(props); + const y = makeObject(props); + const z = x?.optionalMethod?.(y.a, props.a, foo(y.b), bar(props.b)); + return z; +} + +``` + +## Code + +```javascript +function Component(props) { + const $ = React.unstable_useMemoCache(2); + const c_0 = $[0] !== props; + let t0; + if (c_0) { + const x = makeOptionalObject(props); + const y = makeObject(props); + t0 = x?.optionalMethod?.(y.a, props.a, foo(y.b), bar(props.b)); + $[0] = props; + $[1] = t0; + } else { + t0 = $[1]; + } + const z = t0; + return z; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-optional-method.js b/compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-optional-method.js new file mode 100644 index 0000000000..0d62a3c78d --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/optional-receiver-optional-method.js @@ -0,0 +1,6 @@ +function Component(props) { + const x = makeOptionalObject(props); + const y = makeObject(props); + const z = x?.optionalMethod?.(y.a, props.a, foo(y.b), bar(props.b)); + return z; +}