/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import { NodePath, Scope } from "@babel/traverse";
import * as t from "@babel/types";
import { Expression } from "@babel/types";
import invariant from "invariant";
import {
  CompilerError,
  CompilerSuggestionOperation,
  ErrorSeverity,
} from "../CompilerError";
import { Err, Ok, Result } from "../Utils/Result";
import { assertExhaustive, hasNode } from "../Utils/utils";
import { Environment } from "./Environment";
import {
  ArrayExpression,
  ArrayPattern,
  BlockId,
  BranchTerminal,
  BuiltinTag,
  Case,
  Effect,
  GeneratedSource,
  GotoVariant,
  HIRFunction,
  IfTerminal,
  InstructionKind,
  InstructionValue,
  JsxAttribute,
  LoweredFunction,
  ObjectPattern,
  ObjectProperty,
  ObjectPropertyKey,
  Place,
  ReturnTerminal,
  SourceLocation,
  SpreadPattern,
  ThrowTerminal,
  Type,
  makeInstructionId,
  makeType,
  promoteTemporary,
} from "./HIR";
import HIRBuilder, { Bindings } from "./HIRBuilder";
import { BuiltInArrayId } from "./ObjectShape";

/*
 * *******************************************************************************************
 * *******************************************************************************************
 * ************************************* Lowering to HIR *************************************
 * *******************************************************************************************
 * *******************************************************************************************
 */

/*
 * Converts a function into a high-level intermediate form (HIR) which represents
 * the code as a control-flow graph. All normal control-flow is modeled as accurately
 * as possible to allow precise, expression-level memoization. The main exceptions are
 * try/catch statements and exceptions: we currently bail out (skip compilation) for
 * try/catch and do not attempt to model control flow of exceptions, which can occur
 * ~anywhere in JavaScript. The compiler assumes that exceptions will be handled by
 * the runtime, ie by invalidating memoization.
 */
export function lower(
  func: NodePath<t.Function>,
  env: Environment,
  bindings: Bindings | null = null,
  capturedRefs: Array<t.Identifier> = [],
  // the outermost function being compiled, in case lower() is called recursively (for lambdas)
  parent: NodePath<t.Function> | null = null
): Result<HIRFunction, CompilerError> {
  const builder = new HIRBuilder(env, parent ?? func, bindings, capturedRefs);
  const context: Array<Place> = [];

  for (const ref of capturedRefs ?? []) {
    context.push({
      kind: "Identifier",
      identifier: builder.resolveBinding(ref),
      effect: Effect.Unknown,
      reactive: false,
      loc: ref.loc ?? GeneratedSource,
    });
  }

  let id: string | null = null;
  if (func.isFunctionDeclaration() || func.isFunctionExpression()) {
    const idNode = (
      func as NodePath<t.FunctionDeclaration | t.FunctionExpression>
    ).get("id");
    if (hasNode(idNode)) {
      id = idNode.node.name;
    }
  }
  const params: Array<Place | SpreadPattern> = [];
  func.get("params").forEach((param) => {
    if (param.isIdentifier()) {
      const binding = builder.resolveIdentifier(param);
      if (binding.kind !== "Identifier") {
        builder.errors.push({
          reason: `(BuildHIR::lower) Could not find binding for param \`${param.node.name}\``,
          severity: ErrorSeverity.Invariant,
          loc: param.node.loc ?? null,
          suggestions: null,
        });
        return;
      }
      const place: Place = {
        kind: "Identifier",
        identifier: binding.identifier,
        effect: Effect.Unknown,
        reactive: false,
        loc: param.node.loc ?? GeneratedSource,
      };
      params.push(place);
    } else if (
      param.isObjectPattern() ||
      param.isArrayPattern() ||
      param.isAssignmentPattern()
    ) {
      const place: Place = {
        kind: "Identifier",
        identifier: builder.makeTemporary(param.node.loc ?? GeneratedSource),
        effect: Effect.Unknown,
        reactive: false,
        loc: param.node.loc ?? GeneratedSource,
      };
      params.push(place);
      lowerAssignment(
        builder,
        param.node.loc ?? GeneratedSource,
        InstructionKind.Let,
        param,
        place,
        "Assignment"
      );
    } else if (param.isRestElement()) {
      const place: Place = {
        kind: "Identifier",
        identifier: builder.makeTemporary(param.node.loc ?? GeneratedSource),
        effect: Effect.Unknown,
        reactive: false,
        loc: param.node.loc ?? GeneratedSource,
      };
      params.push({
        kind: "Spread",
        place,
      });
      lowerAssignment(
        builder,
        param.node.loc ?? GeneratedSource,
        InstructionKind.Let,
        param.get("argument"),
        place,
        "Assignment"
      );
    } else {
      builder.errors.push({
        reason: `(BuildHIR::lower) Handle ${param.node.type} params`,
        severity: ErrorSeverity.Todo,
        loc: param.node.loc ?? null,
        suggestions: null,
      });
    }
  });

  let directives: Array<string> = [];
  const body = func.get("body");
  if (body.isExpression()) {
    const fallthrough = builder.reserve("block");
    const terminal: ReturnTerminal = {
      kind: "return",
      loc: GeneratedSource,
      value: lowerExpressionToTemporary(builder, body),
      id: makeInstructionId(0),
    };
    builder.terminateWithContinuation(terminal, fallthrough);
  } else if (body.isBlockStatement()) {
    lowerStatement(builder, body);
    directives = body.get("directives").map((d) => d.node.value.value);
  } else {
    builder.errors.push({
      severity: ErrorSeverity.InvalidJS,
      reason: `Unexpected function body kind`,
      description: `Expected function body to be an expression or a block statement, got \`${body.type}\``,
      loc: body.node.loc ?? null,
      suggestions: null,
    });
  }

  if (builder.errors.hasErrors()) {
    return Err(builder.errors);
  }

  builder.terminate(
    {
      kind: "return",
      loc: GeneratedSource,
      value: lowerValueToTemporary(builder, {
        kind: "Primitive",
        value: undefined,
        loc: GeneratedSource,
      }),
      id: makeInstructionId(0),
    },
    null
  );

  return Ok({
    id,
    params,
    fnType: parent == null ? env.fnType : "Other",
    returnType: null, // TODO: extract the actual return type node if present
    body: builder.build(),
    context,
    generator: func.node.generator === true,
    async: func.node.async === true,
    loc: func.node.loc ?? GeneratedSource,
    env,
    effects: null,
    directives,
  });
}

