"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.PreludeGenerator = exports.NameGenerator = exports.Generator = exports.GeneratorEntry = undefined;

var _index = require("../values/index.js");

var _errors = require("../errors.js");

var _index2 = require("../methods/index.js");

var _index3 = require("../domains/index.js");

var _babelTypes = require("babel-types");

var t = _interopRequireWildcard(_babelTypes);

var _invariant = require("../invariant.js");

var _invariant2 = _interopRequireDefault(_invariant);

var _completions = require("../completions.js");

var _internalizer = require("./internalizer.js");

var _singletons = require("../singletons.js");

var _realm = require("../realm.js");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

/**
 * Copyright (c) 2017-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

class GeneratorEntry {
  visit(callbacks, containingGenerator) {
    (0, _invariant2.default)(false, "GeneratorEntry is an abstract base class");
  }

  serialize(context) {
    (0, _invariant2.default)(false, "GeneratorEntry is an abstract base class");
  }
}

exports.GeneratorEntry = GeneratorEntry;


class TemporalBuildNodeEntry extends GeneratorEntry {
  constructor(args) {
    super();
    Object.assign(this, args);
  }
  // If we're just trying to add roots for the serializer to notice, we don't need a buildNode.


  visit(callbacks, containingGenerator) {
    if (this.isPure && this.declared && callbacks.canSkip(this.declared)) {
      callbacks.recordDelayedEntry(containingGenerator, this);
      return false;
    } else {
      if (this.declared) callbacks.recordDeclaration(this.declared);
      callbacks.visitValues(this.args);
      if (this.dependencies) for (let dependency of this.dependencies) callbacks.visitGenerator(dependency, containingGenerator);
      return true;
    }
  }

  serialize(context) {
    if (!this.isPure || !this.declared || !context.canOmit(this.declared)) {
      let nodes = this.args.map((boundArg, i) => context.serializeValue(boundArg));
      if (this.buildNode) {
        let valuesToProcess = new Set();
        let node = this.buildNode(nodes, context, valuesToProcess);
        if (node.type === "BlockStatement") {
          let block = node;
          let statements = block.body;
          if (statements.length === 0) return;
          if (statements.length === 1) {
            node = statements[0];
          }
        }
        let declared = this.declared;
        if (declared !== undefined && context.options.debugScopes) {
          let s = t.emptyStatement();
          s.leadingComments = [{ type: "BlockComment", value: `declaring ${declared.intrinsicName || "?"}` }];
          context.emit(s);
        }
        context.emit(node);
        context.processValues(valuesToProcess);
      }
      if (this.declared !== undefined) context.declare(this.declared);
    }
  }
}

class ModifiedPropertyEntry extends GeneratorEntry {
  constructor(args) {
    super();
    Object.assign(this, args);
  }

  serialize(context) {
    let desc = this.propertyBinding.descriptor;
    (0, _invariant2.default)(desc === this.newDescriptor);
    context.emitPropertyModification(this.propertyBinding);
  }

  visit(context, containingGenerator) {
    (0, _invariant2.default)(containingGenerator === this.containingGenerator, "This entry requires effects to be applied and may not be moved");
    let desc = this.propertyBinding.descriptor;
    (0, _invariant2.default)(desc === this.newDescriptor);
    context.visitObjectProperty(this.propertyBinding);
    return true;
  }
}

class ModifiedBindingEntry extends GeneratorEntry {
  constructor(args) {
    super();
    Object.assign(this, args);
  }

  serialize(context) {
    let residualFunctionBinding = this.residualFunctionBinding;
    if (!residualFunctionBinding) {
      (0, _invariant2.default)(this.modifiedBinding.value instanceof _index.FunctionValue);
      return;
    }
    (0, _invariant2.default)(residualFunctionBinding.referentialized);
    (0, _invariant2.default)(this.modifiedBinding.value === this.newValue);
    (0, _invariant2.default)(residualFunctionBinding.serializedValue, "ResidualFunctionBinding must be referentialized before serializing a mutation to it.");
    let newValue = this.newValue;
    if (newValue) {
      let bindingReference = residualFunctionBinding.serializedValue;
      (0, _invariant2.default)(t.isLVal(bindingReference), "Referentialized values must be LVals even though serializedValues may be any Expression");
      let serializedNewValue = context.serializeValue(newValue);
      context.emit(t.expressionStatement(t.assignmentExpression("=", bindingReference, serializedNewValue)));
    }
  }

  visit(context, containingGenerator) {
    (0, _invariant2.default)(containingGenerator === this.containingGenerator, "This entry requires effects to be applied and may not be moved");
    (0, _invariant2.default)(this.modifiedBinding.value === this.newValue);
    let residualBinding = context.visitModifiedBinding(this.modifiedBinding, this.oldValue);
    (0, _invariant2.default)(this.residualFunctionBinding === undefined || this.residualFunctionBinding === residualBinding);
    this.residualFunctionBinding = residualBinding;
    return true;
  }
}

class ReturnValueEntry extends GeneratorEntry {
  constructor(generator, returnValue) {
    super();
    this.returnValue = returnValue;
    this.containingGenerator = generator;
  }

  visit(context, containingGenerator) {
    (0, _invariant2.default)(containingGenerator === this.containingGenerator, "This entry requires effects to be applied and may not be moved");
    context.visitValues([this.returnValue]);
    return true;
  }

  serialize(context) {
    let result = context.serializeValue(this.returnValue);
    context.emit(t.returnStatement(result));
  }
}

class PossiblyNormalReturnEntry extends GeneratorEntry {
  constructor(generator, completion, realm) {
    super();
    this.completion = completion;
    this.containingGenerator = generator;
    this.condition = completion.joinCondition;

    // The effects of the normal path have already been applied to generator
    let empty_effects = (0, _realm.construct_empty_effects)(realm);
    empty_effects[0] = completion.value;
    let consequentEffects = completion.consequent instanceof _completions.AbruptCompletion ? completion.consequentEffects : empty_effects;
    this.consequentGenerator = Generator.fromEffects(consequentEffects, realm, "ConsequentEffects");
    let alternateEffects = completion.alternate instanceof _completions.AbruptCompletion ? completion.alternateEffects : empty_effects;
    this.alternateGenerator = Generator.fromEffects(alternateEffects, realm, "AlternateEffects");
  }

  visit(context, containingGenerator) {
    (0, _invariant2.default)(containingGenerator === this.containingGenerator, "This entry requires effects to be applied and may not be moved");
    context.visitValues([this.condition]);
    context.visitGenerator(this.consequentGenerator, containingGenerator);
    context.visitGenerator(this.alternateGenerator, containingGenerator);
    return true;
  }

  serialize(context) {
    let condition = context.serializeValue(this.condition);
    let valuesToProcess = new Set();
    let consequentBody = context.serializeGenerator(this.consequentGenerator, valuesToProcess);
    let alternateBody = context.serializeGenerator(this.alternateGenerator, valuesToProcess);
    context.emit(t.ifStatement(condition, t.blockStatement(consequentBody), t.blockStatement(alternateBody)));
    context.processValues(valuesToProcess);
  }
}

function serializeBody(generator, context, valuesToProcess) {
  let statements = context.serializeGenerator(generator, valuesToProcess);
  if (statements.length === 1 && statements[0].type === "BlockStatement") return statements[0];
  return t.blockStatement(statements);
}

class Generator {
  constructor(realm, name, effects) {
    (0, _invariant2.default)(realm.useAbstractInterpretation);
    let realmPreludeGenerator = realm.preludeGenerator;
    (0, _invariant2.default)(realmPreludeGenerator);
    this.preludeGenerator = realmPreludeGenerator;
    this.realm = realm;
    this._entries = [];
    this.id = realm.nextGeneratorId++;
    this._name = name;
    this.effectsToApply = effects;
  }

  static _generatorOfEffects(realm, name, environmentRecordIdAfterGlobalCode, effects) {
    let [result, generator, modifiedBindings, modifiedProperties, createdObjects] = effects;

    let output = new Generator(realm, name, effects);
    output.appendGenerator(generator, "");

    for (let [propertyBinding] of modifiedProperties) {
      let object = propertyBinding.object;
      if (object instanceof _index.ObjectValue && createdObjects.has(object)) continue; // Created Object's binding
      if (object.refuseSerialization) continue; // modification to internal state
      // modifications to intrinsic objects are tracked in the generator
      if (object.isIntrinsic()) continue;
      let newValue = propertyBinding.descriptor;
      output.emitPropertyModification(propertyBinding, newValue);
    }

    for (let [modifiedBinding, oldBinding] of modifiedBindings) {
      // TODO: Instead of looking at the environment ids, keep instead track of a createdEnvironmentRecords set,
      // and only consider bindings here from environment records that already existed, or even better,
      // ensure upstream that only such bindings are ever added to the modified-bindings set.
      if (modifiedBinding.environment.id >= environmentRecordIdAfterGlobalCode) continue;

      output.emitBindingModification(modifiedBinding, oldBinding.value);
    }

    if (result instanceof _index.UndefinedValue) return output;
    if (result instanceof _index.Value) {
      output.emitReturnValue(result);
    } else if (result instanceof _completions.PossiblyNormalCompletion) {
      output.emitPossiblyNormalReturn(result, realm);
    } else if (result instanceof _completions.ThrowCompletion) {
      output.emitThrow(result.value);
    } else {
      /*else if (result instanceof JoinedAbruptCompletions) {
      } */(0, _invariant2.default)(false);
    }
    return output;
  }

  // Make sure to to fixup
  // how to apply things around sets of things
  static fromEffects(effects, realm, name, environmentRecordIdAfterGlobalCode = 0) {
    return realm.withEffectsAppliedInGlobalEnv(this._generatorOfEffects.bind(this, realm, name, environmentRecordIdAfterGlobalCode), effects);
  }

  emitPropertyModification(propertyBinding) {
    (0, _invariant2.default)(this.effectsToApply !== undefined);
    this._entries.push(new ModifiedPropertyEntry({
      propertyBinding,
      newDescriptor: propertyBinding.descriptor,
      containingGenerator: this
    }));
  }

  emitBindingModification(modifiedBinding, oldValue) {
    (0, _invariant2.default)(this.effectsToApply !== undefined);
    this._entries.push(new ModifiedBindingEntry({
      modifiedBinding,
      newValue: modifiedBinding.value,
      oldValue,
      containingGenerator: this
    }));
  }

  emitReturnValue(result) {
    this._entries.push(new ReturnValueEntry(this, result));
  }

  emitPossiblyNormalReturn(result, realm) {
    this._entries.push(new PossiblyNormalReturnEntry(this, result, realm));
  }

  getName() {
    return `${this._name}(#${this.id})`;
  }

  getAsPropertyNameExpression(key, canBeIdentifier = true) {
    // If key is a non-negative numeric string literal, parse it and set it as a numeric index instead.
    let index = Number.parseInt(key, 10);
    if (index >= 0 && index.toString() === key) {
      return t.numericLiteral(index);
    }

    if (canBeIdentifier) {
      // TODO #1020: revert this when Unicode identifiers are supported by all targetted JavaScript engines
      let keyIsAscii = /^[\u0000-\u007f]*$/.test(key);
      if (t.isValidIdentifier(key) && keyIsAscii) return t.identifier(key);
    }

    return t.stringLiteral(key);
  }

  empty() {
    return this._entries.length === 0;
  }

  emitGlobalDeclaration(key, value) {
    this.preludeGenerator.declaredGlobals.add(key);
    if (!(value instanceof _index.UndefinedValue)) this.emitGlobalAssignment(key, value, true);
  }

  emitGlobalAssignment(key, value, strictMode) {
    this._addEntry({
      args: [value],
      buildNode: ([valueNode]) => t.expressionStatement(t.assignmentExpression("=", this.preludeGenerator.globalReference(key, !strictMode), valueNode))
    });
  }

  emitConcreteModel(key, value) {
    this._addEntry({
      args: [(0, _singletons.concretize)(this.realm, value)],
      buildNode: ([valueNode]) => t.expressionStatement(t.assignmentExpression("=", this.preludeGenerator.globalReference(key, false), valueNode))
    });
  }

  emitGlobalDelete(key, strictMode) {
    this._addEntry({
      args: [],
      buildNode: ([]) => t.expressionStatement(t.unaryExpression("delete", this.preludeGenerator.globalReference(key, !strictMode)))
    });
  }

  emitBindingAssignment(binding, value) {
    this._addEntry({
      args: [value],
      buildNode: ([valueNode], context) => t.expressionStatement(t.assignmentExpression("=", context.serializeBinding(binding), valueNode))
    });
  }

  emitPropertyAssignment(object, key, value) {
    if (object.refuseSerialization) return;
    let propName = this.getAsPropertyNameExpression(key);
    this._addEntry({
      args: [object, value],
      buildNode: ([objectNode, valueNode]) => t.expressionStatement(t.assignmentExpression("=", t.memberExpression(objectNode, propName, !t.isIdentifier(propName)), valueNode))
    });
  }

  emitDefineProperty(object, key, desc, isDescChanged = true) {
    if (object.refuseSerialization) return;
    if (desc.enumerable && desc.configurable && desc.writable && desc.value && !isDescChanged) {
      let descValue = desc.value;
      (0, _invariant2.default)(descValue instanceof _index.Value);
      this.emitPropertyAssignment(object, key, descValue);
    } else {
      desc = Object.assign({}, desc);
      let descValue = desc.value || object.$Realm.intrinsics.undefined;
      (0, _invariant2.default)(descValue instanceof _index.Value);
      this._addEntry({
        args: [object, descValue, desc.get || object.$Realm.intrinsics.undefined, desc.set || object.$Realm.intrinsics.undefined],
        buildNode: (_, context) => context.emitDefinePropertyBody(object, key, desc)
      });
    }
  }

  emitPropertyDelete(object, key) {
    if (object.refuseSerialization) return;
    let propName = this.getAsPropertyNameExpression(key);
    this._addEntry({
      args: [object],
      buildNode: ([objectNode]) => t.expressionStatement(t.unaryExpression("delete", t.memberExpression(objectNode, propName, !t.isIdentifier(propName))))
    });
  }

  emitCall(createCallee, args) {
    this._addEntry({
      args,
      buildNode: values => t.expressionStatement(t.callExpression(createCallee(), [...values]))
    });
  }

  emitConsoleLog(method, args) {
    this.emitCall(() => t.memberExpression(t.identifier("console"), t.identifier(method)), args.map(v => typeof v === "string" ? new _index.StringValue(this.realm, v) : v));
  }

  // test must be a temporal value, which means that it must have a defined intrinsicName
  emitDoWhileStatement(test, body) {
    this._addEntry({
      args: [],
      buildNode: function ([], context, valuesToProcess) {
        let testId = test.intrinsicName;
        (0, _invariant2.default)(testId !== undefined);
        let statements = context.serializeGenerator(body, valuesToProcess);
        let block = t.blockStatement(statements);
        return t.doWhileStatement(t.identifier(testId), block);
      },
      dependencies: [body]
    });
  }

  emitConditionalThrow(condition, trueBranch, falseBranch) {
    let [args, buildfunc] = this._deconstruct(condition, trueBranch, falseBranch, completion => {
      this._issueThrowCompilerDiagnostic(completion.value);
      let serializationArgs = [completion.value];
      let func = ([arg]) => t.throwStatement(arg);
      return [serializationArgs, func];
    }, () => [[], () => t.emptyStatement()]);
    this.emitStatement(args, buildfunc);
  }

  getThrowOrReturn(condition, trueBranch, falseBranch) {
    let [args, buildfunc] = this._deconstruct(condition, trueBranch, falseBranch, completion => {
      return [[completion.value], ([arg]) => t.throwStatement(arg)];
    }, value => [[value], ([returnValue]) => t.returnStatement(returnValue)]);
    return [args, buildfunc];
  }

  _deconstruct(condition, trueBranch, falseBranch, onThrowCompletion, onNormalValue) {
    let targs;
    let tfunc;
    let fargs;
    let ffunc;
    if (trueBranch instanceof _completions.JoinedAbruptCompletions) {
      [targs, tfunc] = this._deconstruct(trueBranch.joinCondition, trueBranch.consequent, trueBranch.alternate, onThrowCompletion, onNormalValue);
    } else if (trueBranch instanceof _completions.ThrowCompletion) {
      [targs, tfunc] = onThrowCompletion(trueBranch);
    } else {
      let value = trueBranch instanceof _completions.ReturnCompletion ? trueBranch.value : trueBranch;
      (0, _invariant2.default)(value instanceof _index.Value);
      [targs, tfunc] = onNormalValue(value);
    }
    if (falseBranch instanceof _completions.JoinedAbruptCompletions) {
      [fargs, ffunc] = this._deconstruct(falseBranch.joinCondition, falseBranch.consequent, falseBranch.alternate, onThrowCompletion, onNormalValue);
    } else if (falseBranch instanceof _completions.ThrowCompletion) {
      [fargs, ffunc] = onThrowCompletion(falseBranch);
    } else {
      (0, _invariant2.default)(falseBranch instanceof _index.Value);
      [fargs, ffunc] = onNormalValue(falseBranch);
    }
    let args = [condition].concat(targs).concat(fargs);
    let func = nodes => {
      return t.ifStatement(nodes[0], tfunc(nodes.slice().splice(1, targs.length)), ffunc(nodes.slice().splice(targs.length + 1, fargs.length)));
    };
    return [args, func];
  }

  _issueThrowCompilerDiagnostic(value) {
    let message = "Program may terminate with exception";
    if (value instanceof _index.ObjectValue) {
      let object = value;
      let objectMessage = this.realm.evaluateWithUndo(() => object.$Get("message", value));
      if (objectMessage instanceof _index.StringValue) message += `: ${objectMessage.value}`;
      const objectStack = this.realm.evaluateWithUndo(() => object.$Get("stack", value));
      if (objectStack instanceof _index.StringValue) message += `
  ${objectStack.value}`;
    }
    const diagnostic = new _errors.CompilerDiagnostic(message, value.expressionLocation, "PP1023", "Warning");
    this.realm.handleError(diagnostic);
  }

  emitThrow(value) {
    this._issueThrowCompilerDiagnostic(value);
    this.emitStatement([value], ([argument]) => t.throwStatement(argument));
  }

  // Checks the full set of possible concrete values as well as typeof
  // for any AbstractValues
  // e.g: (obj.property !== undefined && typeof obj.property !== "object")
  // NB: if the type of the AbstractValue is top, skips the invariant
  emitFullInvariant(object, key, value) {
    let propertyIdentifier = this.getAsPropertyNameExpression(key);
    let computed = !t.isIdentifier(propertyIdentifier);
    let accessedPropertyOf = objectNode => t.memberExpression(objectNode, propertyIdentifier, computed);
    let condition;
    if (value instanceof _index.AbstractValue) {
      let isTop = false;
      let concreteComparisons = [];
      let typeComparisons = new Set();

      function populateComparisonsLists(absValue) {
        if (absValue.kind === "abstractConcreteUnion") {
          // recurse
          for (let nestedValue of absValue.args) if (nestedValue instanceof _index.ConcreteValue) {
            concreteComparisons.push(nestedValue);
          } else {
            (0, _invariant2.default)(nestedValue instanceof _index.AbstractValue);
            populateComparisonsLists(nestedValue);
          }
        } else if (absValue.getType().isTop || absValue.getType() === _index.Value) {
          isTop = true;
        } else {
          typeComparisons.add(absValue.getType());
        }
      }
      populateComparisonsLists(value);

      // No point in doing the invariant if we don't know the type
      // of one of the nested abstract values
      if (isTop) {
        return;
      } else {
        condition = ([valueNode]) => {
          // Create `object.property !== concreteValue`
          let checks = concreteComparisons.map(concreteValue => t.binaryExpression("!==", valueNode, t.valueToNode(concreteValue.serialize())));
          // Create `typeof object.property !== typeValue`
          checks = checks.concat([...typeComparisons].map(typeValue => {
            let typeString = _singletons.Utils.typeToString(typeValue);
            (0, _invariant2.default)(typeString !== undefined, typeValue);
            return t.binaryExpression("!==", t.unaryExpression("typeof", valueNode, true), t.stringLiteral(typeString));
          }));
          return checks.reduce((expr, newCondition) => t.logicalExpression("&&", expr, newCondition));
        };
        this.emitInvariant([value, value], condition, valueNode => valueNode);
      }
    } else {
      condition = ([objectNode, valueNode]) => t.binaryExpression("!==", accessedPropertyOf(objectNode), valueNode);
      this.emitInvariant([object, value, object], condition, objnode => accessedPropertyOf(objnode));
    }
  }

  emitInvariant(args, violationConditionFn, appendLastToInvariantFn) {
    if (this.realm.omitInvariants) return;
    this._addEntry({
      args,
      buildNode: nodes => {
        let throwString = t.stringLiteral("Prepack model invariant violation");
        if (appendLastToInvariantFn) {
          let last = nodes.pop();
          throwString = t.binaryExpression("+", t.stringLiteral("Prepack model invariant violation: "), appendLastToInvariantFn(last));
        }
        let condition = violationConditionFn(nodes);
        let throwblock = t.blockStatement([t.throwStatement(t.newExpression(t.identifier("Error"), [throwString]))]);
        return t.ifStatement(condition, throwblock);
      }
    });
  }

  emitCallAndCaptureResult(types, values, createCallee, args, kind) {
    return this.derive(types, values, args, nodes => t.callExpression(createCallee(), nodes), { kind });
  }

  emitStatement(args, buildNode_) {
    this._addEntry({
      args,
      buildNode: buildNode_
    });
  }

  emitVoidExpression(types, values, args, buildNode_) {
    this._addEntry({
      args,
      buildNode: nodes => t.expressionStatement(buildNode_ instanceof Function ? buildNode_(nodes) : buildNode_)
    });
    return this.realm.intrinsics.undefined;
  }

  emitForInStatement(o, lh, sourceObject, targetObject, boundName) {
    this._addEntry({
      // duplicate args to ensure refcount > 1
      args: [o, targetObject, sourceObject, targetObject, sourceObject],
      buildNode: ([obj, tgt, src, obj1, tgt1, src1]) => {
        return t.forInStatement(lh, obj, t.blockStatement([t.expressionStatement(t.assignmentExpression("=", t.memberExpression(tgt, boundName, true), t.memberExpression(src, boundName, true)))]));
      }
    });
  }

  derive(types, values, args, buildNode_, optionalArgs) {
    (0, _invariant2.default)(buildNode_ instanceof Function || args.length === 0);
    let id = t.identifier(this.preludeGenerator.nameGenerator.generate("derived"));
    this.preludeGenerator.derivedIds.set(id.name, args);
    let options = {};
    if (optionalArgs && optionalArgs.kind) options.kind = optionalArgs.kind;
    let Constructor = _index.Value.isTypeCompatibleWith(types.getType(), _index.ObjectValue) ? _index.AbstractObjectValue : _index.AbstractValue;
    let res = new Constructor(this.realm, types, values, (0, _index2.hashString)(id.name), [], id, options);
    this._addEntry({
      isPure: optionalArgs ? optionalArgs.isPure : undefined,
      declared: res,
      args,
      buildNode: (nodes, context, valuesToProcess) => {
        return t.variableDeclaration("var", [t.variableDeclarator(id, buildNode_ instanceof Function ? buildNode_(nodes, context, valuesToProcess) : buildNode_)]);
      }
    });
    let type = types.getType();
    res.intrinsicName = id.name;
    if (optionalArgs && optionalArgs.skipInvariant) return res;
    let typeofString;
    if (type instanceof _index.FunctionValue) typeofString = "function";else if (type === _index.UndefinedValue) (0, _invariant2.default)(false);else if (type === _index.NullValue) (0, _invariant2.default)(false);else if (type === _index.StringValue) typeofString = "string";else if (type === _index.BooleanValue) typeofString = "boolean";else if (type === _index.NumberValue) typeofString = "number";else if (type === _index.IntegralValue) typeofString = "number";else if (type === _index.SymbolValue) typeofString = "symbol";else if (type === _index.ObjectValue) typeofString = "object";
    if (typeofString !== undefined) {
      // Verify that the types are as expected, a failure of this invariant
      // should mean the model is wrong.
      this.emitInvariant([res, res], nodes => {
        (0, _invariant2.default)(typeofString !== undefined);
        let condition = t.binaryExpression("!==", t.unaryExpression("typeof", nodes[0]), t.stringLiteral(typeofString));
        if (typeofString === "object") {
          condition = t.logicalExpression("&&", condition, t.binaryExpression("!==", t.unaryExpression("typeof", nodes[0]), t.stringLiteral("function")));
          condition = t.logicalExpression("||", condition, t.binaryExpression("===", nodes[0], _internalizer.nullExpression));
        }
        return condition;
      }, node => node);
    }

    return res;
  }

  // This function does a deep traversal of this and all dependent generators
  // in order to find all ModifiedBindingEntry entries.
  // Their modifiedBinding and oldValue properties are eventually returned.
  // Note that the modifiedBindings set is different from effectsToApply[2],
  // and the old values are meaningful without effectsToApply being applied.
  getModifiedBindingOldValues() {
    let result = new Map();
    let visit = generator => {
      for (let entry of generator._entries) {
        if (entry instanceof ModifiedBindingEntry) {
          if (!result.has(entry.modifiedBinding)) result.set(entry.modifiedBinding, entry.oldValue);
        } else if (entry instanceof TemporalBuildNodeEntry) {
          if (entry.dependencies) for (let dependency of entry.dependencies) visit(dependency);
        } else if (entry instanceof PossiblyNormalReturnEntry) {
          for (let dependency of [entry.consequentGenerator, entry.alternateGenerator]) visit(dependency);
        } else {
          (0, _invariant2.default)(entry instanceof ReturnValueEntry || entry instanceof ModifiedPropertyEntry, entry.constructor.name);
        }
      }
    };
    visit(this);
    return result;
  }

  visit(callbacks) {
    let visitFn = () => {
      for (let entry of this._entries) entry.visit(callbacks, this);
      return null;
    };
    if (this.effectsToApply) {
      this.realm.withEffectsAppliedInGlobalEnv(visitFn, this.effectsToApply);
    } else {
      visitFn();
    }
  }

  serialize(context) {
    let serializeFn = () => {
      for (let entry of this._entries) entry.serialize(context);
      return null;
    };
    if (this.effectsToApply) {
      this.realm.withEffectsAppliedInGlobalEnv(serializeFn, this.effectsToApply);
    } else {
      serializeFn();
    }
  }

  _addEntry(entry) {
    this._entries.push(new TemporalBuildNodeEntry(entry));
  }

  appendGenerator(other, leadingComment) {
    (0, _invariant2.default)(other !== this);
    (0, _invariant2.default)(other.realm === this.realm);
    (0, _invariant2.default)(other.preludeGenerator === this.preludeGenerator);

    if (other.empty()) return;
    if (other.effectsToApply === undefined) {
      this._entries.push(...other._entries);
    } else {
      this._addEntry({
        args: [],
        buildNode: function (args, context, valuesToProcess) {
          let statements = context.serializeGenerator(other, valuesToProcess);
          if (statements.length === 1) {
            let statement = statements[0];
            if (leadingComment.length > 0) statement.leadingComments = [{ type: "BlockComment", value: leadingComment }];
            return statement;
          }
          let block = t.blockStatement(statements);
          if (leadingComment.length > 0) block.leadingComments = [{ type: "BlockComment", value: leadingComment }];
          return block;
        },
        dependencies: [other]
      });
    }
  }

  composeGenerators(generator1, generator2) {
    (0, _invariant2.default)(generator1 !== this && generator2 !== this && generator1 !== generator2);
    this._addEntry({
      args: [],
      buildNode: function ([], context, valuesToProcess) {
        let statements1 = generator1.empty() ? [] : context.serializeGenerator(generator1, valuesToProcess);
        let statements2 = generator2.empty() ? [] : context.serializeGenerator(generator2, valuesToProcess);
        let statements = statements1.concat(statements2);
        if (statements.length === 1) return statements[0];
        return t.blockStatement(statements);
      },
      dependencies: [generator1, generator2]
    });
  }

  joinGenerators(joinCondition, generator1, generator2) {
    (0, _invariant2.default)(generator1 !== this && generator2 !== this && generator1 !== generator2);
    this._addEntry({
      args: [joinCondition],
      buildNode: function ([cond], context, valuesToProcess) {
        let block1 = generator1.empty() ? null : serializeBody(generator1, context, valuesToProcess);
        let block2 = generator2.empty() ? null : serializeBody(generator2, context, valuesToProcess);
        if (block1) return t.ifStatement(cond, block1, block2);
        (0, _invariant2.default)(block2);
        return t.ifStatement(t.unaryExpression("!", cond), block2);
      },
      dependencies: [generator1, generator2]
    });
  }
}

