/**

* Copyright (c) 2014-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import assert from “assert”; import * as leap from “./leap”; import * as meta from “./meta”; import * as util from “./util”;

let hasOwn = Object.prototype.hasOwnProperty;

function Emitter(contextId) {

assert.ok(this instanceof Emitter);

util.getTypes().assertIdentifier(contextId);

// Used to generate unique temporary names.
this.nextTempId = 0;

// In order to make sure the context object does not collide with
// anything in the local scope, we might have to rename it, so we
// refer to it symbolically instead of just assuming that it will be
// called "context".
this.contextId = contextId;

// An append-only list of Statements that grows each time this.emit is
// called.
this.listing = [];

// A sparse array whose keys correspond to locations in this.listing
// that have been marked as branch/jump targets.
this.marked = [true];

this.insertedLocs = new Set();

// The last location will be marked when this.getDispatchLoop is
// called.
this.finalLoc = this.loc();

// A list of all leap.TryEntry statements emitted.
this.tryEntries = [];

// Each time we evaluate the body of a loop, we tell this.leapManager
// to enter a nested loop context that determines the meaning of break
// and continue statements therein.
this.leapManager = new leap.LeapManager(this);

}

let Ep = Emitter.prototype; exports.Emitter = Emitter;

// Offsets into this.listing that could be used as targets for branches or // jumps are represented as numeric Literal nodes. This representation has // the amazingly convenient benefit of allowing the exact value of the // location to be determined at any time, even after generating code that // refers to the location. Ep.loc = function() {

const l = util.getTypes().numericLiteral(-1)
this.insertedLocs.add(l);
return l;

}

Ep.getInsertedLocs = function() {

return this.insertedLocs;

}

Ep.getContextId = function() {

return util.getTypes().clone(this.contextId);

}

// Sets the exact value of the given location to the offset of the next // Statement emitted. Ep.mark = function(loc) {

util.getTypes().assertLiteral(loc);
let index = this.listing.length;
if (loc.value === -1) {
  loc.value = index;
} else {
  // Locations can be marked redundantly, but their values cannot change
  // once set the first time.
  assert.strictEqual(loc.value, index);
}
this.marked[index] = true;
return loc;

};

Ep.emit = function(node) {

const t = util.getTypes();

if (t.isExpression(node)) {
  node = t.expressionStatement(node);
}

t.assertStatement(node);
this.listing.push(node);

};

// Shorthand for emitting assignment statements. This will come in handy // for assignments to temporary variables. Ep.emitAssign = function(lhs, rhs) {

this.emit(this.assign(lhs, rhs));
return lhs;

};

// Shorthand for an assignment statement. Ep.assign = function(lhs, rhs) {

const t = util.getTypes();
return t.expressionStatement(
  t.assignmentExpression("=", t.cloneDeep(lhs), rhs));

};

// Convenience function for generating expressions like context.next, // context.sent, and context.rval. Ep.contextProperty = function(name, computed) {

const t = util.getTypes();
return t.memberExpression(
  this.getContextId(),
  computed ? t.stringLiteral(name) : t.identifier(name),
  !!computed
);

};

// Shorthand for setting context.rval and jumping to `context.stop()`. Ep.stop = function(rval) {

if (rval) {
  this.setReturnValue(rval);
}

this.jump(this.finalLoc);

};

Ep.setReturnValue = function(valuePath) {

util.getTypes().assertExpression(valuePath.value);

this.emitAssign(
  this.contextProperty("rval"),
  this.explodeExpression(valuePath)
);

};

Ep.clearPendingException = function(tryLoc, assignee) {

const t = util.getTypes();

t.assertLiteral(tryLoc);

let catchCall = t.callExpression(
  this.contextProperty("catch", true),
  [t.clone(tryLoc)]
);

if (assignee) {
  this.emitAssign(assignee, catchCall);
} else {
  this.emit(catchCall);
}

};

// Emits code for an unconditional jump to the given location, even if the // exact value of the location is not yet known. Ep.jump = function(toLoc) {

this.emitAssign(this.contextProperty("next"), toLoc);
this.emit(util.getTypes().breakStatement());

};

// Conditional jump. Ep.jumpIf = function(test, toLoc) {

const t = util.getTypes();

t.assertExpression(test);
t.assertLiteral(toLoc);

this.emit(t.ifStatement(
  test,
  t.blockStatement([
    this.assign(this.contextProperty("next"), toLoc),
    t.breakStatement()
  ])
));

};

// Conditional jump, with the condition negated. Ep.jumpIfNot = function(test, toLoc) {

const t = util.getTypes();

t.assertExpression(test);
t.assertLiteral(toLoc);

let negatedTest;
if (t.isUnaryExpression(test) &&
    test.operator === "!") {
  // Avoid double negation.
  negatedTest = test.argument;
} else {
  negatedTest = t.unaryExpression("!", test);
}

this.emit(t.ifStatement(
  negatedTest,
  t.blockStatement([
    this.assign(this.contextProperty("next"), toLoc),
    t.breakStatement()
  ])
));

};

// Returns a unique MemberExpression that can be used to store and // retrieve temporary values. Since the object of the member expression is // the context object, which is presumed to coexist peacefully with all // other local variables, and since we just increment `nextTempId` // monotonically, uniqueness is assured. Ep.makeTempVar = function() {

return this.contextProperty("t" + this.nextTempId++);

};

Ep.getContextFunction = function(id) {

const t = util.getTypes();

return t.functionExpression(
  id || null/*Anonymous*/,
  [this.getContextId()],
  t.blockStatement([this.getDispatchLoop()]),
  false, // Not a generator anymore!
  false // Nor an expression.
);

};