// Helper to lower a statement
function lowerStatement(
  builder: HIRBuilder,
  stmtPath: NodePath<t.Statement>,
  label: string | null = null
): void {
  const stmtNode = stmtPath.node;
  switch (stmtNode.type) {
    case "ThrowStatement": {
      const stmt = stmtPath as NodePath<t.ThrowStatement>;
      const value = lowerExpressionToTemporary(builder, stmt.get("argument"));
      const handler = builder.resolveThrowHandler();
      if (handler != null) {
        /*
         * NOTE: we could support this, but a `throw` inside try/catch is using exceptions
         * for control-flow and is generally considered an anti-pattern. we can likely
         * just not support this pattern, unless it really becomes necessary for some reason.
         */
        builder.errors.push({
          reason:
            "(BuildHIR::lowerStatement) Support ThrowStatement inside of try/catch",
          severity: ErrorSeverity.Todo,
          loc: stmt.node.loc ?? null,
          suggestions: null,
        });
      }
      const terminal: ThrowTerminal = {
        kind: "throw",
        value,
        id: makeInstructionId(0),
        loc: stmt.node.loc ?? GeneratedSource,
      };
      builder.terminate(terminal, "block");
      return;
    }
    case "ReturnStatement": {
      const stmt = stmtPath as NodePath<t.ReturnStatement>;
      const argument = stmt.get("argument");
      let value;
      if (argument.node === null) {
        value = lowerValueToTemporary(builder, {
          kind: "Primitive",
          value: undefined,
          loc: GeneratedSource,
        });
      } else {
        value = lowerExpressionToTemporary(
          builder,
          argument as NodePath<t.Expression>
        );
      }
      const terminal: ReturnTerminal = {
        kind: "return",
        loc: stmt.node.loc ?? GeneratedSource,
        value,
        id: makeInstructionId(0),
      };
      builder.terminate(terminal, "block");
      return;
    }
    case "IfStatement": {
      const stmt = stmtPath as NodePath<t.IfStatement>;
      //  Block for code following the if
      const continuationBlock = builder.reserve("block");
      //  Block for the consequent (if the test is truthy)
      const consequentBlock = builder.enter("block", (_blockId) => {
        const consequent = stmt.get("consequent");
        lowerStatement(builder, consequent);
        return {
          kind: "goto",
          block: continuationBlock.id,
          variant: GotoVariant.Break,
          id: makeInstructionId(0),
          loc: consequent.node.loc ?? GeneratedSource,
        };
      });
      //  Block for the alternate (if the test is not truthy)
      let alternateBlock: BlockId;
      const alternate = stmt.get("alternate");
      if (hasNode(alternate)) {
        alternateBlock = builder.enter("block", (_blockId) => {
          lowerStatement(builder, alternate);
          return {
            kind: "goto",
            block: continuationBlock.id,
            variant: GotoVariant.Break,
            id: makeInstructionId(0),
            loc: alternate.node?.loc ?? GeneratedSource,
          };
        });
      } else {
        //  If there is no else clause, use the continuation directly
        alternateBlock = continuationBlock.id;
      }
      const test = lowerExpressionToTemporary(builder, stmt.get("test"));
      const terminal: IfTerminal = {
        kind: "if",
        test,
        consequent: consequentBlock,
        alternate: alternateBlock,
        fallthrough: continuationBlock.id,
        id: makeInstructionId(0),
        loc: stmt.node.loc ?? GeneratedSource,
      };
      builder.terminateWithContinuation(terminal, continuationBlock);
      return;
    }
    case "BlockStatement": {
      const stmt = stmtPath as NodePath<t.BlockStatement>;
      const statements = stmt.get("body");
      /**
       * Hoistable identifier bindings defined for this precise block
       * scope (excluding bindings from parent or child block scopes).
       */
      const hoistableIdentifiers: Set<t.Identifier> = new Set();

      for (const [, binding] of Object.entries(stmt.scope.bindings)) {
        // refs to params are always valid / never need to be hoisted
        if (binding.kind !== "param") {
          hoistableIdentifiers.add(binding.identifier);
        }
      }

      for (const s of statements) {
        const willHoist = new Set<NodePath<t.Identifier>>();
        /*
         * If we see a hoistable identifier before its declaration, it should be hoisted just
         * before the statement that references it.
         */
        let fnDepth = s.isFunctionDeclaration() ? 1 : 0;
        const withFunctionContext = {
          enter: (): void => {
            fnDepth++;
          },
          exit: (): void => {
            fnDepth--;
          },
        };
        s.traverse({
          FunctionExpression: withFunctionContext,
          FunctionDeclaration: withFunctionContext,
          ArrowFunctionExpression: withFunctionContext,
          ObjectMethod: withFunctionContext,
          Identifier(id: NodePath<t.Identifier>) {
            const id2 = id;
            if (
              !id2.isReferencedIdentifier() &&
              // isReferencedIdentifier is broken and returns false for reassignments
              id.parent.type !== "AssignmentExpression"
            ) {
              return;
            }
            const binding = id.scope.getBinding(id.node.name);
            /**
             * We can only hoist an identifier decl if
             * 1. the reference occurs within an inner function
             * or
             * 2. the declaration itself is hoistable
             */
            if (
              binding != null &&
              hoistableIdentifiers.has(binding.identifier) &&
              (fnDepth > 0 || binding.kind === "hoisted")
            ) {
              willHoist.add(id);
            }
          },
        });
        /*
         * After visiting the declaration, hoisting is no longer required
         */
        s.traverse({
          Identifier(path: NodePath<t.Identifier>) {
            if (hoistableIdentifiers.has(path.node)) {
              hoistableIdentifiers.delete(path.node);
            }
          },
        });

        // Hoist declarations that need it to the earliest point where they are needed
        for (const id of willHoist) {
          const binding = stmt.scope.getBinding(id.node.name);
          CompilerError.invariant(binding != null, {
            reason: "Expected to find binding for hoisted identifier",
            description: `Could not find a binding for ${id.node.name}`,
            suggestions: null,
            loc: id.node.loc ?? GeneratedSource,
          });
          if (builder.environment.isHoistedIdentifier(binding.identifier)) {
            // Already hoisted
            continue;
          }
          if (!binding.path.isVariableDeclarator()) {
            builder.errors.push({
              severity: ErrorSeverity.Todo,
              reason: "Unsupported declaration type for hoisting",
              description: `variable "${binding.identifier.name}" declared with ${binding.path.type}`,
              suggestions: null,
              loc: id.parentPath.node.loc ?? GeneratedSource,
            });
            continue;
          } else if (!binding.path.get("id").isIdentifier()) {
            builder.errors.push({
              severity: ErrorSeverity.Todo,
              reason: "Unsupported variable declaration type for hoisting",
              description: `variable "${
                binding.identifier.name
              }" declared with ${binding.path.get("id").type}`,
              suggestions: null,
              loc: id.parentPath.node.loc ?? GeneratedSource,
            });
            continue;
          } else if (binding.kind !== "const" && binding.kind !== "var") {
            // Avoid double errors on var declarations, which we do not plan to support anyways
            builder.errors.push({
              severity: ErrorSeverity.Todo,
              reason: "Handle non-const declarations for hoisting",
              description: `variable "${binding.identifier.name}" declared with ${binding.kind}`,
              suggestions: null,
              loc: id.parentPath.node.loc ?? GeneratedSource,
            });
            continue;
          }
          const identifier = builder.resolveIdentifier(id);
          CompilerError.invariant(identifier.kind === "Identifier", {
            reason:
              "Expected hoisted binding to be a local identifier, not a global",
            loc: id.node.loc ?? GeneratedSource,
          });
          const place: Place = {
            effect: Effect.Unknown,
            identifier: identifier.identifier,
            kind: "Identifier",
            reactive: false,
            loc: id.node.loc ?? GeneratedSource,
          };
          lowerValueToTemporary(builder, {
            kind: "DeclareContext",
            lvalue: {
              kind: InstructionKind.HoistedConst,
              place,
            },
            loc: id.node.loc ?? GeneratedSource,
          });
          builder.environment.addHoistedIdentifier(binding.identifier);
        }
        lowerStatement(builder, s);
      }

      return;
    }
    case "BreakStatement": {
      const stmt = stmtPath as NodePath<t.BreakStatement>;
      const block = builder.lookupBreak(stmt.node.label?.name ?? null);
      builder.terminate(
        {
          kind: "goto",
          block,
          variant: GotoVariant.Break,
          id: makeInstructionId(0),
          loc: stmt.node.loc ?? GeneratedSource,
        },
        "block"
      );
      return;
    }
    case "ContinueStatement": {
      const stmt = stmtPath as NodePath<t.ContinueStatement>;
      const block = builder.lookupContinue(stmt.node.label?.name ?? null);
      builder.terminate(
        {
          kind: "goto",
          block,
          variant: GotoVariant.Continue,
          id: makeInstructionId(0),
          loc: stmt.node.loc ?? GeneratedSource,
        },
        "block"
      );
      return;
    }
    case "ForStatement": {
      const stmt = stmtPath as NodePath<t.ForStatement>;

      const testBlock = builder.reserve("loop");
      //  Block for code following the loop
      const continuationBlock = builder.reserve("block");

      const initBlock = builder.enter("loop", (_blockId) => {
        const init = stmt.get("init");
        if (!init.isVariableDeclaration()) {
          builder.errors.push({
            reason:
              "(BuildHIR::lowerStatement) Handle non-variable initialization in ForStatement",
            severity: ErrorSeverity.Todo,
            loc: stmt.node.loc ?? null,
            suggestions: null,
          });
          return {
            kind: "unsupported",
            id: makeInstructionId(0),
            loc: init.node?.loc ?? GeneratedSource,
          };
        }
        lowerStatement(builder, init);
        return {
          kind: "goto",
          block: testBlock.id,
          variant: GotoVariant.Break,
          id: makeInstructionId(0),
          loc: init.node.loc ?? GeneratedSource,
        };
      });

      let updateBlock: BlockId | null = null;
      const update = stmt.get("update");
      if (hasNode(update)) {
        updateBlock = builder.enter("loop", (_blockId) => {
          lowerExpressionToTemporary(builder, update);
          return {
            kind: "goto",
            block: testBlock.id,
            variant: GotoVariant.Break,
            id: makeInstructionId(0),
            loc: update.node?.loc ?? GeneratedSource,
          };
        });
      }

      const bodyBlock = builder.enter("block", (_blockId) => {
        return builder.loop(
          label,
          updateBlock ?? testBlock.id,
          continuationBlock.id,
          () => {
            const body = stmt.get("body");
            lowerStatement(builder, body);
            return {
              kind: "goto",
              block: updateBlock ?? testBlock.id,
              variant: GotoVariant.Continue,
              id: makeInstructionId(0),
              loc: body.node.loc ?? GeneratedSource,
            };
          }
        );
      });

      builder.terminateWithContinuation(
        {
          kind: "for",
          loc: stmtNode.loc ?? GeneratedSource,
          init: initBlock,
          test: testBlock.id,
          update: updateBlock,
          loop: bodyBlock,
          fallthrough: continuationBlock.id,
          id: makeInstructionId(0),
        },
        testBlock
      );

      const test = stmt.get("test");
      if (test.node == null) {
        builder.errors.push({
          reason: `(BuildHIR::lowerStatement) Handle empty test in ForStatement`,
          severity: ErrorSeverity.Todo,
          loc: stmt.node.loc ?? null,
          suggestions: null,
        });
      } else {
        builder.terminateWithContinuation(
          {
            kind: "branch",
            test: lowerExpressionToTemporary(
              builder,
              test as NodePath<t.Expression>
            ),
            consequent: bodyBlock,
            alternate: continuationBlock.id,
            id: makeInstructionId(0),
            loc: stmt.node.loc ?? GeneratedSource,
          },
          continuationBlock
        );
      }
      return;
    }
    case "WhileStatement": {
      const stmt = stmtPath as NodePath<t.WhileStatement>;
      //  Block used to evaluate whether to (re)enter or exit the loop
      const conditionalBlock = builder.reserve("loop");
      //  Block for code following the loop
      const continuationBlock = builder.reserve("block");
      //  Loop body
      const loopBlock = builder.enter("block", (_blockId) => {
        return builder.loop(
          label,
          conditionalBlock.id,
          continuationBlock.id,
          () => {
            const body = stmt.get("body");
            lowerStatement(builder, body);
            return {
              kind: "goto",
              block: conditionalBlock.id,
              variant: GotoVariant.Continue,
              id: makeInstructionId(0),
              loc: body.node.loc ?? GeneratedSource,
            };
          }
        );
      });
      /*
       * The code leading up to the loop must jump to the conditional block,
       * to evaluate whether to enter the loop or bypass to the continuation.
       */
      const loc = stmt.node.loc ?? GeneratedSource;
      builder.terminateWithContinuation(
        {
          kind: "while",
          loc,
          test: conditionalBlock.id,
          loop: loopBlock,
          fallthrough: continuationBlock.id,
          id: makeInstructionId(0),
        },
        conditionalBlock
      );
      /*
       * The conditional block is empty and exists solely as conditional for
       * (re)entering or exiting the loop
       */
      const test = lowerExpressionToTemporary(builder, stmt.get("test"));
      const terminal: BranchTerminal = {
        kind: "branch",
        test,
        consequent: loopBlock,
        alternate: continuationBlock.id,
        id: makeInstructionId(0),
        loc: stmt.node.loc ?? GeneratedSource,
      };
      //  Complete the conditional and continue with code after the loop
      builder.terminateWithContinuation(terminal, continuationBlock);
      return;
    }
    case "LabeledStatement": {
      const stmt = stmtPath as NodePath<t.LabeledStatement>;
      const label = stmt.node.label.name;
      const body = stmt.get("body");
      switch (body.node.type) {
        case "ForInStatement":
        case "ForOfStatement":
        case "ForStatement":
        case "WhileStatement":
        case "DoWhileStatement": {
          /*
           * labeled loops are special because of continue, so push the label
           * down
           */
          lowerStatement(builder, stmt.get("body"), label);
          break;
        }
        default: {
          /*
           * All other statements create a continuation block to allow `break`,
           * explicitly *don't* pass the label down
           */
          const continuationBlock = builder.reserve("block");
          const block = builder.enter("block", () => {
            const body = stmt.get("body");
            builder.label(label, continuationBlock.id, () => {
              lowerStatement(builder, body);
            });
            return {
              kind: "goto",
              block: continuationBlock.id,
              variant: GotoVariant.Break,
              id: makeInstructionId(0),
              loc: body.node.loc ?? GeneratedSource,
            };
          });
          builder.terminateWithContinuation(
            {
              kind: "label",
              block,
              fallthrough: continuationBlock.id,
              id: makeInstructionId(0),
              loc: stmt.node.loc ?? GeneratedSource,
            },
            continuationBlock
          );
        }
      }
      return;
    }
    case "SwitchStatement": {
      const stmt = stmtPath as NodePath<t.SwitchStatement>;
      //  Block following the switch
      const continuationBlock = builder.reserve("block");
      /*
       * The goto target for any cases that fallthrough, which initially starts
       * as the continuation block and is then updated as we iterate through cases
       * in reverse order.
       */
      let fallthrough = continuationBlock.id;
      /*
       * Iterate through cases in reverse order, so that previous blocks can fallthrough
       * to successors
       */
      const cases: Array<Case> = [];
      let hasDefault = false;
      for (let ii = stmt.get("cases").length - 1; ii >= 0; ii--) {
        const case_: NodePath<t.SwitchCase> = stmt.get("cases")[ii];
        const testExpr = case_.get("test");
        if (testExpr.node == null) {
          if (hasDefault) {
            builder.errors.push({
              reason: `Expected at most one \`default\` branch in a switch statement, this code should have failed to parse`,
              severity: ErrorSeverity.InvalidJS,
              loc: case_.node.loc ?? null,
              suggestions: null,
            });
            break;
          }
          hasDefault = true;
        }
        const block = builder.enter("block", (_blockId) => {
          return builder.switch(label, continuationBlock.id, () => {
            case_
              .get("consequent")
              .forEach((consequent) => lowerStatement(builder, consequent));
            /*
             * always generate a fallthrough to the next block, this may be dead code
             * if there was an explicit break, but if so it will be pruned later.
             */
            return {
              kind: "goto",
              block: fallthrough,
              variant: GotoVariant.Break,
              id: makeInstructionId(0),
              loc: case_.node.loc ?? GeneratedSource,
            };
          });
        });
        let test: Place | null = null;
        if (hasNode(testExpr)) {
          test = lowerReorderableExpression(builder, testExpr);
        }
        cases.push({
          test,
          block,
        });
        fallthrough = block;
      }
      /*
       * it doesn't matter for our analysis purposes, but reverse the order of the cases
       * back to the original to make it match the original code/intent.
       */
      cases.reverse();
      /*
       * If there wasn't an explicit default case, generate one to model the fact that execution
       * could bypass any of the other cases and jump directly to the continuation.
       */
      if (!hasDefault) {
        cases.push({ test: null, block: continuationBlock.id });
      }

      const test = lowerExpressionToTemporary(
        builder,
        stmt.get("discriminant")
      );
      builder.terminateWithContinuation(
        {
          kind: "switch",
          test,
          cases,
          fallthrough: continuationBlock.id,
          id: makeInstructionId(0),
          loc: stmt.node.loc ?? GeneratedSource,
        },
        continuationBlock
      );
      return;
    }
    case "VariableDeclaration": {
      const stmt = stmtPath as NodePath<t.VariableDeclaration>;
      const nodeKind: t.VariableDeclaration["kind"] = stmt.node.kind;
      if (nodeKind === "var") {
        builder.errors.push({
          reason: `(BuildHIR::lowerStatement) Handle ${nodeKind} kinds in VariableDeclaration`,
          severity: ErrorSeverity.Todo,
          loc: stmt.node.loc ?? null,
          suggestions: null,
        });
        return;
      }
      const kind =
        nodeKind === "let" ? InstructionKind.Let : InstructionKind.Const;
      for (const declaration of stmt.get("declarations")) {
        const id = declaration.get("id");
        const init = declaration.get("init");
        if (hasNode(init)) {
          const value = lowerExpressionToTemporary(builder, init);
          lowerAssignment(
            builder,
            stmt.node.loc ?? GeneratedSource,
            kind,
            id,
            value,
            id.isObjectPattern() || id.isArrayPattern()
              ? "Destructure"
              : "Assignment"
          );
        } else if (id.isIdentifier()) {
          const binding = builder.resolveIdentifier(id);
          if (binding.kind !== "Identifier") {
            builder.errors.push({
              reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
              severity: ErrorSeverity.Invariant,
              loc: id.node.loc ?? null,
              suggestions: null,
            });
          } else {
            const place: Place = {
              effect: Effect.Unknown,
              identifier: binding.identifier,
              kind: "Identifier",
              reactive: false,
              loc: id.node.loc ?? GeneratedSource,
            };
            if (builder.isContextIdentifier(id)) {
              if (kind === InstructionKind.Const) {
                const declRangeStart = declaration.parentPath.node.start!;
                builder.errors.push({
                  reason: `Expect \`const\` declaration not to be reassigned`,
                  severity: ErrorSeverity.InvalidJS,
                  loc: id.node.loc ?? null,
                  suggestions: [
                    {
                      description: "Change to a `let` declaration",
                      op: CompilerSuggestionOperation.Replace,
                      range: [declRangeStart, declRangeStart + 5], // "const".length
                      text: "let",
                    },
                  ],
                });
              }
              lowerValueToTemporary(builder, {
                kind: "DeclareContext",
                lvalue: {
                  kind: InstructionKind.Let,
                  place,
                },
                loc: id.node.loc ?? GeneratedSource,
              });
            } else {
              const typeAnnotation = id.get("typeAnnotation");
              let type: t.FlowType | t.TSType | null;
              if (typeAnnotation.isTSTypeAnnotation()) {
                const typePath = typeAnnotation.get("typeAnnotation");
                type = typePath.node;
              } else if (typeAnnotation.isTypeAnnotation()) {
                const typePath = typeAnnotation.get("typeAnnotation");
                type = typePath.node;
              } else {
                type = null;
              }
              lowerValueToTemporary(builder, {
                kind: "DeclareLocal",
                lvalue: {
                  kind,
                  place,
                },
                type,
                loc: id.node.loc ?? GeneratedSource,
              });
            }
          }
        } else {
          builder.errors.push({
            reason: `Expected variable declaration to be an identifier if no initializer was provided`,
            description: `Got a \`${id.type}\``,
            severity: ErrorSeverity.InvalidJS,
            loc: stmt.node.loc ?? null,
            suggestions: null,
          });
        }
      }
      return;
    }
    case "ExpressionStatement": {
      const stmt = stmtPath as NodePath<t.ExpressionStatement>;
      const expression = stmt.get("expression");
      lowerExpressionToTemporary(builder, expression);
      return;
    }
    case "DoWhileStatement": {
      const stmt = stmtPath as NodePath<t.DoWhileStatement>;
      //  Block used to evaluate whether to (re)enter or exit the loop
      const conditionalBlock = builder.reserve("loop");
      //  Block for code following the loop
      const continuationBlock = builder.reserve("block");
      //  Loop body, executed at least once uncondtionally prior to exit
      const loopBlock = builder.enter("block", (_loopBlockId) => {
        return builder.loop(
          label,
          conditionalBlock.id,
          continuationBlock.id,
          () => {
            const body = stmt.get("body");
            lowerStatement(builder, body);
            return {
              kind: "goto",
              block: conditionalBlock.id,
              variant: GotoVariant.Continue,
              id: makeInstructionId(0),
              loc: body.node.loc ?? GeneratedSource,
            };
          }
        );
      });
      /*
       * Jump to the conditional block to evaluate whether to (re)enter the loop or exit to the
       * continuation block.
       */
      const loc = stmt.node.loc ?? GeneratedSource;
      builder.terminateWithContinuation(
        {
          kind: "do-while",
          loc,
          test: conditionalBlock.id,
          loop: loopBlock,
          fallthrough: continuationBlock.id,
          id: makeInstructionId(0),
        },
        conditionalBlock
      );
      /*
       * The conditional block is empty and exists solely as conditional for
       * (re)entering or exiting the loop
       */
      const test = lowerExpressionToTemporary(builder, stmt.get("test"));
      const terminal: BranchTerminal = {
        kind: "branch",
        test,
        consequent: loopBlock,
        alternate: continuationBlock.id,
        id: makeInstructionId(0),
        loc,
      };
      //  Complete the conditional and continue with code after the loop
      builder.terminateWithContinuation(terminal, continuationBlock);
      return;
    }
    case "FunctionDeclaration": {
      const stmt = stmtPath as NodePath<t.FunctionDeclaration>;
      stmt.skip();
      CompilerError.invariant(stmt.get("id").type === "Identifier", {
        reason: "function declarations must have a name",
        description: null,
        loc: stmt.node.loc ?? null,
        suggestions: null,
      });
      const id = stmt.get("id") as NodePath<t.Identifier>;

      const fn = lowerValueToTemporary(
        builder,
        lowerFunctionToValue(builder, stmt)
      );
      lowerAssignment(
        builder,
        stmt.node.loc ?? GeneratedSource,
        InstructionKind.Let,
        id,
        fn,
        "Assignment"
      );

      return;
    }
    case "ForOfStatement": {
      const stmt = stmtPath as NodePath<t.ForOfStatement>;
      const continuationBlock = builder.reserve("block");
      const initBlock = builder.reserve("loop");
      const testBlock = builder.reserve("loop");

      if (stmt.node.await) {
        builder.errors.push({
          reason: `(BuildHIR::lowerStatement) Handle for-await loops`,
          severity: ErrorSeverity.Todo,
          loc: stmt.node.loc ?? null,
          suggestions: null,
        });
        return;
      }

      const loopBlock = builder.enter("block", (_blockId) => {
        return builder.loop(label, initBlock.id, continuationBlock.id, () => {
          const body = stmt.get("body");
          lowerStatement(builder, body);
          return {
            kind: "goto",
            block: initBlock.id,
            variant: GotoVariant.Continue,
            id: makeInstructionId(0),
            loc: body.node.loc ?? GeneratedSource,
          };
        });
      });

      const loc = stmt.node.loc ?? GeneratedSource;
      const value = lowerExpressionToTemporary(builder, stmt.get("right"));
      builder.terminateWithContinuation(
        {
          kind: "for-of",
          loc,
          init: initBlock.id,
          test: testBlock.id,
          loop: loopBlock,
          fallthrough: continuationBlock.id,
          id: makeInstructionId(0),
        },
        initBlock
      );

      /*
       * The init of a ForOf statement is compound over a left (VariableDeclaration | LVal) and
       * right (Expression), so we synthesize a new InstrValue and assignment (potentially multiple
       * instructions when we handle other syntax like Patterns)
       */
      const iterator = lowerValueToTemporary(builder, {
        kind: "GetIterator",
        loc: value.loc,
        collection: { ...value },
      });
      builder.terminateWithContinuation(
        {
          id: makeInstructionId(0),
          kind: "goto",
          block: testBlock.id,
          variant: GotoVariant.Break,
          loc: stmt.node.loc ?? GeneratedSource,
        },
        testBlock
      );

      const left = stmt.get("left");
      const leftLoc = left.node.loc ?? GeneratedSource;
      let test: Place;
      if (left.isVariableDeclaration()) {
        const declarations = left.get("declarations");
        CompilerError.invariant(declarations.length === 1, {
          reason: `Expected only one declaration in the init of a ForOfStatement, got ${declarations.length}`,
          description: null,
          loc: left.node.loc ?? null,
          suggestions: null,
        });
        const id = declarations[0].get("id");
        const advanceIterator = lowerValueToTemporary(builder, {
          kind: "IteratorNext",
          loc: leftLoc,
          iterator: { ...iterator },
          collection: { ...value },
        });
        const assign = lowerAssignment(
          builder,
          leftLoc,
          InstructionKind.Let,
          id,
          advanceIterator,
          "Assignment"
        );
        test = lowerValueToTemporary(builder, assign);
      } else {
        builder.errors.push({
          reason: `(BuildHIR::lowerStatement) Handle ${left.type} inits in ForOfStatement`,
          severity: ErrorSeverity.Todo,
          loc: left.node.loc ?? null,
          suggestions: null,
        });
        return;
      }
      builder.terminateWithContinuation(
        {
          id: makeInstructionId(0),
          kind: "branch",
          test,
          consequent: loopBlock,
          alternate: continuationBlock.id,
          loc: stmt.node.loc ?? GeneratedSource,
        },
        continuationBlock
      );
      return;
    }
    case "ForInStatement": {
      const stmt = stmtPath as NodePath<t.ForInStatement>;
      const continuationBlock = builder.reserve("block");
      const initBlock = builder.reserve("loop");

      const loopBlock = builder.enter("block", (_blockId) => {
        return builder.loop(label, initBlock.id, continuationBlock.id, () => {
          const body = stmt.get("body");
          lowerStatement(builder, body);
          return {
            kind: "goto",
            block: initBlock.id,
            variant: GotoVariant.Continue,
            id: makeInstructionId(0),
            loc: body.node.loc ?? GeneratedSource,
          };
        });
      });

      const loc = stmt.node.loc ?? GeneratedSource;
      const value = lowerExpressionToTemporary(builder, stmt.get("right"));
      builder.terminateWithContinuation(
        {
          kind: "for-in",
          loc,
          init: initBlock.id,
          loop: loopBlock,
          fallthrough: continuationBlock.id,
          id: makeInstructionId(0),
        },
        initBlock
      );

      /*
       * The init of a ForIn statement is compound over a left (VariableDeclaration | LVal) and
       * right (Expression), so we synthesize a new InstrValue and assignment (potentially multiple
       * instructions when we handle other syntax like Patterns)
       */
      const left = stmt.get("left");
      const leftLoc = left.node.loc ?? GeneratedSource;
      let test: Place;
      if (left.isVariableDeclaration()) {
        const declarations = left.get("declarations");
        CompilerError.invariant(declarations.length === 1, {
          reason: `Expected only one declaration in the init of a ForInStatement, got ${declarations.length}`,
          description: null,
          loc: left.node.loc ?? null,
          suggestions: null,
        });
        const id = declarations[0].get("id");
        const nextPropertyTemp = lowerValueToTemporary(builder, {
          kind: "NextPropertyOf",
          loc: leftLoc,
          value,
        });
        const assign = lowerAssignment(
          builder,
          leftLoc,
          InstructionKind.Let,
          id,
          nextPropertyTemp,
          "Assignment"
        );
        test = lowerValueToTemporary(builder, assign);
      } else {
        builder.errors.push({
          reason: `(BuildHIR::lowerStatement) Handle ${left.type} inits in ForInStatement`,
          severity: ErrorSeverity.Todo,
          loc: left.node.loc ?? null,
          suggestions: null,
        });
        return;
      }
      builder.terminateWithContinuation(
        {
          id: makeInstructionId(0),
          kind: "branch",
          test,
          consequent: loopBlock,
          alternate: continuationBlock.id,
          loc: stmt.node.loc ?? GeneratedSource,
        },
        continuationBlock
      );
      return;
    }
    case "DebuggerStatement": {
      const stmt = stmtPath as NodePath<t.DebuggerStatement>;
      const loc = stmt.node.loc ?? GeneratedSource;
      builder.push({
        id: makeInstructionId(0),
        lvalue: buildTemporaryPlace(builder, loc),
        value: {
          kind: "Debugger",
          loc,
        },
        loc,
      });
      return;
    }
    case "EmptyStatement": {
      return;
    }
    case "TryStatement": {
      const stmt = stmtPath as NodePath<t.TryStatement>;
      const continuationBlock = builder.reserve("block");

      const handlerPath = stmt.get("handler");
      if (!hasNode(handlerPath)) {
        builder.errors.push({
          reason: `(BuildHIR::lowerStatement) Handle TryStatement without a catch clause`,
          severity: ErrorSeverity.Todo,
          loc: stmt.node.loc ?? null,
          suggestions: null,
        });
        return;
      }
      if (hasNode(stmt.get("finalizer"))) {
        builder.errors.push({
          reason: `(BuildHIR::lowerStatement) Handle TryStatement with a finalizer ('finally') clause`,
          severity: ErrorSeverity.Todo,
          loc: stmt.node.loc ?? null,
          suggestions: null,
        });
      }

      const handlerBindingPath = handlerPath.get("param");
      let handlerBinding: {
        place: Place;
        path: NodePath<t.Identifier | t.ArrayPattern | t.ObjectPattern>;
      } | null = null;
      if (hasNode(handlerBindingPath)) {
        const place: Place = {
          kind: "Identifier",
          identifier: builder.makeTemporary(
            handlerBindingPath.node.loc ?? GeneratedSource
          ),
          effect: Effect.Unknown,
          reactive: false,
          loc: handlerBindingPath.node.loc ?? GeneratedSource,
        };
        promoteTemporary(place.identifier);
        lowerValueToTemporary(builder, {
          kind: "DeclareLocal",
          lvalue: {
            kind: InstructionKind.Catch,
            place: { ...place },
          },
          type: null,
          loc: handlerBindingPath.node.loc ?? GeneratedSource,
        });

        handlerBinding = {
          path: handlerBindingPath,
          place,
        };
      }

      const handler = builder.enter("catch", (_blockId) => {
        if (handlerBinding !== null) {
          lowerAssignment(
            builder,
            handlerBinding.path.node.loc ?? GeneratedSource,
            InstructionKind.Catch,
            handlerBinding.path,
            { ...handlerBinding.place },
            "Assignment"
          );
        }
        lowerStatement(builder, handlerPath.get("body"));
        return {
          kind: "goto",
          block: continuationBlock.id,
          variant: GotoVariant.Break,
          id: makeInstructionId(0),
          loc: handlerPath.node.loc ?? GeneratedSource,
        };
      });

      const block = builder.enter("block", (_blockId) => {
        const block = stmt.get("block");
        builder.enterTryCatch(handler, () => {
          lowerStatement(builder, block);
        });
        return {
          kind: "goto",
          block: continuationBlock.id,
          variant: GotoVariant.Try,
          id: makeInstructionId(0),
          loc: block.node.loc ?? GeneratedSource,
        };
      });

      builder.terminateWithContinuation(
        {
          kind: "try",
          block,
          handlerBinding:
            handlerBinding !== null ? { ...handlerBinding.place } : null,
          handler,
          fallthrough: continuationBlock.id,
          id: makeInstructionId(0),
          loc: stmt.node.loc ?? GeneratedSource,
        },
        continuationBlock
      );

      return;
    }
    case "TypeAlias":
    case "TSTypeAliasDeclaration": {
      // We do not preserve type annotations/syntax through transformation
      return;
    }
    case "ClassDeclaration":
    case "DeclareClass":
    case "DeclareExportAllDeclaration":
    case "DeclareExportDeclaration":
    case "DeclareFunction":
    case "DeclareInterface":
    case "DeclareModule":
    case "DeclareModuleExports":
    case "DeclareOpaqueType":
    case "DeclareTypeAlias":
    case "DeclareVariable":
    case "EnumDeclaration":
    case "ExportAllDeclaration":
    case "ExportDefaultDeclaration":
    case "ExportNamedDeclaration":
    case "ImportDeclaration":
    case "InterfaceDeclaration":
    case "OpaqueType":
    case "TSDeclareFunction":
    case "TSEnumDeclaration":
    case "TSExportAssignment":
    case "TSImportEqualsDeclaration":
    case "TSInterfaceDeclaration":
    case "TSModuleDeclaration":
    case "TSNamespaceExportDeclaration":
    case "WithStatement": {
      builder.errors.push({
        reason: `(BuildHIR::lowerStatement) Handle ${stmtPath.type} statements`,
        severity: ErrorSeverity.Todo,
        loc: stmtPath.node.loc ?? null,
        suggestions: null,
      });
      lowerValueToTemporary(builder, {
        kind: "UnsupportedNode",
        loc: stmtPath.node.loc ?? GeneratedSource,
        node: stmtPath.node,
      });
      return;
    }
    default: {
      return assertExhaustive(
        stmtNode,
        `Unsupported statement kind '${
          (stmtNode as any as NodePath<t.Statement>).type
        }'`
      );
    }
  }
}