exports.Generator = Generator;
function escapeInvalidIdentifierCharacters(s) {
  let res = "";
  for (let c of s) if (c >= "0" && c <= "9" || c >= "a" && c <= "z" || c >= "A" && c <= "Z") res += c;else res += "_" + c.charCodeAt(0);
  return res;
}

const base62characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
function base62encode(n) {
  (0, _invariant2.default)((n | 0) === n && n >= 0);
  if (n === 0) return "0";
  let s = "";
  while (n > 0) {
    let f = n % base62characters.length;
    s = base62characters[f] + s;
    n = (n - f) / base62characters.length;
  }
  return s;
}

class NameGenerator {
  constructor(forbiddenNames, debugNames, uniqueSuffix, prefix) {
    this.prefix = prefix;
    this.uidCounter = 0;
    this.debugNames = debugNames;
    this.forbiddenNames = forbiddenNames;
    this.uniqueSuffix = uniqueSuffix;
  }

  generate(debugSuffix) {
    let id;
    do {
      id = this.prefix + base62encode(this.uidCounter++);
      if (this.uniqueSuffix.length > 0) id += this.uniqueSuffix;
      if (this.debugNames) {
        if (debugSuffix) id += "_" + escapeInvalidIdentifierCharacters(debugSuffix);else id += "_";
      }
    } while (this.forbiddenNames.has(id));
    return id;
  }
}

