BuildHIR for OptionalCallExpression
Implements lowering for a subset of optional calls - specifically, we don't (yet) support when the callee is a member expression or an optional member expression. So `foo?.()` works but we bailout on `object?.foo()` and `object.foo?.()`. For `<calleee>?.(<args>)` we lower as roughly: ``` bb0: t0 = <callee> OptionalCall test=bb1 fallthrough= bb1 (value): Branch t0 consequent=bb2 alternate=bb3 bb2 (value): ...lower <args> here... t1 = Call t0, args StoreLocal res, t1 Goto bb4 bb3 (value): t2 = undefined StoreLocal res, t2 Goto bb4 bb4: // result in `res` here ```
Joe Savona committed
Mar 24, 2023 at 14:22 UTC
feb8e924e49a9b3a6cfaab12c75f6eb3835e0e9d
3 files changed
+112
-3
compiler/forget/src/HIR/BuildHIR.ts
+107
-3
@@ -11,7 +11,6 @@ import { Expression } from "@babel/types";
11
import invariant from "invariant";
12
import { CompilerError, ErrorSeverity } from "../CompilerError";
13
import { Err, Ok, Result } from "../Utils/Result";
14
-import todo from "../Utils/todo";
14
import { assertExhaustive } from "../Utils/utils";
15
import { Environment, EnvironmentOptions } from "./Environment";
16
import {
@@ -942,9 +941,114 @@ function lowerExpression(
941
}
942
case "OptionalCallExpression": {
943
const expr = exprPath as NodePath<t.OptionalCallExpression>;
945
- const _optional = expr.get("optional");
944
+ const optional = expr.get("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
+ }
959
+ const loc = expr.node.loc ?? GeneratedSource;
960
+ const place = buildTemporaryPlace(builder, loc);
961
+ const continuationBlock = builder.reserve(builder.currentBlockKind());
962
+
963
+ const callee = lowerExpressionToTemporary(builder, calleePath);
964
947
- return todo("OptionalCallExpression");
965
+ const consequent = builder.enter("value", () => {
966
+ const args = lowerArguments(builder, expr.get("arguments"));
967
+ const temp = buildTemporaryPlace(builder, loc);
968
+ builder.push({
969
+ id: makeInstructionId(0),
970
+ lvalue: { ...temp },
971
+ value: {
972
+ kind: "CallExpression",
973
+ callee: { ...callee },
974
+ args,
975
+ loc,
976
+ },
977
+ loc,
978
+ });
979
+ builder.push({
980
+ id: makeInstructionId(0),
981
+ lvalue: buildTemporaryPlace(builder, loc),
982
+ value: {
983
+ kind: "StoreLocal",
984
+ lvalue: { kind: InstructionKind.Const, place: { ...place } },
985
+ value: { ...temp },
986
+ loc,
987
+ },
988
+ loc,
989
+ });
990
+ return {
991
+ kind: "goto",
992
+ variant: GotoVariant.Break,
993
+ block: continuationBlock.id,
994
+ id: makeInstructionId(0),
995
+ };
996
+ });
997
+ const alternate = builder.enter("value", () => {
998
+ const temp = buildTemporaryPlace(builder, loc);
999
+ builder.push({
1000
+ id: makeInstructionId(0),
1001
+ lvalue: { ...temp },
1002
+ value: {
1003
+ kind: "Primitive",
1004
+ value: undefined,
1005
+ loc,
1006
+ },
1007
+ loc,
1008
+ });
1009
+ builder.push({
1010
+ id: makeInstructionId(0),
1011
+ lvalue: buildTemporaryPlace(builder, loc),
1012
+ value: {
1013
+ kind: "StoreLocal",
1014
+ lvalue: { kind: InstructionKind.Const, place: { ...place } },
1015
+ value: { ...temp },
1016
+ loc,
1017
+ },
1018
+ loc,
1019
+ });
1020
+ return {
1021
+ kind: "goto",
1022
+ variant: GotoVariant.Break,
1023
+ block: continuationBlock.id,
1024
+ id: makeInstructionId(0),
1025
+ };
1026
+ });
1027
+
1028
+ const testBlock = builder.enter("value", () => {
1029
+ return {
1030
+ kind: "branch",
1031
+ test: { ...callee },
1032
+ consequent,
1033
+ alternate,
1034
+ id: makeInstructionId(0),
1035
+ loc,
1036
+ };
1037
+ });
1038
+
1039
+ builder.terminateWithContinuation(
1040
+ {
1041
+ kind: "optional-call",
1042
+ optional,
1043
+ test: testBlock,
1044
+ fallthrough: continuationBlock.id,
1045
+ id: makeInstructionId(0),
1046
+ loc,
1047
+ },
1048
+ continuationBlock
1049
+ );
1050
+
1051
+ return { kind: "LoadLocal", place, loc: place.loc };
1052
}
1053
case "CallExpression": {
1054
const expr = exprPath as NodePath<t.CallExpression>;
compiler/forget/src/HIR/HIR.ts
+4
@@ -355,6 +355,10 @@ export type TernaryTerminal = {
355
356
export type OptionalCallTerminal = {
357
kind: "optional-call";
358
+ // Whether the call itself is optional. If false, this means that the callee itself
359
+ // is optionally evaluated (has some OptionalMemberExpresion) but the final call is
360
+ // required (if the callee is not null/undefined).
361
+ optional: boolean;
362
test: BlockId;
363
fallthrough: BlockId;
364
id: InstructionId;
compiler/forget/src/HIR/visitors.ts
+1
@@ -567,6 +567,7 @@ export function mapTerminalSuccessors(
567
const fallthrough = fn(terminal.fallthrough);
568
return {
569
kind: "optional-call",
570
+ optional: terminal.optional,
571
test,
572
fallthrough,
573
id: makeInstructionId(0),