function lowerObjectMethod(
  builder: HIRBuilder,
  property: NodePath<t.ObjectMethod>
): InstructionValue {
  const loc = property.node.loc ?? GeneratedSource;
  const loweredFunc = lowerFunction(builder, property);
  if (!loweredFunc) {
    return { kind: "UnsupportedNode", node: property.node, loc: loc };
  }

  return {
    kind: "ObjectMethod",
    loc,
    loweredFunc,
  };
}

function lowerObjectPropertyKey(
  builder: HIRBuilder,
  property: NodePath<t.ObjectProperty | t.ObjectMethod>
): ObjectPropertyKey | null {
  const key = property.get("key");
  if (key.isStringLiteral()) {
    return {
      kind: "string",
      name: key.node.value,
    };
  } else if (property.node.computed && key.isExpression()) {
    if (!key.isIdentifier()) {
      /*
       * NOTE: allowing complex key expressions can trigger a bug where a mutation is made conditional
       * see fixture
       * error.object-expression-computed-key-modified-during-after-construction.js
       */
      builder.errors.push({
        reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
        severity: ErrorSeverity.Todo,
        loc: key.node.loc ?? null,
        suggestions: null,
      });
      return null;
    }
    const place = lowerExpressionToTemporary(builder, key);
    return {
      kind: "computed",
      name: place,
    };
  } else if (key.isIdentifier()) {
    return {
      kind: "identifier",
      name: key.node.name,
    };
  }

  builder.errors.push({
    reason: `(BuildHIR::lowerExpression) Expected Identifier, got ${key.type} key in ObjectExpression`,
    severity: ErrorSeverity.Todo,
    loc: key.node.loc ?? null,
    suggestions: null,
  });
  return null;
}