exports.NameGenerator = NameGenerator;
class PreludeGenerator {
  constructor(debugNames, uniqueSuffix) {
    this.prelude = [];
    this.derivedIds = new Map();
    this.memoizedRefs = new Map();
    this.nameGenerator = new NameGenerator(new Set(), !!debugNames, uniqueSuffix || "", "_$");
    this.usesThis = false;
    this.declaredGlobals = new Set();
  }

  createNameGenerator(prefix) {
    return new NameGenerator(this.nameGenerator.forbiddenNames, this.nameGenerator.debugNames, this.nameGenerator.uniqueSuffix, prefix);
  }

  convertStringToMember(str) {
    return str.split(".").map(name => {
      if (name === "global") {
        return this.memoizeReference(name);
      } else if (name === "this") {
        return t.thisExpression();
      } else {
        return t.identifier(name);
      }
    }).reduce((obj, prop) => t.memberExpression(obj, prop));
  }

  globalReference(key, globalScope = false) {
    if (globalScope && t.isValidIdentifier(key)) return t.identifier(key);
    let keyNode = t.isValidIdentifier(key) ? t.identifier(key) : t.stringLiteral(key);
    return t.memberExpression(this.memoizeReference("global"), keyNode, !t.isIdentifier(keyNode));
  }

  memoizeReference(key) {
    let ref = this.memoizedRefs.get(key);
    if (ref) return ref;

    let init;
    if (key.includes("(") || key.includes("[")) {
      // Horrible but effective hack:
      // Some internal object have intrinsic names such as
      //    ([][Symbol.iterator]().__proto__.__proto__)
      // and
      //    RegExp.prototype[Symbol.match]
      // which get turned into a babel node here.
      // TODO: We should properly parse such a string, and memoize all references in it separately.
      // Instead, we just turn it into a funky identifier, which Babel seems to accept.
      init = t.identifier(key);
    } else if (key === "global") {
      this.usesThis = true;
      init = t.thisExpression();
    } else {
      let i = key.lastIndexOf(".");
      if (i === -1) {
        init = t.memberExpression(this.memoizeReference("global"), t.identifier(key));
      } else {
        init = t.memberExpression(this.memoizeReference(key.substr(0, i)), t.identifier(key.substr(i + 1)));
      }
    }
    ref = t.identifier(this.nameGenerator.generate(key));
    this.prelude.push(t.variableDeclaration("var", [t.variableDeclarator(ref, init)]));
    this.memoizedRefs.set(key, ref);
    return ref;
  }
}
exports.PreludeGenerator = PreludeGenerator;
//# sourceMappingURL=generator.js.map