// Turns this.listing into a loop of the form // // while (1) switch (context.next) { // case 0: // … // case n: // return context.stop(); // } // // Each marked location in this.listing will correspond to one generated // case statement. Ep.getDispatchLoop = function() {

const self = this;
const t = util.getTypes();
let cases = [];
let current;

// If we encounter a break, continue, or return statement in a switch
// case, we can skip the rest of the statements until the next case.
let alreadyEnded = false;

self.listing.forEach(function(stmt, i) {
  if (self.marked.hasOwnProperty(i)) {
    cases.push(t.switchCase(
      t.numericLiteral(i),
      current = []));
    alreadyEnded = false;
  }

  if (!alreadyEnded) {
    current.push(stmt);
    if (t.isCompletionStatement(stmt))
      alreadyEnded = true;
  }
});

// Now that we know how many statements there will be in this.listing,
// we can finally resolve this.finalLoc.value.
this.finalLoc.value = this.listing.length;

cases.push(
  t.switchCase(this.finalLoc, [
    // Intentionally fall through to the "end" case...
  ]),

  // So that the runtime can jump to the final location without having
  // to know its offset, we provide the "end" case as a synonym.
  t.switchCase(t.stringLiteral("end"), [
    // This will check/clear both context.thrown and context.rval.
    t.returnStatement(
      t.callExpression(this.contextProperty("stop"), [])
    )
  ])
);

return t.whileStatement(
  t.numericLiteral(1),
  t.switchStatement(
    t.assignmentExpression(
      "=",
      this.contextProperty("prev"),
      this.contextProperty("next")
    ),
    cases
  )
);

};

Ep.getTryLocsList = function() {

if (this.tryEntries.length === 0) {
  // To avoid adding a needless [] to the majority of runtime.wrap
  // argument lists, force the caller to handle this case specially.
  return null;
}

const t = util.getTypes();
let lastLocValue = 0;

return t.arrayExpression(
  this.tryEntries.map(function(tryEntry) {
    let thisLocValue = tryEntry.firstLoc.value;
    assert.ok(thisLocValue >= lastLocValue, "try entries out of order");
    lastLocValue = thisLocValue;

    let ce = tryEntry.catchEntry;
    let fe = tryEntry.finallyEntry;

    let locs = [
      tryEntry.firstLoc,
      // The null here makes a hole in the array.
      ce ? ce.firstLoc : null
    ];

    if (fe) {
      locs[2] = fe.firstLoc;
      locs[3] = fe.afterLoc;
    }

    return t.arrayExpression(locs.map(loc => loc && t.clone(loc)));
  })
);

};