function lowerExpression(
  builder: HIRBuilder,
  exprPath: NodePath<t.Expression>
): InstructionValue {
  const exprNode = exprPath.node;
  const exprLoc = exprNode.loc ?? GeneratedSource;
  switch (exprNode.type) {
    case "Identifier": {
      const expr = exprPath as NodePath<t.Identifier>;
      const place = lowerIdentifier(builder, expr);
      return {
        kind: getLoadKind(builder, expr),
        place,
        loc: exprLoc,
      };
    }
    case "NullLiteral": {
      return {
        kind: "Primitive",
        value: null,
        loc: exprLoc,
      };
    }
    case "BooleanLiteral":
    case "NumericLiteral":
    case "StringLiteral": {
      const expr = exprPath as NodePath<
        t.StringLiteral | t.BooleanLiteral | t.NumericLiteral
      >;
      const value = expr.node.value;
      return {
        kind: "Primitive",
        value,
        loc: exprLoc,
      };
    }
    case "ObjectExpression": {
      const expr = exprPath as NodePath<t.ObjectExpression>;
      const propertyPaths = expr.get("properties");
      const properties: Array<ObjectProperty | SpreadPattern> = [];
      for (const propertyPath of propertyPaths) {
        if (propertyPath.isObjectProperty()) {
          const loweredKey = lowerObjectPropertyKey(builder, propertyPath);
          if (!loweredKey) {
            continue;
          }
          const valuePath = propertyPath.get("value");
          if (!valuePath.isExpression()) {
            builder.errors.push({
              reason: `(BuildHIR::lowerExpression) Handle ${valuePath.type} values in ObjectExpression`,
              severity: ErrorSeverity.Todo,
              loc: valuePath.node.loc ?? null,
              suggestions: null,
            });
            continue;
          }
          const value = lowerExpressionToTemporary(builder, valuePath);
          properties.push({
            kind: "ObjectProperty",
            type: "property",
            place: value,
            key: loweredKey,
          });
        } else if (propertyPath.isSpreadElement()) {
          const place = lowerExpressionToTemporary(
            builder,
            propertyPath.get("argument")
          );
          properties.push({
            kind: "Spread",
            place,
          });
        } else if (propertyPath.isObjectMethod()) {
          if (propertyPath.node.kind !== "method") {
            builder.errors.push({
              reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.node.kind} functions in ObjectExpression`,
              severity: ErrorSeverity.Todo,
              loc: propertyPath.node.loc ?? null,
              suggestions: null,
            });
            continue;
          }
          const method = lowerObjectMethod(builder, propertyPath);
          const place = lowerValueToTemporary(builder, method);
          const loweredKey = lowerObjectPropertyKey(builder, propertyPath);
          if (!loweredKey) {
            continue;
          }
          properties.push({
            kind: "ObjectProperty",
            type: "method",
            place,
            key: loweredKey,
          });
        } else {
          builder.errors.push({
            reason: `(BuildHIR::lowerExpression) Handle ${propertyPath.type} properties in ObjectExpression`,
            severity: ErrorSeverity.Todo,
            loc: propertyPath.node.loc ?? null,
            suggestions: null,
          });
          continue;
        }
      }
      return {
        kind: "ObjectExpression",
        properties,
        loc: exprLoc,
      };
    }
    case "ArrayExpression": {
      const expr = exprPath as NodePath<t.ArrayExpression>;
      let elements: ArrayExpression["elements"] = [];
      for (const element of expr.get("elements")) {
        if (element.node == null) {
          elements.push({
            kind: "Hole",
          });
          continue;
        } else if (element.isExpression()) {
          elements.push(lowerExpressionToTemporary(builder, element));
        } else if (element.isSpreadElement()) {
          const place = lowerExpressionToTemporary(
            builder,
            element.get("argument")
          );
          elements.push({ kind: "Spread", place });
        } else {
          builder.errors.push({
            reason: `(BuildHIR::lowerExpression) Handle ${element.type} elements in ArrayExpression`,
            severity: ErrorSeverity.Todo,
            loc: element.node.loc ?? null,
            suggestions: null,
          });
          continue;
        }
      }
      return {
        kind: "ArrayExpression",
        elements,
        loc: exprLoc,
      };
    }
    case "NewExpression": {
      const expr = exprPath as NodePath<t.NewExpression>;
      const calleePath = expr.get("callee");
      if (!calleePath.isExpression()) {
        builder.errors.push({
          reason: `Expected an expression as the \`new\` expression receiver (v8 intrinsics are not supported)`,
          description: `Got a \`${calleePath.node.type}\``,
          severity: ErrorSeverity.InvalidJS,
          loc: calleePath.node.loc ?? null,
          suggestions: null,
        });
        return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
      }
      const callee = lowerExpressionToTemporary(builder, calleePath);
      const args = lowerArguments(builder, expr.get("arguments"));

      return {
        kind: "NewExpression",
        callee,
        args,
        loc: exprLoc,
      };
    }
    case "OptionalCallExpression": {
      const expr = exprPath as NodePath<t.OptionalCallExpression>;
      return lowerOptionalCallExpression(builder, expr, null);
    }
    case "CallExpression": {
      const expr = exprPath as NodePath<t.CallExpression>;
      const calleePath = expr.get("callee");
      if (!calleePath.isExpression()) {
        builder.errors.push({
          reason: `Expected Expression, got ${calleePath.type} in CallExpression (v8 intrinsics not supported). This error is likely caused by a bug in React Compiler. Please file an issue`,
          severity: ErrorSeverity.Todo,
          loc: calleePath.node.loc ?? null,
          suggestions: null,
        });
        return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
      }
      if (calleePath.isMemberExpression()) {
        const memberExpr = lowerMemberExpression(builder, calleePath);
        const propertyPlace = lowerValueToTemporary(builder, memberExpr.value);
        const args = lowerArguments(builder, expr.get("arguments"));
        return {
          kind: "MethodCall",
          receiver: memberExpr.object,
          property: { ...propertyPlace },
          args,
          loc: exprLoc,
        };
      } else {
        const callee = lowerExpressionToTemporary(builder, calleePath);
        const args = lowerArguments(builder, expr.get("arguments"));
        return {
          kind: "CallExpression",
          callee,
          args,
          loc: exprLoc,
        };
      }
    }
    case "BinaryExpression": {
      const expr = exprPath as NodePath<t.BinaryExpression>;
      const leftPath = expr.get("left");
      if (!leftPath.isExpression()) {
        builder.errors.push({
          reason: `(BuildHIR::lowerExpression) Expected Expression, got ${leftPath.type} lval in BinaryExpression`,
          severity: ErrorSeverity.Todo,
          loc: leftPath.node.loc ?? null,
          suggestions: null,
        });
        return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
      }
      const left = lowerExpressionToTemporary(builder, leftPath);
      const right = lowerExpressionToTemporary(builder, expr.get("right"));
      const operator = expr.node.operator;
      if (operator === "|>") {
        builder.errors.push({
          reason: `(BuildHIR::lowerExpression) Pipe operator not supported`,
          severity: ErrorSeverity.Todo,
          loc: leftPath.node.loc ?? null,
          suggestions: null,
        });
        return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
      }
      return {
        kind: "BinaryExpression",
        operator,
        left,
        right,
        loc: exprLoc,
      };
    }
    case "SequenceExpression": {
      const expr = exprPath as NodePath<t.SequenceExpression>;
      const exprLoc = expr.node.loc ?? GeneratedSource;

      const continuationBlock = builder.reserve(builder.currentBlockKind());
      const place = buildTemporaryPlace(builder, exprLoc);

      const sequenceBlock = builder.enter("sequence", (_) => {
        let last: Place | null = null;
        for (const item of expr.get("expressions")) {
          last = lowerExpressionToTemporary(builder, item);
        }
        if (last === null) {
          builder.errors.push({
            reason: `Expected sequence expression to have at least one expression`,
            severity: ErrorSeverity.InvalidJS,
            loc: expr.node.loc ?? null,
            suggestions: null,
          });
        } else {
          lowerValueToTemporary(builder, {
            kind: "StoreLocal",
            lvalue: { kind: InstructionKind.Const, place: { ...place } },
            value: last,
            type: null,
            loc: exprLoc,
          });
        }
        return {
          kind: "goto",
          id: makeInstructionId(0),
          block: continuationBlock.id,
          loc: exprLoc,
          variant: GotoVariant.Break,
        };
      });

      builder.terminateWithContinuation(
        {
          kind: "sequence",
          block: sequenceBlock,
          fallthrough: continuationBlock.id,
          id: makeInstructionId(0),
          loc: exprLoc,
        },
        continuationBlock
      );
      return { kind: "LoadLocal", place, loc: place.loc };
    }
    case "ConditionalExpression": {
      const expr = exprPath as NodePath<t.ConditionalExpression>;
      const exprLoc = expr.node.loc ?? GeneratedSource;

      //  Block for code following the if
      const continuationBlock = builder.reserve(builder.currentBlockKind());
      const testBlock = builder.reserve("value");
      const place = buildTemporaryPlace(builder, exprLoc);

      //  Block for the consequent (if the test is truthy)
      const consequentBlock = builder.enter("value", (_blockId) => {
        const consequentPath = expr.get("consequent");
        const consequent = lowerExpressionToTemporary(builder, consequentPath);
        lowerValueToTemporary(builder, {
          kind: "StoreLocal",
          lvalue: { kind: InstructionKind.Const, place: { ...place } },
          value: consequent,
          type: null,
          loc: exprLoc,
        });
        return {
          kind: "goto",
          block: continuationBlock.id,
          variant: GotoVariant.Break,
          id: makeInstructionId(0),
          loc: consequentPath.node.loc ?? GeneratedSource,
        };
      });
      //  Block for the alternate (if the test is not truthy)
      const alternateBlock = builder.enter("value", (_blockId) => {
        const alternatePath = expr.get("alternate");
        const alternate = lowerExpressionToTemporary(builder, alternatePath);
        lowerValueToTemporary(builder, {
          kind: "StoreLocal",
          lvalue: { kind: InstructionKind.Const, place: { ...place } },
          value: alternate,
          type: null,
          loc: exprLoc,
        });
        return {
          kind: "goto",
          block: continuationBlock.id,
          variant: GotoVariant.Break,
          id: makeInstructionId(0),
          loc: alternatePath.node.loc ?? GeneratedSource,
        };
      });

      builder.terminateWithContinuation(
        {
          kind: "ternary",
          fallthrough: continuationBlock.id,
          id: makeInstructionId(0),
          test: testBlock.id,
          loc: exprLoc,
        },
        testBlock
      );
      const testPlace = lowerExpressionToTemporary(builder, expr.get("test"));
      builder.terminateWithContinuation(
        {
          kind: "branch",
          test: { ...testPlace },
          consequent: consequentBlock,
          alternate: alternateBlock,
          id: makeInstructionId(0),
          loc: exprLoc,
        },
        continuationBlock
      );
      return { kind: "LoadLocal", place, loc: place.loc };
    }
    case "LogicalExpression": {
      const expr = exprPath as NodePath<t.LogicalExpression>;
      const exprLoc = expr.node.loc ?? GeneratedSource;
      const continuationBlock = builder.reserve(builder.currentBlockKind());
      const testBlock = builder.reserve("value");
      const place = buildTemporaryPlace(builder, exprLoc);
      const leftPlace = buildTemporaryPlace(
        builder,
        expr.get("left").node.loc ?? GeneratedSource
      );
      const consequent = builder.enter("value", () => {
        lowerValueToTemporary(builder, {
          kind: "StoreLocal",
          lvalue: { kind: InstructionKind.Const, place: { ...place } },
          value: { ...leftPlace },
          type: null,
          loc: leftPlace.loc,
        });
        return {
          kind: "goto",
          block: continuationBlock.id,
          variant: GotoVariant.Break,
          id: makeInstructionId(0),
          loc: leftPlace.loc,
        };
      });
      const alternate = builder.enter("value", () => {
        const right = lowerExpressionToTemporary(builder, expr.get("right"));
        lowerValueToTemporary(builder, {
          kind: "StoreLocal",
          lvalue: { kind: InstructionKind.Const, place: { ...place } },
          value: { ...right },
          type: null,
          loc: right.loc,
        });
        return {
          kind: "goto",
          block: continuationBlock.id,
          variant: GotoVariant.Break,
          id: makeInstructionId(0),
          loc: right.loc,
        };
      });
      builder.terminateWithContinuation(
        {
          kind: "logical",
          fallthrough: continuationBlock.id,
          id: makeInstructionId(0),
          test: testBlock.id,
          operator: expr.node.operator,
          loc: exprLoc,
        },
        testBlock
      );
      const leftValue = lowerExpressionToTemporary(builder, expr.get("left"));
      builder.push({
        id: makeInstructionId(0),
        lvalue: { ...leftPlace },
        value: {
          kind: "LoadLocal",
          place: leftValue,
          loc: exprLoc,
        },
        loc: exprLoc,
      });
      builder.terminateWithContinuation(
        {
          kind: "branch",
          test: { ...leftPlace },
          consequent,
          alternate,
          id: makeInstructionId(0),
          loc: exprLoc,
        },
        continuationBlock
      );
      return { kind: "LoadLocal", place, loc: place.loc };
    }
    case "AssignmentExpression": {
      const expr = exprPath as NodePath<t.AssignmentExpression>;
      const operator = expr.node.operator;

      if (operator === "=") {
        const left = expr.get("left");
        return lowerAssignment(
          builder,
          left.node.loc ?? GeneratedSource,
          InstructionKind.Reassign,
          left,
          lowerExpressionToTemporary(builder, expr.get("right")),
          left.isArrayPattern() || left.isObjectPattern()
            ? "Destructure"
            : "Assignment"
        );
      }

      const operators: {
        [key: string]: Exclude<t.BinaryExpression["operator"], "|>">;
      } = {
        "+=": "+",
        "-=": "-",
        "/=": "/",
        "%=": "%",
        "*=": "*",
        "**=": "**",
        "&=": "&",
        "|=": "|",
        ">>=": ">>",
        ">>>=": ">>>",
        "<<=": "<<",
        "^=": "^",
      };
      const binaryOperator = operators[operator];
      if (binaryOperator == null) {
        builder.errors.push({
          reason: `(BuildHIR::lowerExpression) Handle ${operator} operators in AssignmentExpression`,
          severity: ErrorSeverity.Todo,
          loc: expr.node.loc ?? null,
          suggestions: null,
        });
        return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
      }
      const left = expr.get("left");
      const leftNode = left.node;
      switch (leftNode.type) {
        case "Identifier": {
          const leftExpr = left as NodePath<t.Identifier>;
          const identifier = lowerIdentifier(builder, leftExpr);
          const leftPlace = lowerExpressionToTemporary(builder, leftExpr);
          const right = lowerExpressionToTemporary(builder, expr.get("right"));
          const binaryPlace = lowerValueToTemporary(builder, {
            kind: "BinaryExpression",
            operator: binaryOperator,
            left: leftPlace,
            right,
            loc: exprLoc,
          });
          const kind = getStoreKind(builder, leftExpr);
          if (kind === "StoreLocal") {
            lowerValueToTemporary(builder, {
              kind: "StoreLocal",
              lvalue: {
                place: { ...identifier },
                kind: InstructionKind.Reassign,
              },
              value: { ...binaryPlace },
              type: null,
              loc: exprLoc,
            });
            return { kind: "LoadLocal", place: identifier, loc: exprLoc };
          } else {
            lowerValueToTemporary(builder, {
              kind: "StoreContext",
              lvalue: {
                place: { ...identifier },
                kind: InstructionKind.Reassign,
              },
              value: { ...binaryPlace },
              loc: exprLoc,
            });
            return { kind: "LoadContext", place: identifier, loc: exprLoc };
          }
        }
        case "MemberExpression": {
          // a.b.c += <right>
          const leftExpr = left as NodePath<t.MemberExpression>;
          const { object, property, value } = lowerMemberExpression(
            builder,
            leftExpr
          );

          // Store the previous value to a temporary
          const previousValuePlace = lowerValueToTemporary(builder, value);
          // Store the new value to a temporary
          const newValuePlace = lowerValueToTemporary(builder, {
            kind: "BinaryExpression",
            operator: binaryOperator,
            left: { ...previousValuePlace },
            right: lowerExpressionToTemporary(builder, expr.get("right")),
            loc: leftExpr.node.loc ?? GeneratedSource,
          });

          // Save the result back to the property
          if (typeof property === "string") {
            return {
              kind: "PropertyStore",
              object: { ...object },
              property,
              value: { ...newValuePlace },
              loc: leftExpr.node.loc ?? GeneratedSource,
            };
          } else {
            return {
              kind: "ComputedStore",
              object: { ...object },
              property: { ...property },
              value: { ...newValuePlace },
              loc: leftExpr.node.loc ?? GeneratedSource,
            };
          }
        }
        default: {
          builder.errors.push({
            reason: `(BuildHIR::lowerExpression) Expected Identifier or MemberExpression, got ${expr.type} lval in AssignmentExpression`,
            severity: ErrorSeverity.Todo,
            loc: expr.node.loc ?? null,
            suggestions: null,
          });
          return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
        }
      }
    }
    case "OptionalMemberExpression": {
      const expr = exprPath as NodePath<t.OptionalMemberExpression>;
      const { value } = lowerOptionalMemberExpression(builder, expr, null);
      return { kind: "LoadLocal", place: value, loc: value.loc };
    }
    case "MemberExpression": {
      const expr = exprPath as NodePath<
        t.MemberExpression | t.OptionalMemberExpression
      >;
      const { value } = lowerMemberExpression(builder, expr);
      const place = lowerValueToTemporary(builder, value);
      return { kind: "LoadLocal", place, loc: place.loc };
    }
    case "JSXElement": {
      const expr = exprPath as NodePath<t.JSXElement>;
      const opening = expr.get("openingElement");
      const openingLoc = opening.node.loc ?? GeneratedSource;
      const tag = lowerJsxElementName(builder, opening.get("name"));
      const props: Array<JsxAttribute> = [];
      for (const attribute of opening.get("attributes")) {
        if (attribute.isJSXSpreadAttribute()) {
          const argument = lowerExpressionToTemporary(
            builder,
            attribute.get("argument")
          );
          props.push({ kind: "JsxSpreadAttribute", argument });
          continue;
        }
        if (!attribute.isJSXAttribute()) {
          builder.errors.push({
            reason: `(BuildHIR::lowerExpression) Handle ${attribute.type} attributes in JSXElement`,
            severity: ErrorSeverity.Todo,
            loc: attribute.node.loc ?? null,
            suggestions: null,
          });
          continue;
        }
        const namePath = attribute.get("name");
        let propName;
        if (namePath.isJSXIdentifier()) {
          propName = namePath.node.name;
          if (propName.indexOf(":") !== -1) {
            builder.errors.push({
              reason: `(BuildHIR::lowerExpression) Unexpected colon in attribute name \`${name}\``,
              severity: ErrorSeverity.Todo,
              loc: namePath.node.loc ?? null,
              suggestions: null,
            });
          }
        } else {
          CompilerError.invariant(namePath.isJSXNamespacedName(), {
            reason: "Refinement",
            description: null,
            loc: namePath.node.loc ?? null,
            suggestions: null,
          });
          const namespace = namePath.node.namespace.name;
          const name = namePath.node.name.name;
          propName = `${namespace}:${name}`;
        }
        const valueExpr = attribute.get("value");
        let value;
        if (valueExpr.isJSXElement() || valueExpr.isStringLiteral()) {
          value = lowerExpressionToTemporary(builder, valueExpr);
        } else if (valueExpr.type == null) {
          value = lowerValueToTemporary(builder, {
            kind: "Primitive",
            value: true,
            loc: attribute.node.loc ?? GeneratedSource,
          });
        } else {
          if (!valueExpr.isJSXExpressionContainer()) {
            builder.errors.push({
              reason: `(BuildHIR::lowerExpression) Handle ${valueExpr.type} attribute values in JSXElement`,
              severity: ErrorSeverity.Todo,
              loc: valueExpr.node?.loc ?? null,
              suggestions: null,
            });
            continue;
          }
          const expression = valueExpr.get("expression");
          if (!expression.isExpression()) {
            builder.errors.push({
              reason: `(BuildHIR::lowerExpression) Handle ${expression.type} expressions in JSXExpressionContainer within JSXElement`,
              severity: ErrorSeverity.Todo,
              loc: valueExpr.node.loc ?? null,
              suggestions: null,
            });
            continue;
          }
          value = lowerExpressionToTemporary(builder, expression);
        }
        props.push({ kind: "JsxAttribute", name: propName, place: value });
      }
      if (
        tag.kind === "BuiltinTag" &&
        (tag.name === "fbt" || tag.name === "fbs")
      ) {
        const tagName = tag.name;
        const openingIdentifier = opening.get("name");
        const tagIdentifier = openingIdentifier.isJSXIdentifier()
          ? builder.resolveIdentifier(openingIdentifier)
          : null;
        if (tagIdentifier != null && tagIdentifier.kind === "Identifier") {
          CompilerError.throwTodo({
            reason: `Support <${tagName}> tags where '${tagName}' is a local variable instead of a global`,
            loc: openingIdentifier.node.loc ?? GeneratedSource,
            description: null,
            suggestions: null,
          });
        }
        const fbtEnumLocations: Array<SourceLocation> = [];
        expr.traverse({
          JSXNamespacedName(path) {
            if (
              path.node.namespace.name === tagName &&
              path.node.name.name === "enum"
            ) {
              fbtEnumLocations.push(path.node.loc ?? GeneratedSource);
            }
          },
        });
        if (fbtEnumLocations.length > 1) {
          CompilerError.throwTodo({
            reason: `Support <${tagName}> tags with multiple <${tagName}:enum> values`,
            loc: fbtEnumLocations.at(-1) ?? GeneratedSource,
            description: null,
            suggestions: null,
          });
        }
      }

      let children: Array<Place>;
      if (
        tag.kind === "BuiltinTag" &&
        (tag.name === "fbt" || tag.name === "fbs")
      ) {
        children = expr
          .get("children")
          .map((child) => {
            if (child.isJSXText()) {
              /*
               * FBT whitespace normalization differs from standard JSX:
               * https://github.com/facebook/fbt/blob/0b4e0d13c30bffd0daa2a75715d606e3587b4e40/packages/babel-plugin-fbt/src/FbtUtil.js#L76-L87
               */
              const text = child.node.value.replace(/[^\S\u00A0]+/g, " ");
              return lowerValueToTemporary(builder, {
                kind: "JSXText",
                value: text,
                loc: child.node.loc ?? GeneratedSource,
              });
            }
            return lowerJsxElement(builder, child);
          })
          .filter(notNull);
      } else {
        children = expr
          .get("children")
          .map((child) => lowerJsxElement(builder, child))
          .filter(notNull);
      }
      return {
        kind: "JsxExpression",
        tag,
        props,
        children: children.length === 0 ? null : children,
        loc: exprLoc,
        openingLoc: openingLoc,
        closingLoc: expr.get("closingElement").node?.loc ?? GeneratedSource,
      };
    }
    case "JSXFragment": {
      const expr = exprPath as NodePath<t.JSXFragment>;
      const children: Array<Place> = expr
        .get("children")
        .map((child) => lowerJsxElement(builder, child))
        .filter(notNull);
      return {
        kind: "JsxFragment",
        children,
        loc: exprLoc,
      };
    }
    case "ArrowFunctionExpression":
    case "FunctionExpression": {
      const expr = exprPath as NodePath<
        t.FunctionExpression | t.ArrowFunctionExpression
      >;
      return lowerFunctionToValue(builder, expr);
    }
    case "TaggedTemplateExpression": {
      const expr = exprPath as NodePath<t.TaggedTemplateExpression>;
      if (expr.get("quasi").get("expressions").length !== 0) {
        builder.errors.push({
          reason:
            "(BuildHIR::lowerExpression) Handle tagged template with interpolations",
          severity: ErrorSeverity.Todo,
          loc: exprPath.node.loc ?? null,
          suggestions: null,
        });
        return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
      }
      CompilerError.invariant(expr.get("quasi").get("quasis").length == 1, {
        reason:
          "there should be only one quasi as we don't support interpolations yet",
        description: null,
        loc: expr.node.loc ?? null,
        suggestions: null,
      });
      const value = expr.get("quasi").get("quasis").at(0)!.node.value;
      if (value.raw !== value.cooked) {
        builder.errors.push({
          reason:
            "(BuildHIR::lowerExpression) Handle tagged template where cooked value is different from raw value",
          severity: ErrorSeverity.Todo,
          loc: exprPath.node.loc ?? null,
          suggestions: null,
        });
        return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
      }

      return {
        kind: "TaggedTemplateExpression",
        tag: lowerExpressionToTemporary(builder, expr.get("tag")),
        value,
        loc: exprLoc,
      };
    }
    case "TemplateLiteral": {
      const expr = exprPath as NodePath<t.TemplateLiteral>;
      const subexprs = expr.get("expressions");
      const quasis = expr.get("quasis");

      if (subexprs.length !== quasis.length - 1) {
        builder.errors.push({
          reason: `Unexpected quasi and subexpression lengths in template literal`,
          severity: ErrorSeverity.InvalidJS,
          loc: exprPath.node.loc ?? null,
          suggestions: null,
        });
        return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
      }

      if (subexprs.some((e) => !e.isExpression())) {
        builder.errors.push({
          reason: `(BuildHIR::lowerAssignment) Handle TSType in TemplateLiteral.`,
          severity: ErrorSeverity.Todo,
          loc: exprPath.node.loc ?? null,
          suggestions: null,
        });
        return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
      }

      const subexprPlaces = subexprs.map((e) =>
        lowerExpressionToTemporary(builder, e as NodePath<t.Expression>)
      );

      return {
        kind: "TemplateLiteral",
        subexprs: subexprPlaces,
        quasis: expr.get("quasis").map((q) => q.node.value),
        loc: exprLoc,
      };
    }
    case "UnaryExpression": {
      let expr = exprPath as NodePath<t.UnaryExpression>;
      if (expr.node.operator === "delete") {
        const argument = expr.get("argument");
        if (argument.isMemberExpression()) {
          const { object, property } = lowerMemberExpression(builder, argument);
          if (typeof property === "string") {
            return {
              kind: "PropertyDelete",
              object,
              property,
              loc: exprLoc,
            };
          } else {
            return {
              kind: "ComputedDelete",
              object,
              property,
              loc: exprLoc,
            };
          }
        } else {
          builder.errors.push({
            reason: `Only object properties can be deleted`,
            severity: ErrorSeverity.InvalidJS,
            loc: expr.node.loc ?? null,
            suggestions: [
              {
                description: "Remove this line",
                range: [expr.node.start!, expr.node.end!],
                op: CompilerSuggestionOperation.Remove,
              },
            ],
          });
          return { kind: "UnsupportedNode", node: expr.node, loc: exprLoc };
        }
      } else if (expr.node.operator === "throw") {
        builder.errors.push({
          reason: `Throw expressions are not supported`,
          severity: ErrorSeverity.InvalidJS,
          loc: expr.node.loc ?? null,
          suggestions: [
            {
              description: "Remove this line",
              range: [expr.node.start!, expr.node.end!],
              op: CompilerSuggestionOperation.Remove,
            },
          ],
        });
        return { kind: "UnsupportedNode", node: expr.node, loc: exprLoc };
      } else {
        return {
          kind: "UnaryExpression",
          operator: expr.node.operator,
          value: lowerExpressionToTemporary(builder, expr.get("argument")),
          loc: exprLoc,
        };
      }
    }
    case "AwaitExpression": {
      let expr = exprPath as NodePath<t.AwaitExpression>;
      return {
        kind: "Await",
        value: lowerExpressionToTemporary(builder, expr.get("argument")),
        loc: exprLoc,
      };
    }
    case "TypeCastExpression": {
      let expr = exprPath as NodePath<t.TypeCastExpression>;
      const typeAnnotation = expr.get("typeAnnotation").get("typeAnnotation");
      return {
        kind: "TypeCastExpression",
        value: lowerExpressionToTemporary(builder, expr.get("expression")),
        typeAnnotation: typeAnnotation.node,
        type: lowerType(typeAnnotation.node),
        loc: exprLoc,
      };
    }
    case "TSAsExpression": {
      let expr = exprPath as NodePath<t.TSAsExpression>;
      const typeAnnotation = expr.get("typeAnnotation");
      return {
        kind: "TypeCastExpression",
        value: lowerExpressionToTemporary(builder, expr.get("expression")),
        typeAnnotation: typeAnnotation.node,
        type: lowerType(typeAnnotation.node),
        loc: exprLoc,
      };
    }
    case "UpdateExpression": {
      let expr = exprPath as NodePath<t.UpdateExpression>;
      const argument = expr.get("argument");
      if (!argument.isIdentifier()) {
        builder.errors.push({
          reason: `(BuildHIR::lowerExpression) Handle UpdateExpression with ${argument.type} argument`,
          severity: ErrorSeverity.Todo,
          loc: exprPath.node.loc ?? null,
          suggestions: null,
        });
        return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
      } else if (builder.isContextIdentifier(argument)) {
        builder.errors.push({
          reason: `(BuildHIR::lowerExpression) Handle UpdateExpression to variables captured within lambdas.`,
          severity: ErrorSeverity.Todo,
          loc: exprPath.node.loc ?? null,
          suggestions: null,
        });
        return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
      }
      const lvalue = lowerIdentifierForAssignment(
        builder,
        argument.node.loc ?? GeneratedSource,
        InstructionKind.Reassign,
        argument
      );
      if (lvalue === null) {
        /*
         * lowerIdentifierForAssignment should have already reported an error if it returned null,
         * we check here just in case
         */
        if (!builder.errors.hasErrors()) {
          builder.errors.push({
            reason: `(BuildHIR::lowerExpression) Found an invalid UpdateExpression without a previously reported error`,
            severity: ErrorSeverity.Invariant,
            loc: exprLoc,
            suggestions: null,
          });
        }
        return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
      } else if (lvalue.kind === "Global") {
        builder.errors.push({
          reason: `(BuildHIR::lowerExpression) Support UpdateExpression where argument is a global`,
          severity: ErrorSeverity.Todo,
          loc: exprLoc,
          suggestions: null,
        });
        return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
      }
      const value = lowerIdentifier(builder, argument);
      if (expr.node.prefix) {
        return {
          kind: "PrefixUpdate",
          lvalue,
          operation: expr.node.operator,
          value,
          loc: exprLoc,
        };
      } else {
        return {
          kind: "PostfixUpdate",
          lvalue,
          operation: expr.node.operator,
          value,
          loc: exprLoc,
        };
      }
    }
    case "RegExpLiteral": {
      let expr = exprPath as NodePath<t.RegExpLiteral>;
      return {
        kind: "RegExpLiteral",
        pattern: expr.node.pattern,
        flags: expr.node.flags,
        loc: expr.node.loc ?? GeneratedSource,
      };
    }
    case "TSNonNullExpression": {
      let expr = exprPath as NodePath<t.TSNonNullExpression>;
      return lowerExpression(builder, expr.get("expression"));
    }
    case "MetaProperty": {
      let expr = exprPath as NodePath<t.MetaProperty>;
      if (
        expr.node.meta.name === "import" &&
        expr.node.property.name === "meta"
      ) {
        return {
          kind: "MetaProperty",
          meta: expr.node.meta.name,
          property: expr.node.property.name,
          loc: expr.node.loc ?? GeneratedSource,
        };
      }

      builder.errors.push({
        reason: `(BuildHIR::lowerExpression) Handle MetaProperty expressions other than import.meta`,
        severity: ErrorSeverity.Todo,
        loc: exprPath.node.loc ?? null,
        suggestions: null,
      });
      return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
    }
    default: {
      builder.errors.push({
        reason: `(BuildHIR::lowerExpression) Handle ${exprPath.type} expressions`,
        severity: ErrorSeverity.Todo,
        loc: exprPath.node.loc ?? null,
        suggestions: null,
      });
      return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
    }
  }
}

function lowerOptionalMemberExpression(
  builder: HIRBuilder,
  expr: NodePath<t.OptionalMemberExpression>,
  parentAlternate: BlockId | null
): { object: Place; value: Place } {
  const optional = expr.node.optional;
  const loc = expr.node.loc ?? GeneratedSource;
  const place = buildTemporaryPlace(builder, loc);
  const continuationBlock = builder.reserve(builder.currentBlockKind());
  const consequent = builder.reserve("value");

  /*
   * block to evaluate if the callee is null/undefined, this sets the result of the call to undefined.
   * note that we only create an alternate when first entering an optional subtree of the ast: if this
   * is a child of an optional node, we use the alterate created by the parent.
   */
  const alternate =
    parentAlternate !== null
      ? parentAlternate
      : builder.enter("value", () => {
          const temp = lowerValueToTemporary(builder, {
            kind: "Primitive",
            value: undefined,
            loc,
          });
          lowerValueToTemporary(builder, {
            kind: "StoreLocal",
            lvalue: { kind: InstructionKind.Const, place: { ...place } },
            value: { ...temp },
            type: null,
            loc,
          });
          return {
            kind: "goto",
            variant: GotoVariant.Break,
            block: continuationBlock.id,
            id: makeInstructionId(0),
            loc,
          };
        });

  let object: Place | null = null;
  const testBlock = builder.enter("value", () => {
    const objectPath = expr.get("object");
    if (objectPath.isOptionalMemberExpression()) {
      const { value } = lowerOptionalMemberExpression(
        builder,
        objectPath,
        alternate
      );
      object = value;
    } else if (objectPath.isOptionalCallExpression()) {
      const value = lowerOptionalCallExpression(builder, objectPath, alternate);
      object = lowerValueToTemporary(builder, value);
    } else {
      object = lowerExpressionToTemporary(builder, objectPath);
    }
    return {
      kind: "branch",
      test: { ...object },
      consequent: consequent.id,
      alternate,
      id: makeInstructionId(0),
      loc,
    };
  });
  CompilerError.invariant(object !== null, {
    reason: "Satisfy type checker",
    description: null,
    loc: null,
    suggestions: null,
  });

  /*
   * 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
   */
  builder.enterReserved(consequent, () => {
    const { value } = lowerMemberExpression(builder, expr, object);
    const temp = lowerValueToTemporary(builder, value);
    lowerValueToTemporary(builder, {
      kind: "StoreLocal",
      lvalue: { kind: InstructionKind.Const, place: { ...place } },
      value: { ...temp },
      type: null,
      loc,
    });
    return {
      kind: "goto",
      variant: GotoVariant.Break,
      block: continuationBlock.id,
      id: makeInstructionId(0),
      loc,
    };
  });

  builder.terminateWithContinuation(
    {
      kind: "optional",
      optional,
      test: testBlock,
      fallthrough: continuationBlock.id,
      id: makeInstructionId(0),
      loc,
    },
    continuationBlock
  );

  return { object, value: place };
}

function lowerOptionalCallExpression(
  builder: HIRBuilder,
  expr: NodePath<t.OptionalCallExpression>,
  parentAlternate: BlockId | null
): InstructionValue {
  const optional = expr.node.optional;
  const calleePath = expr.get("callee");
  const loc = expr.node.loc ?? GeneratedSource;
  const place = buildTemporaryPlace(builder, loc);
  const continuationBlock = builder.reserve(builder.currentBlockKind());
  const consequent = builder.reserve("value");

  /*
   * block to evaluate if the callee is null/undefined, this sets the result of the call to undefined.
   * note that we only create an alternate when first entering an optional subtree of the ast: if this
   * is a child of an optional node, we use the alterate created by the parent.
   */
  const alternate =
    parentAlternate !== null
      ? parentAlternate
      : builder.enter("value", () => {
          const temp = lowerValueToTemporary(builder, {
            kind: "Primitive",
            value: undefined,
            loc,
          });
          lowerValueToTemporary(builder, {
            kind: "StoreLocal",
            lvalue: { kind: InstructionKind.Const, place: { ...place } },
            value: { ...temp },
            type: null,
            loc,
          });
          return {
            kind: "goto",
            variant: GotoVariant.Break,
            block: continuationBlock.id,
            id: makeInstructionId(0),
            loc,
          };
        });

  /*
   * Lower the callee within the test block to represent the fact that the code for the callee is
   * scoped within the optional
   */
  let callee:
    | { kind: "CallExpression"; callee: Place }
    | { kind: "MethodCall"; receiver: Place; property: Place };
  const testBlock = builder.enter("value", () => {
    if (calleePath.isOptionalCallExpression()) {
      // Recursively call lowerOptionalCallExpression to thread down the alternate block
      const value = lowerOptionalCallExpression(builder, calleePath, alternate);
      const valuePlace = lowerValueToTemporary(builder, value);
      callee = {
        kind: "CallExpression",
        callee: valuePlace,
      };
    } else if (calleePath.isOptionalMemberExpression()) {
      const { object, value } = lowerOptionalMemberExpression(
        builder,
        calleePath,
        alternate
      );
      callee = {
        kind: "MethodCall",
        receiver: object,
        property: value,
      };
    } else if (calleePath.isMemberExpression()) {
      const memberExpr = lowerMemberExpression(builder, calleePath);
      const propertyPlace = lowerValueToTemporary(builder, memberExpr.value);
      callee = {
        kind: "MethodCall",
        receiver: memberExpr.object,
        property: propertyPlace,
      };
    } else {
      callee = {
        kind: "CallExpression",
        callee: lowerExpressionToTemporary(builder, calleePath),
      };
    }
    const testPlace =
      callee.kind === "CallExpression" ? callee.callee : callee.property;
    return {
      kind: "branch",
      test: { ...testPlace },
      consequent: consequent.id,
      alternate,
      id: makeInstructionId(0),
      loc,
    };
  });

  /*
   * 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
   */
  builder.enterReserved(consequent, () => {
    const args = lowerArguments(builder, expr.get("arguments"));
    const temp = buildTemporaryPlace(builder, loc);
    if (callee.kind === "CallExpression") {
      builder.push({
        id: makeInstructionId(0),
        lvalue: { ...temp },
        value: {
          kind: "CallExpression",
          callee: { ...callee.callee },
          args,
          loc,
        },
        loc,
      });
    } else {
      builder.push({
        id: makeInstructionId(0),
        lvalue: { ...temp },
        value: {
          kind: "MethodCall",
          receiver: { ...callee.receiver },
          property: { ...callee.property },
          args,
          loc,
        },
        loc,
      });
    }
    lowerValueToTemporary(builder, {
      kind: "StoreLocal",
      lvalue: { kind: InstructionKind.Const, place: { ...place } },
      value: { ...temp },
      type: null,
      loc,
    });
    return {
      kind: "goto",
      variant: GotoVariant.Break,
      block: continuationBlock.id,
      id: makeInstructionId(0),
      loc,
    };
  });

  builder.terminateWithContinuation(
    {
      kind: "optional",
      optional,
      test: testBlock,
      fallthrough: continuationBlock.id,
      id: makeInstructionId(0),
      loc,
    },
    continuationBlock
  );

  return { kind: "LoadLocal", place, loc: place.loc };
}

/*
 * There are a few places where we do not preserve original evaluation ordering and/or control flow, such as
 * switch case test values and default values in destructuring (assignment patterns). In these cases we allow
 * simple expressions whose evaluation cannot be observed:
 *  - primitives
 *  - arrays/objects whose values are also safely reorderable.
 */
function lowerReorderableExpression(
  builder: HIRBuilder,
  expr: NodePath<t.Expression>
): Place {
  if (!isReorderableExpression(builder, expr, true)) {
    builder.errors.push({
      reason: `(BuildHIR::node.lowerReorderableExpression) Expression type \`${expr.type}\` cannot be safely reordered`,
      severity: ErrorSeverity.Todo,
      loc: expr.node.loc ?? null,
      suggestions: null,
    });
  }
  return lowerExpressionToTemporary(builder, expr);
}

function isReorderableExpression(
  builder: HIRBuilder,
  expr: NodePath<t.Expression>,
  allowLocalIdentifiers: boolean
): boolean {
  switch (expr.node.type) {
    case "Identifier": {
      const binding = builder.resolveIdentifier(expr as NodePath<t.Identifier>);
      if (binding.kind === "Identifier") {
        return allowLocalIdentifiers;
      } else {
        // global, definitely safe
        return true;
      }
    }
    case "RegExpLiteral":
    case "StringLiteral":
    case "NumericLiteral":
    case "NullLiteral":
    case "BooleanLiteral":
    case "BigIntLiteral": {
      return true;
    }
    case "UnaryExpression": {
      const unary = expr as NodePath<t.UnaryExpression>;
      switch (expr.node.operator) {
        case "!":
        case "+":
        case "-": {
          return isReorderableExpression(
            builder,
            unary.get("argument"),
            allowLocalIdentifiers
          );
        }
        default: {
          return false;
        }
      }
    }
    case "TypeCastExpression": {
      return isReorderableExpression(
        builder,
        (expr as NodePath<t.TypeCastExpression>).get("expression"),
        allowLocalIdentifiers
      );
    }
    case "ConditionalExpression": {
      const conditional = expr as NodePath<t.ConditionalExpression>;
      return (
        isReorderableExpression(
          builder,
          conditional.get("test"),
          allowLocalIdentifiers
        ) &&
        isReorderableExpression(
          builder,
          conditional.get("consequent"),
          allowLocalIdentifiers
        ) &&
        isReorderableExpression(
          builder,
          conditional.get("alternate"),
          allowLocalIdentifiers
        )
      );
    }
    case "ArrayExpression": {
      return (expr as NodePath<t.ArrayExpression>)
        .get("elements")
        .every(
          (element) =>
            element.isExpression() &&
            isReorderableExpression(builder, element, allowLocalIdentifiers)
        );
    }
    case "ObjectExpression": {
      return (expr as NodePath<t.ObjectExpression>)
        .get("properties")
        .every((property) => {
          if (!property.isObjectProperty() || property.node.computed) {
            return false;
          }
          const value = property.get("value");
          return (
            value.isExpression() &&
            isReorderableExpression(builder, value, allowLocalIdentifiers)
          );
        });
    }
    case "MemberExpression": {
      /*
       * A common pattern is switch statements where the case test values are properties of a global,
       * eg `case ProductOptions.Option: { ... }`
       * We therefore allow expressions where the innermost object is a global identifier, and reject
       * all other member expressions (for now).
       */
      const test = expr as NodePath<t.MemberExpression>;
      let innerObject: NodePath<t.Expression> = test;
      while (innerObject.isMemberExpression()) {
        innerObject = innerObject.get("object");
      }
      if (
        innerObject.isIdentifier() &&
        builder.resolveIdentifier(innerObject).kind !== "Identifier"
      ) {
        // This is a property/computed load from a global, that's safe to reorder
        return true;
      } else {
        return false;
      }
    }
    case "ArrowFunctionExpression": {
      const fn = expr as NodePath<t.ArrowFunctionExpression>;
      const body = fn.get("body");
      if (body.node.type === "BlockStatement") {
        return body.node.body.length === 0;
      } else {
        // For TypeScript
        invariant(body.isExpression(), "Expected an expression");
        return isReorderableExpression(
          builder,
          body,
          /* disallow local identifiers in the body */ false
        );
      }
    }
    case "CallExpression": {
      const call = expr as NodePath<t.CallExpression>;
      const callee = call.get("callee");
      return (
        callee.isExpression() &&
        isReorderableExpression(builder, callee, allowLocalIdentifiers) &&
        call
          .get("arguments")
          .every(
            (arg) =>
              arg.isExpression() &&
              isReorderableExpression(builder, arg, allowLocalIdentifiers)
          )
      );
    }
    default: {
      return false;
    }
  }
}

function lowerArguments(
  builder: HIRBuilder,
  expr: Array<
    NodePath<
      | t.Expression
      | t.SpreadElement
      | t.JSXNamespacedName
      | t.ArgumentPlaceholder
    >
  >
): Array<Place | SpreadPattern> {
  let args: Array<Place | SpreadPattern> = [];
  for (const argPath of expr) {
    if (argPath.isSpreadElement()) {
      args.push({
        kind: "Spread",
        place: lowerExpressionToTemporary(builder, argPath.get("argument")),
      });
    } else if (argPath.isExpression()) {
      args.push(lowerExpressionToTemporary(builder, argPath));
    } else {
      builder.errors.push({
        reason: `(BuildHIR::lowerExpression) Handle ${argPath.type} arguments in CallExpression`,
        severity: ErrorSeverity.Todo,
        loc: argPath.node.loc ?? null,
        suggestions: null,
      });
    }
  }
  return args;
}

type LoweredMemberExpression = {
  object: Place;
  property: Place | string;
  value: InstructionValue;
};
function lowerMemberExpression(
  builder: HIRBuilder,
  expr: NodePath<t.MemberExpression | t.OptionalMemberExpression>,
  loweredObject: Place | null = null
): LoweredMemberExpression {
  const exprNode = expr.node;
  const exprLoc = exprNode.loc ?? GeneratedSource;
  const objectNode = expr.get("object");
  const propertyNode = expr.get("property");
  const object =
    loweredObject ?? lowerExpressionToTemporary(builder, objectNode);

  if (!expr.node.computed) {
    if (!propertyNode.isIdentifier()) {
      builder.errors.push({
        reason: `(BuildHIR::lowerMemberExpression) Handle ${propertyNode.type} property`,
        severity: ErrorSeverity.Todo,
        loc: propertyNode.node.loc ?? null,
        suggestions: null,
      });
      return {
        object,
        property: propertyNode.toString(),
        value: { kind: "UnsupportedNode", node: exprNode, loc: exprLoc },
      };
    }
    const value: InstructionValue = {
      kind: "PropertyLoad",
      object: { ...object },
      property: propertyNode.node.name,
      loc: exprLoc,
    };
    return { object, property: propertyNode.node.name, value };
  } else {
    if (!propertyNode.isExpression()) {
      builder.errors.push({
        reason: `(BuildHIR::lowerMemberExpression) Expected Expression, got ${propertyNode.type} property`,
        severity: ErrorSeverity.Todo,
        loc: propertyNode.node.loc ?? null,
        suggestions: null,
      });
      return {
        object,
        property: propertyNode.toString(),
        value: {
          kind: "UnsupportedNode",
          node: exprNode,
          loc: exprLoc,
        },
      };
    }
    const property = lowerExpressionToTemporary(builder, propertyNode);
    const value: InstructionValue = {
      kind: "ComputedLoad",
      object: { ...object },
      property: { ...property },
      loc: exprLoc,
    };
    return { object, property, value };
  }
}

function lowerJsxElementName(
  builder: HIRBuilder,
  exprPath: NodePath<
    t.JSXIdentifier | t.JSXMemberExpression | t.JSXNamespacedName
  >
): Place | BuiltinTag {
  const exprNode = exprPath.node;
  const exprLoc = exprNode.loc ?? GeneratedSource;
  if (exprPath.isJSXIdentifier()) {
    const tag: string = exprPath.node.name;
    if (tag.match(/^[A-Z]/)) {
      const kind = getLoadKind(builder, exprPath);
      return lowerValueToTemporary(builder, {
        kind: kind,
        place: lowerIdentifier(builder, exprPath),
        loc: exprLoc,
      });
    } else {
      return {
        kind: "BuiltinTag",
        name: tag,
        loc: exprLoc,
      };
    }
  } else if (exprPath.isJSXMemberExpression()) {
    return lowerJsxMemberExpression(builder, exprPath);
  } else if (exprPath.isJSXNamespacedName()) {
    const namespace = exprPath.node.namespace.name;
    const name = exprPath.node.name.name;
    const tag = `${namespace}:${name}`;
    if (namespace.indexOf(":") !== -1 || name.indexOf(":") !== -1) {
      builder.errors.push({
        reason: `Expected JSXNamespacedName to have no colons in the namespace or name`,
        description: `Got \`${namespace}\` : \`${name}\``,
        severity: ErrorSeverity.InvalidJS,
        loc: exprPath.node.loc ?? null,
        suggestions: null,
      });
    }
    const place = lowerValueToTemporary(builder, {
      kind: "Primitive",
      value: tag,
      loc: exprLoc,
    });
    return place;
  } else {
    builder.errors.push({
      reason: `(BuildHIR::lowerJsxElementName) Handle ${exprPath.type} tags`,
      severity: ErrorSeverity.Todo,
      loc: exprPath.node.loc ?? null,
      suggestions: null,
    });
    return lowerValueToTemporary(builder, {
      kind: "UnsupportedNode",
      node: exprNode,
      loc: exprLoc,
    });
  }
}

function lowerJsxMemberExpression(
  builder: HIRBuilder,
  exprPath: NodePath<t.JSXMemberExpression>
): Place {
  const loc = exprPath.node.loc ?? GeneratedSource;
  const object = exprPath.get("object");
  let objectPlace: Place;
  if (object.isJSXMemberExpression()) {
    objectPlace = lowerJsxMemberExpression(builder, object);
  } else {
    CompilerError.invariant(object.isJSXIdentifier(), {
      reason: `TypeScript refinement fail: expected 'JsxIdentifier', got \`${object.node.type}\``,
      description: null,
      loc: object.node.loc ?? null,
      suggestions: null,
    });
    objectPlace = lowerIdentifier(builder, object);
  }
  const property = exprPath.get("property").node.name;
  return lowerValueToTemporary(builder, {
    kind: "PropertyLoad",
    object: objectPlace,
    property,
    loc,
  });
}

function lowerJsxElement(
  builder: HIRBuilder,
  exprPath: NodePath<
    | t.JSXText
    | t.JSXExpressionContainer
    | t.JSXSpreadChild
    | t.JSXElement
    | t.JSXFragment
  >
): Place | null {
  const exprNode = exprPath.node;
  const exprLoc = exprNode.loc ?? GeneratedSource;
  if (exprPath.isJSXElement() || exprPath.isJSXFragment()) {
    return lowerExpressionToTemporary(builder, exprPath);
  } else if (exprPath.isJSXExpressionContainer()) {
    const expression = exprPath.get("expression");
    if (expression.isJSXEmptyExpression()) {
      return null;
    } else {
      CompilerError.invariant(expression.isExpression(), {
        reason: `(BuildHIR::lowerJsxElement) Expected Expression but found ${expression.type}!`,
        description: null,
        loc: expression.node.loc ?? null,
        suggestions: null,
      });
      return lowerExpressionToTemporary(builder, expression);
    }
  } else if (exprPath.isJSXText()) {
    const text = trimJsxText(exprPath.node.value);
    if (text === null) {
      return null;
    }
    const place = lowerValueToTemporary(builder, {
      kind: "JSXText",
      value: text,
      loc: exprLoc,
    });
    return place;
  } else {
    builder.errors.push({
      reason: `(BuildHIR::lowerJsxElement) Unhandled JsxElement, got: ${exprPath.type}`,
      severity: ErrorSeverity.Todo,
      loc: exprPath.node.loc ?? null,
      suggestions: null,
    });
    const place = lowerValueToTemporary(builder, {
      kind: "UnsupportedNode",
      node: exprNode,
      loc: exprLoc,
    });
    return place;
  }
}

/*
 * Trims whitespace according to the JSX spec:
 * > JSX removes whitespace at the beginning and ending of a line.
 * > It also removes blank lines. New lines adjacent to tags are removed;
 * > new lines that occur in the middle of string literals are condensed
 * > into a single space.
 *
 * From https://legacy.reactjs.org/docs/jsx-in-depth.html#string-literals-1
 *
 * Implementation adapted from Babel:
 * https://github.com/babel/babel/blob/54d30f206057be64b496d2da1ec8c49d244ba4e4/packages/babel-types/src/utils/react/cleanJSXElementLiteralChild.ts#L5
 */
function trimJsxText(original: string): string | null {
  const lines = original.split(/\r\n|\n|\r/);

  let lastNonEmptyLine = 0;

  for (let i = 0; i < lines.length; i++) {
    if (lines[i].match(/[^ \t]/)) {
      lastNonEmptyLine = i;
    }
  }

  let str = "";

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];

    const isFirstLine = i === 0;
    const isLastLine = i === lines.length - 1;
    const isLastNonEmptyLine = i === lastNonEmptyLine;

    // replace rendered whitespace tabs with spaces
    let trimmedLine = line.replace(/\t/g, " ");

    // trim whitespace touching a newline
    if (!isFirstLine) {
      trimmedLine = trimmedLine.replace(/^[ ]+/, "");
    }

    // trim whitespace touching an endline
    if (!isLastLine) {
      trimmedLine = trimmedLine.replace(/[ ]+$/, "");
    }

    if (trimmedLine) {
      if (!isLastNonEmptyLine) {
        trimmedLine += " ";
      }

      str += trimmedLine;
    }
  }

  if (str.length !== 0) {
    return str;
  } else {
    return null;
  }
}

