From feb8e924e49a9b3a6cfaab12c75f6eb3835e0e9d Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Fri, 24 Mar 2023 14:22:13 -0700 Subject: [PATCH] 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 `?.()` we lower as roughly: ``` bb0: t0 = OptionalCall test=bb1 fallthrough= bb1 (value): Branch t0 consequent=bb2 alternate=bb3 bb2 (value): ...lower here... t1 = Call t0, args StoreLocal res, t1 Goto bb4 bb3 (value): t2 = undefined StoreLocal res, t2 Goto bb4 bb4: // result in `res` here ``` --- compiler/forget/src/HIR/BuildHIR.ts | 110 +++++++++++++++++++++++++++- compiler/forget/src/HIR/HIR.ts | 4 + compiler/forget/src/HIR/visitors.ts | 1 + 3 files changed, 112 insertions(+), 3 deletions(-) diff --git a/compiler/forget/src/HIR/BuildHIR.ts b/compiler/forget/src/HIR/BuildHIR.ts index 19379f7fc8..21cf7b0be0 100644 --- a/compiler/forget/src/HIR/BuildHIR.ts +++ b/compiler/forget/src/HIR/BuildHIR.ts @@ -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; - 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; diff --git a/compiler/forget/src/HIR/HIR.ts b/compiler/forget/src/HIR/HIR.ts index aadc215d9e..93912893aa 100644 --- a/compiler/forget/src/HIR/HIR.ts +++ b/compiler/forget/src/HIR/HIR.ts @@ -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; diff --git a/compiler/forget/src/HIR/visitors.ts b/compiler/forget/src/HIR/visitors.ts index 0951313b1d..a34acc5083 100644 --- a/compiler/forget/src/HIR/visitors.ts +++ b/compiler/forget/src/HIR/visitors.ts @@ -567,6 +567,7 @@ export function mapTerminalSuccessors( const fallthrough = fn(terminal.fallthrough); return { kind: "optional-call", + optional: terminal.optional, test, fallthrough, id: makeInstructionId(0),