// All side effects must be realized in order.

// If any subexpression harbors a leap, all subexpressions must be // neutered of side effects.

// No destructive modification of AST nodes.

Ep.explode = function(path, ignoreResult) {

const t = util.getTypes();
let node = path.node;
let self = this;

t.assertNode(node);

if (t.isDeclaration(node))
  throw getDeclError(node);

if (t.isStatement(node))
  return self.explodeStatement(path);

if (t.isExpression(node))
  return self.explodeExpression(path, ignoreResult);

switch (node.type) {
case "Program":
  return path.get("body").map(
    self.explodeStatement,
    self
  );

case "VariableDeclarator":
  throw getDeclError(node);

// These node types should be handled by their parent nodes
// (ObjectExpression, SwitchStatement, and TryStatement, respectively).
case "Property":
case "SwitchCase":
case "CatchClause":
  throw new Error(
    node.type + " nodes should be handled by their parents");

default:
  throw new Error(
    "unknown Node of type " +
      JSON.stringify(node.type));
}

};

function getDeclError(node) {

return new Error(
  "all declarations should have been transformed into " +
  "assignments before the Exploder began its work: " +
  JSON.stringify(node));

}

Ep.explodeStatement = function(path, labelId) {

const t = util.getTypes();
let stmt = path.node;
let self = this;
let before, after, head;

t.assertStatement(stmt);

if (labelId) {
  t.assertIdentifier(labelId);
} else {
  labelId = null;
}

// Explode BlockStatement nodes even if they do not contain a yield,
// because we don't want or need the curly braces.
if (t.isBlockStatement(stmt)) {
  path.get("body").forEach(function (path) {
    self.explodeStatement(path);
  });
  return;
}

if (!meta.containsLeap(stmt)) {
  // Technically we should be able to avoid emitting the statement
  // altogether if !meta.hasSideEffects(stmt), but that leads to
  // confusing generated code (for instance, `while (true) {}` just
  // disappears) and is probably a more appropriate job for a dedicated
  // dead code elimination pass.
  self.emit(stmt);
  return;
}

switch (stmt.type) {
case "ExpressionStatement":
  self.explodeExpression(path.get("expression"), true);
  break;

case "LabeledStatement":
  after = this.loc();

  // Did you know you can break from any labeled block statement or
  // control structure? Well, you can! Note: when a labeled loop is
  // encountered, the leap.LabeledEntry created here will immediately
  // enclose a leap.LoopEntry on the leap manager's stack, and both
  // entries will have the same label. Though this works just fine, it
  // may seem a bit redundant. In theory, we could check here to
  // determine if stmt knows how to handle its own label; for example,
  // stmt happens to be a WhileStatement and so we know it's going to
  // establish its own LoopEntry when we explode it (below). Then this
  // LabeledEntry would be unnecessary. Alternatively, we might be
  // tempted not to pass stmt.label down into self.explodeStatement,
  // because we've handled the label here, but that's a mistake because
  // labeled loops may contain labeled continue statements, which is not
  // something we can handle in this generic case. All in all, I think a
  // little redundancy greatly simplifies the logic of this case, since
  // it's clear that we handle all possible LabeledStatements correctly
  // here, regardless of whether they interact with the leap manager
  // themselves. Also remember that labels and break/continue-to-label
  // statements are rare, and all of this logic happens at transform
  // time, so it has no additional runtime cost.
  self.leapManager.withEntry(
    new leap.LabeledEntry(after, stmt.label),
    function() {
      self.explodeStatement(path.get("body"), stmt.label);
    }
  );

  self.mark(after);

  break;

case "WhileStatement":
  before = this.loc();
  after = this.loc();

  self.mark(before);
  self.jumpIfNot(self.explodeExpression(path.get("test")), after);
  self.leapManager.withEntry(
    new leap.LoopEntry(after, before, labelId),
    function() { self.explodeStatement(path.get("body")); }
  );
  self.jump(before);
  self.mark(after);

  break;

case "DoWhileStatement":
  let first = this.loc();
  let test = this.loc();
  after = this.loc();

  self.mark(first);
  self.leapManager.withEntry(
    new leap.LoopEntry(after, test, labelId),
    function() { self.explode(path.get("body")); }
  );
  self.mark(test);
  self.jumpIf(self.explodeExpression(path.get("test")), first);
  self.mark(after);

  break;

case "ForStatement":
  head = this.loc();
  let update = this.loc();
  after = this.loc();

  if (stmt.init) {
    // We pass true here to indicate that if stmt.init is an expression
    // then we do not care about its result.
    self.explode(path.get("init"), true);
  }

  self.mark(head);

  if (stmt.test) {
    self.jumpIfNot(self.explodeExpression(path.get("test")), after);
  } else {
    // No test means continue unconditionally.
  }

  self.leapManager.withEntry(
    new leap.LoopEntry(after, update, labelId),
    function() { self.explodeStatement(path.get("body")); }
  );

  self.mark(update);

  if (stmt.update) {
    // We pass true here to indicate that if stmt.update is an
    // expression then we do not care about its result.
    self.explode(path.get("update"), true);
  }

  self.jump(head);

  self.mark(after);

  break;

case "TypeCastExpression":
  return self.explodeExpression(path.get("expression"));

case "ForInStatement":
  head = this.loc();
  after = this.loc();

  let keyIterNextFn = self.makeTempVar();
  self.emitAssign(
    keyIterNextFn,
    t.callExpression(
      util.runtimeProperty("keys"),
      [self.explodeExpression(path.get("right"))]
    )
  );

  self.mark(head);

  let keyInfoTmpVar = self.makeTempVar();
  self.jumpIf(
    t.memberExpression(
      t.assignmentExpression(
        "=",
        keyInfoTmpVar,
        t.callExpression(t.cloneDeep(keyIterNextFn), [])
      ),
      t.identifier("done"),
      false
    ),
    after
  );

  self.emitAssign(
    stmt.left,
    t.memberExpression(
      t.cloneDeep(keyInfoTmpVar),
      t.identifier("value"),
      false
    )
  );

  self.leapManager.withEntry(
    new leap.LoopEntry(after, head, labelId),
    function() { self.explodeStatement(path.get("body")); }
  );

  self.jump(head);

  self.mark(after);

  break;

case "BreakStatement":
  self.emitAbruptCompletion({
    type: "break",
    target: self.leapManager.getBreakLoc(stmt.label)
  });

  break;

case "ContinueStatement":
  self.emitAbruptCompletion({
    type: "continue",
    target: self.leapManager.getContinueLoc(stmt.label)
  });

  break;

case "SwitchStatement":
  // Always save the discriminant into a temporary variable in case the
  // test expressions overwrite values like context.sent.
  let disc = self.emitAssign(
    self.makeTempVar(),
    self.explodeExpression(path.get("discriminant"))
  );

  after = this.loc();
  let defaultLoc = this.loc();
  let condition = defaultLoc;
  let caseLocs = [];

  // If there are no cases, .cases might be undefined.
  let cases = stmt.cases || [];

  for (let i = cases.length - 1; i >= 0; --i) {
    let c = cases[i];
    t.assertSwitchCase(c);

    if (c.test) {
      condition = t.conditionalExpression(
        t.binaryExpression("===", t.cloneDeep(disc), c.test),
        caseLocs[i] = this.loc(),
        condition
      );
    } else {
      caseLocs[i] = defaultLoc;
    }
  }

  let discriminant = path.get("discriminant");
  util.replaceWithOrRemove(discriminant, condition);
  self.jump(self.explodeExpression(discriminant));

  self.leapManager.withEntry(
    new leap.SwitchEntry(after),
    function() {
      path.get("cases").forEach(function(casePath) {
        let i = casePath.key;
        self.mark(caseLocs[i]);

        casePath.get("consequent").forEach(function (path) {
          self.explodeStatement(path);
        });
      });
    }
  );

  self.mark(after);
  if (defaultLoc.value === -1) {
    self.mark(defaultLoc);
    assert.strictEqual(after.value, defaultLoc.value);
  }

  break;

case "IfStatement":
  let elseLoc = stmt.alternate && this.loc();
  after = this.loc();

  self.jumpIfNot(
    self.explodeExpression(path.get("test")),
    elseLoc || after
  );

  self.explodeStatement(path.get("consequent"));

  if (elseLoc) {
    self.jump(after);
    self.mark(elseLoc);
    self.explodeStatement(path.get("alternate"));
  }

  self.mark(after);

  break;

case "ReturnStatement":
  self.emitAbruptCompletion({
    type: "return",
    value: self.explodeExpression(path.get("argument"))
  });

  break;

case "WithStatement":
  throw new Error("WithStatement not supported in generator functions.");

case "TryStatement":
  after = this.loc();

  let handler = stmt.handler;

  let catchLoc = handler && this.loc();
  let catchEntry = catchLoc && new leap.CatchEntry(
    catchLoc,
    handler.param
  );

  let finallyLoc = stmt.finalizer && this.loc();
  let finallyEntry = finallyLoc &&
    new leap.FinallyEntry(finallyLoc, after);

  let tryEntry = new leap.TryEntry(
    self.getUnmarkedCurrentLoc(),
    catchEntry,
    finallyEntry
  );

  self.tryEntries.push(tryEntry);
  self.updateContextPrevLoc(tryEntry.firstLoc);

  self.leapManager.withEntry(tryEntry, function() {
    self.explodeStatement(path.get("block"));

    if (catchLoc) {
      if (finallyLoc) {
        // If we have both a catch block and a finally block, then
        // because we emit the catch block first, we need to jump over
        // it to the finally block.
        self.jump(finallyLoc);

      } else {
        // If there is no finally block, then we need to jump over the
        // catch block to the fall-through location.
        self.jump(after);
      }

      self.updateContextPrevLoc(self.mark(catchLoc));

      let bodyPath = path.get("handler.body");
      let safeParam = self.makeTempVar();
      self.clearPendingException(tryEntry.firstLoc, safeParam);

      bodyPath.traverse(catchParamVisitor, {
        getSafeParam: () => t.cloneDeep(safeParam),
        catchParamName: handler.param.name
      });

      self.leapManager.withEntry(catchEntry, function() {
        self.explodeStatement(bodyPath);
      });
    }

    if (finallyLoc) {
      self.updateContextPrevLoc(self.mark(finallyLoc));

      self.leapManager.withEntry(finallyEntry, function() {
        self.explodeStatement(path.get("finalizer"));
      });

      self.emit(t.returnStatement(t.callExpression(
        self.contextProperty("finish"),
        [finallyEntry.firstLoc]
      )));
    }
  });

  self.mark(after);

  break;

case "ThrowStatement":
  self.emit(t.throwStatement(
    self.explodeExpression(path.get("argument"))
  ));

  break;

default:
  throw new Error(
    "unknown Statement of type " +
      JSON.stringify(stmt.type));
}

};