function lowerFunctionToValue(
  builder: HIRBuilder,
  expr: NodePath<
    t.FunctionExpression | t.ArrowFunctionExpression | t.FunctionDeclaration
  >
): InstructionValue {
  const exprNode = expr.node;
  const exprLoc = exprNode.loc ?? GeneratedSource;
  let name: string | null = null;
  if (expr.isFunctionExpression()) {
    name = expr.get("id")?.node?.name ?? null;
  }
  const loweredFunc = lowerFunction(builder, expr);
  if (!loweredFunc) {
    return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
  }
  return {
    kind: "FunctionExpression",
    name,
    expr: expr.node,
    loc: exprLoc,
    loweredFunc,
  };
}

function lowerFunction(
  builder: HIRBuilder,
  expr: NodePath<
    | t.FunctionExpression
    | t.ArrowFunctionExpression
    | t.FunctionDeclaration
    | t.ObjectMethod
  >
): LoweredFunction | null {
  const componentScope: Scope = builder.parentFunction.scope;
  const captured = gatherCapturedDeps(builder, expr, componentScope);

  /*
   * TODO(gsn): In the future, we could only pass in the context identifiers
   * that are actually used by this function and it's nested functions, rather
   * than all context identifiers.
   *
   * This isn't a problem in practice because use Babel's scope analysis to
   * identify the correct references.
   */
  const lowering = lower(
    expr,
    builder.environment,
    builder.bindings,
    [...builder.context, ...captured.identifiers],
    builder.parentFunction
  );
  let loweredFunc: HIRFunction;
  if (lowering.isErr()) {
    lowering
      .unwrapErr()
      .details.forEach((detail) => builder.errors.pushErrorDetail(detail));
    return null;
  }
  loweredFunc = lowering.unwrap();
  return {
    func: loweredFunc,
    dependencies: captured.refs,
  };
}

