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 

```
This commit is contained in:
Joe Savona
2023-03-24 14:22:13 -07:00
parent 40bd9b060c
commit feb8e924e4
3 changed files with 112 additions and 3 deletions
+107 -3
View File
@@ -11,7 +11,6 @@ import { Expression } from "@babel/types";
import invariant from "invariant";
import { CompilerError, ErrorSeverity } from "../CompilerError";
import { Err, Ok, Result } from "../Utils/Result";
import todo from "../Utils/todo";
import { assertExhaustive } from "../Utils/utils";
import { Environment, EnvironmentOptions } from "./Environment";
import {
@@ -942,9 +941,114 @@ function lowerExpression(
}
case "OptionalCallExpression": {
const expr = exprPath as NodePath<t.OptionalCallExpression>;
const _optional = expr.get("optional");
const optional = expr.get("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());
return todo("OptionalCallExpression");
const callee = lowerExpressionToTemporary(builder, calleePath);
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,
loc,
},
loc,
});
builder.push({
id: makeInstructionId(0),
lvalue: buildTemporaryPlace(builder, loc),
value: {
kind: "StoreLocal",
lvalue: { kind: InstructionKind.Const, place: { ...place } },
value: { ...temp },
loc,
},
loc,
});
return {
kind: "goto",
variant: GotoVariant.Break,
block: continuationBlock.id,
id: makeInstructionId(0),
};
});
const alternate = builder.enter("value", () => {
const temp = buildTemporaryPlace(builder, loc);
builder.push({
id: makeInstructionId(0),
lvalue: { ...temp },
value: {
kind: "Primitive",
value: undefined,
loc,
},
loc,
});
builder.push({
id: makeInstructionId(0),
lvalue: buildTemporaryPlace(builder, loc),
value: {
kind: "StoreLocal",
lvalue: { kind: InstructionKind.Const, place: { ...place } },
value: { ...temp },
loc,
},
loc,
});
return {
kind: "goto",
variant: GotoVariant.Break,
block: continuationBlock.id,
id: makeInstructionId(0),
};
});
const testBlock = builder.enter("value", () => {
return {
kind: "branch",
test: { ...callee },
consequent,
alternate,
id: makeInstructionId(0),
loc,
};
});
builder.terminateWithContinuation(
{
kind: "optional-call",
optional,
test: testBlock,
fallthrough: continuationBlock.id,
id: makeInstructionId(0),
loc,
},
continuationBlock
);
return { kind: "LoadLocal", place, loc: place.loc };
}
case "CallExpression": {
const expr = exprPath as NodePath<t.CallExpression>;
+4
View File
@@ -355,6 +355,10 @@ export type TernaryTerminal = {
export type OptionalCallTerminal = {
kind: "optional-call";
// Whether the call itself is optional. If false, this means that the callee itself
// is optionally evaluated (has some OptionalMemberExpresion) but the final call is
// required (if the callee is not null/undefined).
optional: boolean;
test: BlockId;
fallthrough: BlockId;
id: InstructionId;
+1
View File
@@ -567,6 +567,7 @@ export function mapTerminalSuccessors(
const fallthrough = fn(terminal.fallthrough);
return {
kind: "optional-call",
optional: terminal.optional,
test,
fallthrough,
id: makeInstructionId(0),