let catchParamVisitor = {

Identifier: function(path, state) {
  if (path.node.name === state.catchParamName && util.isReference(path)) {
    util.replaceWithOrRemove(path, state.getSafeParam());
  }
},

Scope: function(path, state) {
  if (path.scope.hasOwnBinding(state.catchParamName)) {
    // Don't descend into nested scopes that shadow the catch
    // parameter with their own declarations.
    path.skip();
  }
}

};

Ep.emitAbruptCompletion = function(record) {

if (!isValidCompletion(record)) {
  assert.ok(
    false,
    "invalid completion record: " +
      JSON.stringify(record)
  );
}

assert.notStrictEqual(
  record.type, "normal",
  "normal completions are not abrupt"
);

const t = util.getTypes();
let abruptArgs = [t.stringLiteral(record.type)];

if (record.type === "break" ||
    record.type === "continue") {
  t.assertLiteral(record.target);
  abruptArgs[1] = this.insertedLocs.has(record.target)
    ? record.target
    : t.cloneDeep(record.target);
} else if (record.type === "return" ||
           record.type === "throw") {
  if (record.value) {
    t.assertExpression(record.value);
    abruptArgs[1] = this.insertedLocs.has(record.value)
      ? record.value
      : t.cloneDeep(record.value);
  }
}

this.emit(
  t.returnStatement(
    t.callExpression(
      this.contextProperty("abrupt"),
      abruptArgs
    )
  )
);

};