function lowerExpressionToTemporary(
  builder: HIRBuilder,
  exprPath: NodePath<t.Expression>
): Place {
  const value = lowerExpression(builder, exprPath);
  return lowerValueToTemporary(builder, value);
}

function lowerValueToTemporary(
  builder: HIRBuilder,
  value: InstructionValue
): Place {
  if (value.kind === "LoadLocal" && value.place.identifier.name === null) {
    return value.place;
  }
  const place: Place = buildTemporaryPlace(builder, value.loc);
  builder.push({
    id: makeInstructionId(0),
    value: value,
    loc: value.loc,
    lvalue: { ...place },
  });
  return place;
}

function lowerIdentifier(
  builder: HIRBuilder,
  exprPath: NodePath<t.Identifier | t.JSXIdentifier>
): Place {
  const exprNode = exprPath.node;
  const exprLoc = exprNode.loc ?? GeneratedSource;
  const binding = builder.resolveIdentifier(exprPath);
  switch (binding.kind) {
    case "Identifier": {
      const place: Place = {
        kind: "Identifier",
        identifier: binding.identifier,
        effect: Effect.Unknown,
        reactive: false,
        loc: exprLoc,
      };
      return place;
    }
    default: {
      return lowerValueToTemporary(builder, {
        kind: "LoadGlobal",
        binding,
        loc: exprLoc,
      });
    }
  }
}

// Creates a temporary Identifier and Place referencing that identifier.
function buildTemporaryPlace(builder: HIRBuilder, loc: SourceLocation): Place {
  const place: Place = {
    kind: "Identifier",
    identifier: builder.makeTemporary(loc),
    effect: Effect.Unknown,
    reactive: false,
    loc,
  };
  return place;
}

function getStoreKind(
  builder: HIRBuilder,
  identifier: NodePath<t.Identifier>
): "StoreLocal" | "StoreContext" {
  const isContext = builder.isContextIdentifier(identifier);
  return isContext ? "StoreContext" : "StoreLocal";
}

function getLoadKind(
  builder: HIRBuilder,
  identifier: NodePath<t.Identifier | t.JSXIdentifier>
): "LoadLocal" | "LoadContext" {
  const isContext = builder.isContextIdentifier(identifier);
  return isContext ? "LoadContext" : "LoadLocal";
}

function lowerIdentifierForAssignment(
  builder: HIRBuilder,
  loc: SourceLocation,
  kind: InstructionKind,
  path: NodePath<t.Identifier>
): Place | { kind: "Global"; name: string } | null {
  const binding = builder.resolveIdentifier(path);
  if (binding.kind !== "Identifier") {
    if (kind === InstructionKind.Reassign) {
      return { kind: "Global", name: path.node.name };
    } else {
      // Else its an internal error bc we couldn't find the binding
      builder.errors.push({
        reason: `(BuildHIR::lowerAssignment) Could not find binding for declaration.`,
        severity: ErrorSeverity.Invariant,
        loc: path.node.loc ?? null,
        suggestions: null,
      });
      return null;
    }
  } else if (
    binding.bindingKind === "const" &&
    kind === InstructionKind.Reassign
  ) {
    builder.errors.push({
      reason: `Cannot reassign a \`const\` variable`,
      severity: ErrorSeverity.InvalidJS,
      loc: path.node.loc ?? null,
      description:
        binding.identifier.name != null
          ? `\`${binding.identifier.name.value}\` is declared as const`
          : null,
    });
    return null;
  }

  const place: Place = {
    kind: "Identifier",
    identifier: binding.identifier,
    effect: Effect.Unknown,
    reactive: false,
    loc,
  };
  return place;
}