function isValidCompletion(record) {

let type = record.type;

if (type === "normal") {
  return !hasOwn.call(record, "target");
}

if (type === "break" ||
    type === "continue") {
  return !hasOwn.call(record, "value")
      && util.getTypes().isLiteral(record.target);
}

if (type === "return" ||
    type === "throw") {
  return hasOwn.call(record, "value")
      && !hasOwn.call(record, "target");
}

return false;

}

// Not all offsets into emitter.listing are potential jump targets. For // example, execution typically falls into the beginning of a try block // without jumping directly there. This method returns the current offset // without marking it, so that a switch case will not necessarily be // generated for this offset (I say “not necessarily” because the same // location might end up being marked in the process of emitting other // statements). There's no logical harm in marking such locations as jump // targets, but minimizing the number of switch cases keeps the generated // code shorter. Ep.getUnmarkedCurrentLoc = function() {

return util.getTypes().numericLiteral(this.listing.length);

};

// The context.prev property takes the value of context.next whenever we // evaluate the switch statement discriminant, which is generally good // enough for tracking the last location we jumped to, but sometimes // context.prev needs to be more precise, such as when we fall // successfully out of a try block and into a finally block without // jumping. This method exists to update context.prev to the freshest // available location. If we were implementing a full interpreter, we // would know the location of the current instruction with complete // precision at all times, but we don't have that luxury here, as it would // be costly and verbose to set context.prev before every statement. Ep.updateContextPrevLoc = function(loc) {