function lowerAssignment(
  builder: HIRBuilder,
  loc: SourceLocation,
  kind: InstructionKind,
  lvaluePath: NodePath<t.LVal>,
  value: Place,
  assignmentKind: "Destructure" | "Assignment"
): InstructionValue {
  const lvalueNode = lvaluePath.node;
  switch (lvalueNode.type) {
    case "Identifier": {
      const lvalue = lvaluePath as NodePath<t.Identifier>;
      const place = lowerIdentifierForAssignment(builder, loc, kind, lvalue);
      if (place === null) {
        return {
          kind: "UnsupportedNode",
          loc: lvalue.node.loc ?? GeneratedSource,
          node: lvalue.node,
        };
      } else if (place.kind === "Global") {
        const temporary = lowerValueToTemporary(builder, {
          kind: "StoreGlobal",
          name: place.name,
          value,
          loc,
        });
        return { kind: "LoadLocal", place: temporary, loc: temporary.loc };
      }
      const isHoistedIdentifier = builder.environment.isHoistedIdentifier(
        lvalue.node
      );

      let temporary;
      if (builder.isContextIdentifier(lvalue)) {
        if (kind !== InstructionKind.Reassign && !isHoistedIdentifier) {
          if (kind === InstructionKind.Const) {
            builder.errors.push({
              reason: `Expected \`const\` declaration not to be reassigned`,
              severity: ErrorSeverity.InvalidJS,
              loc: lvalue.node.loc ?? null,
              suggestions: null,
            });
          }
          lowerValueToTemporary(builder, {
            kind: "DeclareContext",
            lvalue: {
              kind: InstructionKind.Let,
              place: { ...place },
            },
            loc: place.loc,
          });
        }

        temporary = lowerValueToTemporary(builder, {
          kind: "StoreContext",
          lvalue: { place: { ...place }, kind: InstructionKind.Reassign },
          value,
          loc,
        });
      } else {
        const typeAnnotation = lvalue.get("typeAnnotation");
        let type: t.FlowType | t.TSType | null;
        if (typeAnnotation.isTSTypeAnnotation()) {
          const typePath = typeAnnotation.get("typeAnnotation");
          type = typePath.node;
        } else if (typeAnnotation.isTypeAnnotation()) {
          const typePath = typeAnnotation.get("typeAnnotation");
          type = typePath.node;
        } else {
          type = null;
        }
        temporary = lowerValueToTemporary(builder, {
          kind: "StoreLocal",
          lvalue: { place: { ...place }, kind },
          value,
          type,
          loc,
        });
      }
      return { kind: "LoadLocal", place: temporary, loc: temporary.loc };
    }
    case "MemberExpression": {
      // This can only occur because of a coding error, parsers enforce this condition
      CompilerError.invariant(kind === InstructionKind.Reassign, {
        reason: "MemberExpression may only appear in an assignment expression",
        description: null,
        loc: lvaluePath.node.loc ?? null,
        suggestions: null,
      });
      const lvalue = lvaluePath as NodePath<t.MemberExpression>;
      const property = lvalue.get("property");
      const object = lowerExpressionToTemporary(builder, lvalue.get("object"));
      if (!lvalue.node.computed) {
        if (!property.isIdentifier()) {
          builder.errors.push({
            reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in MemberExpression`,
            severity: ErrorSeverity.Todo,
            loc: property.node.loc ?? null,
            suggestions: null,
          });
          return { kind: "UnsupportedNode", node: lvalueNode, loc };
        }
        const temporary = lowerValueToTemporary(builder, {
          kind: "PropertyStore",
          object,
          property: property.node.name,
          value,
          loc,
        });
        return { kind: "LoadLocal", place: temporary, loc: temporary.loc };
      } else {
        if (!property.isExpression()) {
          builder.errors.push({
            reason:
              "(BuildHIR::lowerAssignment) Expected private name to appear as a non-computed property",
            severity: ErrorSeverity.Todo,
            loc: property.node.loc ?? null,
            suggestions: null,
          });
          return { kind: "UnsupportedNode", node: lvalueNode, loc };
        }
        const propertyPlace = lowerExpressionToTemporary(builder, property);
        const temporary = lowerValueToTemporary(builder, {
          kind: "ComputedStore",
          object,
          property: propertyPlace,
          value,
          loc,
        });
        return { kind: "LoadLocal", place: temporary, loc: temporary.loc };
      }
    }
    case "ArrayPattern": {
      const lvalue = lvaluePath as NodePath<t.ArrayPattern>;
      const elements = lvalue.get("elements");
      const items: ArrayPattern["items"] = [];
      const followups: Array<{ place: Place; path: NodePath<t.LVal> }> = [];
      /*
       * A given destructuring statement must contain all declarations or all
       * reassignments. This is enforced by the parser, but we rewrite nested
       * destructuring into assignment to a temporary. Therefore, if we see
       * any reassignments that are nested destructuring we fall back to
       * using temporaries for all variables, and emitting the actual reassignments
       * in follow-up statements
       */
      const forceTemporaries =
        kind === InstructionKind.Reassign &&
        (elements.some((element) => !element.isIdentifier()) ||
          elements.some(
            (element) =>
              element.isIdentifier() &&
              (getStoreKind(builder, element) !== "StoreLocal" ||
                builder.resolveIdentifier(element).kind !== "Identifier")
          ));
      for (let i = 0; i < elements.length; i++) {
        const element = elements[i];
        if (element.node == null) {
          items.push({
            kind: "Hole",
          });
          continue;
        }
        if (element.isRestElement()) {
          const argument = element.get("argument");
          if (
            argument.isIdentifier() &&
            !forceTemporaries &&
            (assignmentKind === "Assignment" ||
              getStoreKind(builder, argument) === "StoreLocal")
          ) {
            const identifier = lowerIdentifierForAssignment(
              builder,
              element.node.loc ?? GeneratedSource,
              kind,
              argument
            );
            if (identifier === null) {
              continue;
            } else if (identifier.kind === "Global") {
              builder.errors.push({
                severity: ErrorSeverity.Todo,
                reason:
                  "Expected reassignment of globals to enable forceTemporaries",
                loc: element.node.loc ?? GeneratedSource,
              });
              continue;
            }
            items.push({
              kind: "Spread",
              place: identifier,
            });
          } else {
            const temp = buildTemporaryPlace(
              builder,
              element.node.loc ?? GeneratedSource
            );
            promoteTemporary(temp.identifier);
            items.push({
              kind: "Spread",
              place: { ...temp },
            });
            followups.push({ place: temp, path: argument as NodePath<t.LVal> }); // TODO remove type cast
          }
        } else if (
          element.isIdentifier() &&
          !forceTemporaries &&
          (assignmentKind === "Assignment" ||
            getStoreKind(builder, element) === "StoreLocal")
        ) {
          const identifier = lowerIdentifierForAssignment(
            builder,
            element.node.loc ?? GeneratedSource,
            kind,
            element
          );
          if (identifier === null) {
            continue;
          } else if (identifier.kind === "Global") {
            builder.errors.push({
              severity: ErrorSeverity.Todo,
              reason:
                "Expected reassignment of globals to enable forceTemporaries",
              loc: element.node.loc ?? GeneratedSource,
            });
            continue;
          }
          items.push(identifier);
        } else {
          const temp = buildTemporaryPlace(
            builder,
            element.node.loc ?? GeneratedSource
          );
          promoteTemporary(temp.identifier);
          items.push({ ...temp });
          followups.push({ place: temp, path: element as NodePath<t.LVal> }); // TODO remove type cast
        }
      }
      const temporary = lowerValueToTemporary(builder, {
        kind: "Destructure",
        lvalue: {
          kind,
          pattern: {
            kind: "ArrayPattern",
            items,
          },
        },
        value,
        loc,
      });
      for (const { place, path } of followups) {
        lowerAssignment(
          builder,
          path.node.loc ?? loc,
          kind,
          path,
          place,
          assignmentKind
        );
      }
      return { kind: "LoadLocal", place: temporary, loc: value.loc };
    }
    case "ObjectPattern": {
      const lvalue = lvaluePath as NodePath<t.ObjectPattern>;
      const propertiesPaths = lvalue.get("properties");
      const properties: ObjectPattern["properties"] = [];
      const followups: Array<{ place: Place; path: NodePath<t.LVal> }> = [];
      /*
       * A given destructuring statement must contain all declarations or all
       * reassignments. This is enforced by the parser, but we rewrite nested
       * destructuring into assignment to a temporary. Therefore, if we see
       * any reassignments that are nested destructuring we fall back to
       * using temporaries for all variables, and emitting the actual reassignments
       * in follow-up statements
       */
      const forceTemporaries =
        kind === InstructionKind.Reassign &&
        propertiesPaths.some(
          (property) =>
            property.isRestElement() ||
            (property.isObjectProperty() &&
              (!property.get("value").isIdentifier() ||
                builder.resolveIdentifier(
                  property.get("value") as NodePath<t.Identifier>
                ).kind !== "Identifier"))
        );
      for (let i = 0; i < propertiesPaths.length; i++) {
        const property = propertiesPaths[i];
        if (property.isRestElement()) {
          const argument = property.get("argument");
          if (!argument.isIdentifier()) {
            builder.errors.push({
              reason: `(BuildHIR::lowerAssignment) Handle ${argument.node.type} rest element in ObjectPattern`,
              severity: ErrorSeverity.Todo,
              loc: argument.node.loc ?? null,
              suggestions: null,
            });
            continue;
          }
          if (
            forceTemporaries ||
            getStoreKind(builder, argument) === "StoreContext"
          ) {
            const temp = buildTemporaryPlace(
              builder,
              property.node.loc ?? GeneratedSource
            );
            promoteTemporary(temp.identifier);
            properties.push({
              kind: "Spread",
              place: { ...temp },
            });
            followups.push({ place: temp, path: argument as NodePath<t.LVal> }); // TODO remove type cast
          } else {
            const identifier = lowerIdentifierForAssignment(
              builder,
              property.node.loc ?? GeneratedSource,
              kind,
              argument
            );
            if (identifier === null) {
              continue;
            } else if (identifier.kind === "Global") {
              builder.errors.push({
                severity: ErrorSeverity.Todo,
                reason:
                  "Expected reassignment of globals to enable forceTemporaries",
                loc: property.node.loc ?? GeneratedSource,
              });
              continue;
            }
            properties.push({
              kind: "Spread",
              place: identifier,
            });
          }
        } else {
          // TODO: this should always be true given the if/else
          if (!property.isObjectProperty()) {
            builder.errors.push({
              reason: `(BuildHIR::lowerAssignment) Handle ${property.type} properties in ObjectPattern`,
              severity: ErrorSeverity.Todo,
              loc: property.node.loc ?? null,
              suggestions: null,
            });
            continue;
          }
          if (property.node.computed) {
            builder.errors.push({
              reason: `(BuildHIR::lowerAssignment) Handle computed properties in ObjectPattern`,
              severity: ErrorSeverity.Todo,
              loc: property.node.loc ?? null,
              suggestions: null,
            });
            continue;
          }
          const loweredKey = lowerObjectPropertyKey(builder, property);
          if (!loweredKey) {
            continue;
          }
          const element = property.get("value");
          if (!element.isLVal()) {
            builder.errors.push({
              reason: `(BuildHIR::lowerAssignment) Expected object property value to be an LVal, got: ${element.type}`,
              severity: ErrorSeverity.Todo,
              loc: element.node.loc ?? null,
              suggestions: null,
            });
            continue;
          }
          if (
            element.isIdentifier() &&
            !forceTemporaries &&
            (assignmentKind === "Assignment" ||
              getStoreKind(builder, element) === "StoreLocal")
          ) {
            const identifier = lowerIdentifierForAssignment(
              builder,
              element.node.loc ?? GeneratedSource,
              kind,
              element
            );
            if (identifier === null) {
              continue;
            } else if (identifier.kind === "Global") {
              builder.errors.push({
                severity: ErrorSeverity.Todo,
                reason:
                  "Expected reassignment of globals to enable forceTemporaries",
                loc: element.node.loc ?? GeneratedSource,
              });
              continue;
            }
            properties.push({
              kind: "ObjectProperty",
              type: "property",
              place: identifier,
              key: loweredKey,
            });
          } else {
            const temp = buildTemporaryPlace(
              builder,
              element.node.loc ?? GeneratedSource
            );
            promoteTemporary(temp.identifier);
            properties.push({
              kind: "ObjectProperty",
              type: "property",
              place: { ...temp },
              key: loweredKey,
            });
            followups.push({ place: temp, path: element as NodePath<t.LVal> }); // TODO remove type cast
          }
        }
      }
      const temporary = lowerValueToTemporary(builder, {
        kind: "Destructure",
        lvalue: {
          kind,
          pattern: {
            kind: "ObjectPattern",
            properties,
          },
        },
        value,
        loc,
      });
      for (const { place, path } of followups) {
        lowerAssignment(
          builder,
          path.node.loc ?? loc,
          kind,
          path,
          place,
          assignmentKind
        );
      }
      return { kind: "LoadLocal", place: temporary, loc: value.loc };
    }
    case "AssignmentPattern": {
      const lvalue = lvaluePath as NodePath<t.AssignmentPattern>;
      const loc = lvalue.node.loc ?? GeneratedSource;
      const temp = buildTemporaryPlace(builder, loc);

      const testBlock = builder.reserve("value");
      const continuationBlock = builder.reserve(builder.currentBlockKind());

      const consequent = builder.enter("value", () => {
        /*
         * Because we reorder evaluation, we restrict the allowed default values to those where
         * evaluation order is unobservable
         */
        const defaultValue = lowerReorderableExpression(
          builder,
          lvalue.get("right")
        );
        lowerValueToTemporary(builder, {
          kind: "StoreLocal",
          lvalue: { kind: InstructionKind.Const, place: { ...temp } },
          value: { ...defaultValue },
          type: null,
          loc,
        });
        return {
          kind: "goto",
          variant: GotoVariant.Break,
          block: continuationBlock.id,
          id: makeInstructionId(0),
          loc,
        };
      });

      const alternate = builder.enter("value", () => {
        lowerValueToTemporary(builder, {
          kind: "StoreLocal",
          lvalue: { kind: InstructionKind.Const, place: { ...temp } },
          value: { ...value },
          type: null,
          loc,
        });
        return {
          kind: "goto",
          variant: GotoVariant.Break,
          block: continuationBlock.id,
          id: makeInstructionId(0),
          loc,
        };
      });
      builder.terminateWithContinuation(
        {
          kind: "ternary",
          test: testBlock.id,
          fallthrough: continuationBlock.id,
          id: makeInstructionId(0),
          loc,
        },
        testBlock
      );
      const undef = lowerValueToTemporary(builder, {
        kind: "Primitive",
        value: undefined,
        loc,
      });
      const test = lowerValueToTemporary(builder, {
        kind: "BinaryExpression",
        left: { ...value },
        operator: "===",
        right: { ...undef },
        loc,
      });
      builder.terminateWithContinuation(
        {
          kind: "branch",
          test: { ...test },
          consequent,
          alternate,
          id: makeInstructionId(0),
          loc,
        },
        continuationBlock
      );

      return lowerAssignment(
        builder,
        loc,
        kind,
        lvalue.get("left"),
        temp,
        assignmentKind
      );
    }
    default: {
      builder.errors.push({
        reason: `(BuildHIR::lowerAssignment) Handle ${lvaluePath.type} assignments`,
        severity: ErrorSeverity.Todo,
        loc: lvaluePath.node.loc ?? null,
        suggestions: null,
      });
      return { kind: "UnsupportedNode", node: lvalueNode, loc };
    }
  }
}

function isValidDependency(path: NodePath<t.MemberExpression>): boolean {
  const parent: NodePath<t.Node> = path.parentPath;
  return (
    !path.node.computed &&
    !(parent.isCallExpression() && parent.get("callee") === path)
  );
}

function captureScopes({ from, to }: { from: Scope; to: Scope }): Set<Scope> {
  let scopes: Set<Scope> = new Set();
  while (from) {
    scopes.add(from);

    if (from === to) {
      break;
    }

    from = from.parent;
  }
  return scopes;
}

function gatherCapturedDeps(
  builder: HIRBuilder,
  fn: NodePath<
    | t.FunctionExpression
    | t.ArrowFunctionExpression
    | t.FunctionDeclaration
    | t.ObjectMethod
  >,
  componentScope: Scope
): { identifiers: Array<t.Identifier>; refs: Array<Place> } {
  const capturedIds: Map<t.Identifier, number> = new Map();
  const capturedRefs: Set<Place> = new Set();
  const seenPaths: Set<string> = new Set();

  /*
   * Capture all the scopes from the parent of this function up to and including
   * the component scope.
   */
  const pureScopes: Set<Scope> = captureScopes({
    from: fn.scope.parent,
    to: componentScope,
  });

  function addCapturedId(bindingIdentifier: t.Identifier): number {
    if (!capturedIds.has(bindingIdentifier)) {
      const index = capturedIds.size;
      capturedIds.set(bindingIdentifier, index);
      return index;
    } else {
      return capturedIds.get(bindingIdentifier)!;
    }
  }

  function handleMaybeDependency(
    path:
      | NodePath<t.MemberExpression>
      | NodePath<t.Identifier>
      | NodePath<t.JSXOpeningElement>
  ): void {
    // Base context variable to depend on
    let baseIdentifier: NodePath<t.Identifier> | NodePath<t.JSXIdentifier>;
    /*
     * Base expression to depend on, which (for now) may contain non side-effectful
     * member expressions
     */
    let dependency:
      | NodePath<t.MemberExpression>
      | NodePath<t.JSXMemberExpression>
      | NodePath<t.Identifier>
      | NodePath<t.JSXIdentifier>;
    if (path.isJSXOpeningElement()) {
      const name = path.get("name");
      if (!(name.isJSXMemberExpression() || name.isJSXIdentifier())) {
        // TODO: should JSX namespaced names be handled here as well?
        return;
      }
      let current: NodePath<t.JSXMemberExpression | t.JSXIdentifier> = name;
      while (current.isJSXMemberExpression()) {
        current = current.get("object");
      }
      invariant(
        current.isJSXIdentifier(),
        "Invalid logic in gatherCapturedDeps"
      );
      baseIdentifier = current;

      /*
       * Get the expression to depend on, which may involve PropertyLoads
       * for member expressions
       */
      let currentDep:
        | NodePath<t.JSXMemberExpression>
        | NodePath<t.Identifier>
        | NodePath<t.JSXIdentifier> = baseIdentifier;

      while (true) {
        const nextDep: null | NodePath<t.Node> = currentDep.parentPath;
        if (nextDep && nextDep.isJSXMemberExpression()) {
          currentDep = nextDep;
        } else {
          break;
        }
      }
      dependency = currentDep;
    } else if (path.isMemberExpression()) {
      // Calculate baseIdentifier
      let currentId: NodePath<Expression> = path;
      while (currentId.isMemberExpression()) {
        currentId = currentId.get("object");
      }
      if (!currentId.isIdentifier()) {
        return;
      }
      baseIdentifier = currentId;

      /*
       * Get the expression to depend on, which may involve PropertyLoads
       * for member expressions
       */
      let currentDep:
        | NodePath<t.MemberExpression>
        | NodePath<t.Identifier>
        | NodePath<t.JSXIdentifier> = baseIdentifier;

      while (true) {
        const nextDep: null | NodePath<t.Node> = currentDep.parentPath;
        if (
          nextDep &&
          nextDep.isMemberExpression() &&
          isValidDependency(nextDep)
        ) {
          currentDep = nextDep;
        } else {
          break;
        }
      }

      dependency = currentDep;
    } else {
      baseIdentifier = path;
      dependency = path;
    }

    /*
     * Skip dependency path, as we already tried to recursively add it (+ all subexpressions)
     * as a dependency.
     */
    dependency.skip();

    // Add the base identifier binding as a dependency.
    const binding = baseIdentifier.scope.getBinding(baseIdentifier.node.name);
    if (binding === undefined || !pureScopes.has(binding.scope)) {
      return;
    }
    const idKey = String(addCapturedId(binding.identifier));

    // Add the expression (potentially a memberexpr path) as a dependency.
    let exprKey = idKey;
    if (dependency.isMemberExpression()) {
      let pathTokens = [];
      let current: NodePath<Expression> = dependency;
      while (current.isMemberExpression()) {
        const property = current.get("property") as NodePath<t.Identifier>;
        pathTokens.push(property.node.name);
        current = current.get("object");
      }

      exprKey += "." + pathTokens.reverse().join(".");
    } else if (dependency.isJSXMemberExpression()) {
      let pathTokens = [];
      let current: NodePath<t.JSXMemberExpression | t.JSXIdentifier> =
        dependency;
      while (current.isJSXMemberExpression()) {
        const property = current.get("property");
        pathTokens.push(property.node.name);
        current = current.get("object");
      }
    }

    if (!seenPaths.has(exprKey)) {
      let loweredDep: Place;
      if (dependency.isJSXIdentifier()) {
        loweredDep = lowerValueToTemporary(builder, {
          kind: "LoadLocal",
          place: lowerIdentifier(builder, dependency),
          loc: path.node.loc ?? GeneratedSource,
        });
      } else if (dependency.isJSXMemberExpression()) {
        loweredDep = lowerJsxMemberExpression(builder, dependency);
      } else {
        loweredDep = lowerExpressionToTemporary(builder, dependency);
      }
      capturedRefs.add(loweredDep);
      seenPaths.add(exprKey);
    }
  }

  fn.traverse({
    TypeAnnotation(path) {
      path.skip();
    },
    TSTypeAnnotation(path) {
      path.skip();
    },
    TypeAlias(path) {
      path.skip();
    },
    TSTypeAliasDeclaration(path) {
      path.skip();
    },
    Expression(path) {
      if (path.isAssignmentExpression()) {
        /*
         * Babel has a bug where it doesn't visit the LHS of an
         * AssignmentExpression if it's an Identifier. Work around it by explicitly
         * visiting it.
         */
        const left = path.get("left");
        if (left.isIdentifier()) {
          handleMaybeDependency(left);
        }
        return;
      } else if (path.isJSXElement()) {
        handleMaybeDependency(path.get("openingElement"));
      } else if (path.isMemberExpression() || path.isIdentifier()) {
        handleMaybeDependency(path);
      }
    },
  });

  return { identifiers: [...capturedIds.keys()], refs: [...capturedRefs] };
}

function notNull<T>(value: T | null): value is T {
  return value !== null;
}

export function lowerType(node: t.FlowType | t.TSType): Type {
  switch (node.type) {
    case "GenericTypeAnnotation": {
      const id = node.id;
      if (id.type === "Identifier" && id.name === "Array") {
        return { kind: "Object", shapeId: BuiltInArrayId };
      }
      return makeType();
    }
    case "TSTypeReference": {
      const typeName = node.typeName;
      if (typeName.type === "Identifier" && typeName.name === "Array") {
        return { kind: "Object", shapeId: BuiltInArrayId };
      }
      return makeType();
    }
    case "ArrayTypeAnnotation":
    case "TSArrayType": {
      return { kind: "Object", shapeId: BuiltInArrayId };
    }
    case "BooleanLiteralTypeAnnotation":
    case "BooleanTypeAnnotation":
    case "NullLiteralTypeAnnotation":
    case "NumberLiteralTypeAnnotation":
    case "NumberTypeAnnotation":
    case "StringLiteralTypeAnnotation":
    case "StringTypeAnnotation":
    case "TSBooleanKeyword":
    case "TSNullKeyword":
    case "TSNumberKeyword":
    case "TSStringKeyword":
    case "TSSymbolKeyword":
    case "TSUndefinedKeyword":
    case "TSVoidKeyword":
    case "VoidTypeAnnotation": {
      return { kind: "Primitive" };
    }
    default: {
      return makeType();
    }
  }
}