const t = util.getTypes();
if (loc) {
  t.assertLiteral(loc);

  if (loc.value === -1) {
    // If an uninitialized location literal was passed in, set its value
    // to the current this.listing.length.
    loc.value = this.listing.length;
  } else {
    // Otherwise assert that the location matches the current offset.
    assert.strictEqual(loc.value, this.listing.length);
  }

} else {
  loc = this.getUnmarkedCurrentLoc();
}

// Make sure context.prev is up to date in case we fell into this try
// statement without jumping to it. TODO Consider avoiding this
// assignment when we know control must have jumped here.
this.emitAssign(this.contextProperty("prev"), loc);

};

Ep.explodeExpression = function(path, ignoreResult) {

const t = util.getTypes();
let expr = path.node;
if (expr) {
  t.assertExpression(expr);
} else {
  return expr;
}

let self = this;
let result; // Used optionally by several cases below.
let after;

function finish(expr) {
  t.assertExpression(expr);
  if (ignoreResult) {
    self.emit(expr);
  } else {
    return expr;
  }
}

// If the expression does not contain a leap, then we either emit the
// expression as a standalone statement or return it whole.
if (!meta.containsLeap(expr)) {
  return finish(expr);
}

// If any child contains a leap (such as a yield or labeled continue or
// break statement), then any sibling subexpressions will almost
// certainly have to be exploded in order to maintain the order of their
// side effects relative to the leaping child(ren).
let hasLeapingChildren = meta.containsLeap.onlyChildren(expr);

// In order to save the rest of explodeExpression from a combinatorial
// trainwreck of special cases, explodeViaTempVar is responsible for
// deciding when a subexpression needs to be "exploded," which is my
// very technical term for emitting the subexpression as an assignment
// to a temporary variable and the substituting the temporary variable
// for the original subexpression. Think of exploded view diagrams, not
// Michael Bay movies. The point of exploding subexpressions is to
// control the precise order in which the generated code realizes the
// side effects of those subexpressions.
function explodeViaTempVar(tempVar, childPath, ignoreChildResult) {
  assert.ok(
    !ignoreChildResult || !tempVar,
    "Ignoring the result of a child expression but forcing it to " +
      "be assigned to a temporary variable?"
  );

  let result = self.explodeExpression(childPath, ignoreChildResult);

  if (ignoreChildResult) {
    // Side effects already emitted above.

  } else if (tempVar || (hasLeapingChildren &&
                         !t.isLiteral(result))) {
    // If tempVar was provided, then the result will always be assigned
    // to it, even if the result does not otherwise need to be assigned
    // to a temporary variable.  When no tempVar is provided, we have
    // the flexibility to decide whether a temporary variable is really
    // necessary.  Unfortunately, in general, a temporary variable is
    // required whenever any child contains a yield expression, since it
    // is difficult to prove (at all, let alone efficiently) whether
    // this result would evaluate to the same value before and after the
    // yield (see #206).  One narrow case where we can prove it doesn't
    // matter (and thus we do not need a temporary variable) is when the
    // result in question is a Literal value.
    result = self.emitAssign(
      tempVar || self.makeTempVar(),
      result
    );
  }
  return result;
}

// If ignoreResult is true, then we must take full responsibility for
// emitting the expression with all its side effects, and we should not
// return a result.

switch (expr.type) {
case "MemberExpression":
  return finish(t.memberExpression(
    self.explodeExpression(path.get("object")),
    expr.computed
      ? explodeViaTempVar(null, path.get("property"))
      : expr.property,
    expr.computed
  ));

case "CallExpression":
  let calleePath = path.get("callee");
  let argsPath = path.get("arguments");

  let newCallee;
  let newArgs = [];

  let hasLeapingArgs = false;
  argsPath.forEach(function(argPath) {
    hasLeapingArgs = hasLeapingArgs ||
      meta.containsLeap(argPath.node);
  });

  if (t.isMemberExpression(calleePath.node)) {
    if (hasLeapingArgs) {
      // If the arguments of the CallExpression contained any yield
      // expressions, then we need to be sure to evaluate the callee
      // before evaluating the arguments, but if the callee was a member
      // expression, then we must be careful that the object of the
      // member expression still gets bound to `this` for the call.

      let newObject = explodeViaTempVar(
        // Assign the exploded callee.object expression to a temporary
        // variable so that we can use it twice without reevaluating it.
        self.makeTempVar(),
        calleePath.get("object")
      );

      let newProperty = calleePath.node.computed
        ? explodeViaTempVar(null, calleePath.get("property"))
        : calleePath.node.property;

      newArgs.unshift(newObject);

      newCallee = t.memberExpression(
        t.memberExpression(
          t.cloneDeep(newObject),
          newProperty,
          calleePath.node.computed
        ),
        t.identifier("call"),
        false
      );

    } else {
      newCallee = self.explodeExpression(calleePath);
    }

  } else {
    newCallee = explodeViaTempVar(null, calleePath);

    if (t.isMemberExpression(newCallee)) {
      // If the callee was not previously a MemberExpression, then the
      // CallExpression was "unqualified," meaning its `this` object
      // should be the global object. If the exploded expression has
      // become a MemberExpression (e.g. a context property, probably a
      // temporary variable), then we need to force it to be unqualified
      // by using the (0, object.property)(...) trick; otherwise, it
      // will receive the object of the MemberExpression as its `this`
      // object.
      newCallee = t.sequenceExpression([
        t.numericLiteral(0),
        t.cloneDeep(newCallee)
      ]);
    }
  }

  argsPath.forEach(function(argPath) {
    newArgs.push(explodeViaTempVar(null, argPath));
  });

  return finish(t.callExpression(
    newCallee,
    newArgs.map(arg => t.cloneDeep(arg))
  ));

case "NewExpression":
  return finish(t.newExpression(
    explodeViaTempVar(null, path.get("callee")),
    path.get("arguments").map(function(argPath) {
      return explodeViaTempVar(null, argPath);
    })
  ));

case "ObjectExpression":
  return finish(t.objectExpression(
    path.get("properties").map(function(propPath) {
      if (propPath.isObjectProperty()) {
        return t.objectProperty(
          propPath.node.key,
          explodeViaTempVar(null, propPath.get("value")),
          propPath.node.computed
        );
      } else {
        return propPath.node;
      }
    })
  ));

case "ArrayExpression":
  return finish(t.arrayExpression(
    path.get("elements").map(function(elemPath) {
      return explodeViaTempVar(null, elemPath);
    })
  ));

case "SequenceExpression":
  let lastIndex = expr.expressions.length - 1;

  path.get("expressions").forEach(function(exprPath) {
    if (exprPath.key === lastIndex) {
      result = self.explodeExpression(exprPath, ignoreResult);
    } else {
      self.explodeExpression(exprPath, true);
    }
  });

  return result;

case "LogicalExpression":
  after = this.loc();

  if (!ignoreResult) {
    result = self.makeTempVar();
  }

  let left = explodeViaTempVar(result, path.get("left"));

  if (expr.operator === "&&") {
    self.jumpIfNot(left, after);
  } else {
    assert.strictEqual(expr.operator, "||");
    self.jumpIf(left, after);
  }

  explodeViaTempVar(result, path.get("right"), ignoreResult);

  self.mark(after);

  return result;

case "ConditionalExpression":
  let elseLoc = this.loc();
  after = this.loc();
  let test = self.explodeExpression(path.get("test"));

  self.jumpIfNot(test, elseLoc);

  if (!ignoreResult) {
    result = self.makeTempVar();
  }

  explodeViaTempVar(result, path.get("consequent"), ignoreResult);
  self.jump(after);

  self.mark(elseLoc);
  explodeViaTempVar(result, path.get("alternate"), ignoreResult);

  self.mark(after);

  return result;

case "UnaryExpression":
  return finish(t.unaryExpression(
    expr.operator,
    // Can't (and don't need to) break up the syntax of the argument.
    // Think about delete a[b].
    self.explodeExpression(path.get("argument")),
    !!expr.prefix
  ));

case "BinaryExpression":
  return finish(t.binaryExpression(
    expr.operator,
    explodeViaTempVar(null, path.get("left")),
    explodeViaTempVar(null, path.get("right"))
  ));

case "AssignmentExpression":
  if (expr.operator === "=") {
    // If this is a simple assignment, the left hand side does not need
    // to be read before the right hand side is evaluated, so we can
    // avoid the more complicated logic below.
    return finish(t.assignmentExpression(
      expr.operator,
      self.explodeExpression(path.get("left")),
      self.explodeExpression(path.get("right"))
    ));
  }

  const lhs = self.explodeExpression(path.get("left"));
  const temp = self.emitAssign(self.makeTempVar(), lhs);

  // For example,
  //
  //   x += yield y
  //
  // becomes
  //
  //   context.t0 = x
  //   x = context.t0 += yield y
  //
  // so that the left-hand side expression is read before the yield.
  // Fixes https://github.com/facebook/regenerator/issues/345.

  return finish(t.assignmentExpression(
    "=",
    t.cloneDeep(lhs),
    t.assignmentExpression(
      expr.operator,
      t.cloneDeep(temp),
      self.explodeExpression(path.get("right"))
    )
  ));

case "UpdateExpression":
  return finish(t.updateExpression(
    expr.operator,
    self.explodeExpression(path.get("argument")),
    expr.prefix
  ));

case "YieldExpression":
  after = this.loc();
  let arg = expr.argument && self.explodeExpression(path.get("argument"));

  if (arg && expr.delegate) {
    let result = self.makeTempVar();

    let ret = t.returnStatement(t.callExpression(
      self.contextProperty("delegateYield"),
      [
        arg,
        t.stringLiteral(result.property.name),
        after
      ]
    ));
    ret.loc = expr.loc;

    self.emit(ret);
    self.mark(after);

    return result;
  }

  self.emitAssign(self.contextProperty("next"), after);

  let ret = t.returnStatement(t.cloneDeep(arg) || null);
  // Preserve the `yield` location so that source mappings for the statements
  // link back to the yield properly.
  ret.loc = expr.loc;
  self.emit(ret);
  self.mark(after);

  return self.contextProperty("sent");

default:
  throw new Error(
    "unknown Expression of type " +
      JSON.stringify(expr.type));
}

};