"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.Modules = exports.ModuleTracer = undefined;

var _environment = require("../environment.js");

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

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

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

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

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

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

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

var t = _interopRequireWildcard(_babelTypes);

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

var _invariant2 = _interopRequireDefault(_invariant);

var _logger = require("./logger.js");

var _types = require("../serializer/types.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; } }

function downgradeErrorsToWarnings(realm, f) {
  let savedHandler = realm.errorHandler;
  function handler(e) {
    e.severity = "Warning";
    realm.errorHandler = savedHandler;
    try {
      return realm.handleError(e);
    } finally {
      realm.errorHandler = handler;
    }
  }
  realm.errorHandler = handler;
  try {
    return f();
  } finally {
    realm.errorHandler = savedHandler;
  }
} /**
   * 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 ModuleTracer extends _realm.Tracer {
  constructor(modules, statistics, logModules) {
    super();
    this.modules = modules;
    this.evaluateForEffectsNesting = 0;
    this.requireStack = [];
    this.requireSequence = [];
    this.logModules = logModules;
    this.uninitializedModuleIdsRequiredInEvaluateForEffects = new Set();
    this.statistics = statistics;
  }
  // We can't say that a module has been initialized if it was initialized in a
  // evaluate for effects context until we know the effects are applied.


  log(message) {
    if (this.logModules) console.log(`[modules] ${this.requireStack.map(_ => "  ").join("")}${message}`);
  }

  beginEvaluateForEffects(state) {
    if (state !== this) {
      this.log(">evaluate for effects");
      this.evaluateForEffectsNesting++;
      this.requireStack.push(undefined);
    }
  }

  endEvaluateForEffects(state, effects) {
    if (state !== this) {
      let popped = this.requireStack.pop();
      (0, _invariant2.default)(popped === undefined);
      this.evaluateForEffectsNesting--;
      this.log("<evaluate for effects");
    }
  }

  // If we don't delay unsupported requires, we simply want to record here
  // when a module gets initialized, and then we return.
  _callRequireAndRecord(moduleIdValue, performCall) {
    let realm = this.modules.realm;
    if ((this.requireStack.length === 0 || this.requireStack[this.requireStack.length - 1] !== moduleIdValue) && this.modules.moduleIds.has(moduleIdValue)) {
      this.requireStack.push(moduleIdValue);
      try {
        let value = performCall();
        // Make this into a join point by suppressing the conditional exception.
        // TODO: delete this code and let the caller deal with the conditional exception.
        let completion = _singletons.Functions.incorporateSavedCompletion(realm, value);
        if (completion instanceof _completions.PossiblyNormalCompletion) {
          realm.stopEffectCapture(completion);
          let warning = new _errors.CompilerDiagnostic("Module import may fail with an exception", completion.location, "PP0018", "Warning");
          realm.handleError(warning);
        } else {
          this.modules.recordModuleInitialized(moduleIdValue, value);
        }
        return value;
      } finally {
        (0, _invariant2.default)(this.requireStack.pop() === moduleIdValue);
      }
    }
    return undefined;
  }

  _callRequireAndAccelerate(isTopLevelRequire, moduleIdValue, performCall) {
    let realm = this.modules.realm;
    let acceleratedModuleIds, effects;
    do {
      try {
        effects = realm.evaluateForEffects(() => performCall(), this, "_callRequireAndAccelerate");
      } catch (e) {
        e;
      }

      acceleratedModuleIds = [];
      if (isTopLevelRequire && effects !== undefined && !(effects[0] instanceof _completions.AbruptCompletion)) {
        // We gathered all effects, but didn't apply them yet.
        // Let's check if there was any call to `require` in a
        // evaluate-for-effects context. If so, try to initialize
        // that module right now. Acceleration module initialization in this
        // way might not actually be desirable, but it works around
        // general prepack-limitations around joined abstract values involving
        // conditionals. Long term, Prepack needs to implement a notion of refinement
        // of conditional abstract values under the known path condition.
        // Example:
        //   if (*) require(1); else require(2);
        //   let x = require(1).X;
        // =>
        //   require(1);
        //   require(2);
        //   if (*) require(1); else require(2);
        //   let x = require(1).X;

        for (let nestedModuleId of this.uninitializedModuleIdsRequiredInEvaluateForEffects) {
          let nestedEffects = this.modules.tryInitializeModule(nestedModuleId, `accelerated initialization of conditional module ${nestedModuleId} as it's required in an evaluate-for-effects context by module ${moduleIdValue}`);
          if (this.modules.accelerateUnsupportedRequires && nestedEffects !== undefined && nestedEffects[0] instanceof _index2.Value && this.modules.isModuleInitialized(nestedModuleId)) {
            acceleratedModuleIds.push(nestedModuleId);
          }
        }
        this.uninitializedModuleIdsRequiredInEvaluateForEffects.clear();
        // Keep restarting for as long as we find additional modules to accelerate.
        if (acceleratedModuleIds.length > 0) {
          console.log(`restarting require(${moduleIdValue}) after accelerating conditional require calls for ${acceleratedModuleIds.join()}`);
          this.statistics.acceleratedModules += acceleratedModuleIds.length;
        }
      }
    } while (acceleratedModuleIds.length > 0);

    return effects;
  }

  // If a require fails, recover from it and delay the factory call until runtime
  // Also, only in this mode, consider "accelerating" require calls, see below.
  _callRequireAndDelayIfNeeded(moduleIdValue, performCall) {
    let realm = this.modules.realm;
    this.log(`>require(${moduleIdValue})`);
    let isTopLevelRequire = this.requireStack.length === 0;
    if (this.evaluateForEffectsNesting > 0) {
      if (isTopLevelRequire) {
        let diagnostic = new _errors.CompilerDiagnostic("Non-deterministically conditional top-level require not currently supported", realm.currentLocation, "PP0017", "FatalError");
        realm.handleError(diagnostic);
        throw new _errors.FatalError();
      } else if (!this.modules.isModuleInitialized(moduleIdValue))
        // Nested require call: We record that this happened. Just so that
        // if we discover later this this require call needs to get delayed,
        // then we still know (some of) which modules it in turn required,
        // and then we'll later "accelerate" requiring them to preserve the
        // require ordering. See below for more details on acceleration.
        this.uninitializedModuleIdsRequiredInEvaluateForEffects.add(moduleIdValue);

      return undefined;
    } else {
      return downgradeErrorsToWarnings(realm, () => {
        let result;
        try {
          this.requireStack.push(moduleIdValue);
          let requireSequenceStart = this.requireSequence.length;
          this.requireSequence.push(moduleIdValue);
          const previousNumDelayedModules = this.statistics.delayedModules;
          let effects = this._callRequireAndAccelerate(isTopLevelRequire, moduleIdValue, performCall);
          if (effects === undefined || effects[0] instanceof _completions.AbruptCompletion) {
            console.log(`delaying require(${moduleIdValue})`);
            this.statistics.delayedModules = previousNumDelayedModules + 1;
            // So we are about to emit a delayed require(...) call.
            // However, before we do that, let's try to require all modules that we
            // know this delayed require call will require.
            // This way, we ensure that those modules will be fully initialized
            // before the require call executes.
            // TODO #690: More needs to be done to make the delayUnsupportedRequires
            // feature completely safe. Open issues are:
            // 1) Side-effects on the heap of delayed factory functions are not discovered or rejected.
            // 2) While we do process an appropriate list of transitively required modules here,
            //    it's likely just a subset / prefix of all transivitely required modules, as
            //    more modules would have been required if the Introspection exception had not been thrown.
            //    To be correct, those modules would have to be prepacked here as well.
            //    TODO #798: Watch out for an upcoming change to the __d module declaration where the statically known
            //    list of dependencies will be announced, so we'll no longer have to guess.
            let nestedModulesIds = new Set();
            for (let i = requireSequenceStart; i < this.requireSequence.length; i++) {
              let nestedModuleId = this.requireSequence[i];
              if (nestedModulesIds.has(nestedModuleId)) continue;
              nestedModulesIds.add(nestedModuleId);
              this.modules.tryInitializeModule(nestedModuleId, `initialization of module ${nestedModuleId} as it's required by module ${moduleIdValue}`);
            }

            result = _index2.AbstractValue.createTemporalFromBuildFunction(realm, _index2.Value, [], ([]) => t.callExpression(t.identifier("require"), [t.valueToNode(moduleIdValue)]));
          } else {
            result = effects[0];
            if (result instanceof _index2.Value) {
              realm.applyEffects(effects, `initialization of module ${moduleIdValue}`);
              this.modules.recordModuleInitialized(moduleIdValue, result);
            } else if (result instanceof _completions.PossiblyNormalCompletion) {
              let warning = new _errors.CompilerDiagnostic("Module import may fail with an exception", result.location, "PP0018", "Warning");
              realm.handleError(warning);
              result = result.value;
              realm.applyEffects(effects, `initialization of module ${moduleIdValue}`);
            } else {
              (0, _invariant2.default)(false);
            }
          }
        } finally {
          let popped = this.requireStack.pop();
          (0, _invariant2.default)(popped === moduleIdValue);
          this.log(`<require(${moduleIdValue})`);
        }
        (0, _invariant2.default)(result instanceof _index2.Value);
        return result;
      });
    }
  }

  _tryExtractDependencies(value) {
    if (value === undefined || value instanceof _index2.NullValue || value instanceof _index2.UndefinedValue) return [];
    if (value instanceof _index2.ArrayValue) {
      const realm = this.modules.realm;
      const lengthValue = (0, _index.Get)(realm, value, "length");
      if (lengthValue instanceof _index2.NumberValue) {
        const dependencies = [];
        const logger = this.modules.logger;
        for (let i = 0; i < lengthValue.value; i++) {
          const elementValue = logger.tryQuery(() => (0, _index.Get)(realm, value, "" + i), realm.intrinsics.undefined);
          dependencies.push(elementValue);
        }
        return dependencies;
      }
    }
    return undefined;
  }

  detourCall(F, thisArgument, argumentsList, newTarget, performCall) {
    if (F === this.modules.getRequire() && !this.modules.disallowDelayingRequiresOverride && argumentsList.length === 1) {
      // Here, we handle calls of the form
      //   require(42)

      let moduleId = argumentsList[0];
      let moduleIdValue;
      // Do some sanity checks and request require(...) calls with bad arguments
      if (moduleId instanceof _index2.NumberValue || moduleId instanceof _index2.StringValue) {
        moduleIdValue = moduleId.value;
        if (!this.modules.moduleIds.has(moduleIdValue) && this.modules.delayUnsupportedRequires) {
          this.modules.logger.logError(moduleId, "Module referenced by require call has not been defined.");
        }
      } else {
        if (this.modules.delayUnsupportedRequires) {
          this.modules.logger.logError(moduleId, "First argument to require function is not a number or string value.");
        }
        return undefined;
      }

      if (this.modules.delayUnsupportedRequires) return this._callRequireAndDelayIfNeeded(moduleIdValue, performCall);else return this._callRequireAndRecord(moduleIdValue, performCall);
    } else if (F === this.modules.getDefine()) {
      // Here, we handle calls of the form
      //   __d(factoryFunction, moduleId, dependencyArray)

      if (this.evaluateForEffectsNesting !== 0) this.modules.logger.logError(F, "Defining a module in nested partial evaluation is not supported.");
      let factoryFunction = argumentsList[0];
      if (factoryFunction instanceof _index2.FunctionValue) {
        let dependencies = this._tryExtractDependencies(argumentsList[2]);
        if (dependencies !== undefined) this.modules.factoryFunctionDependencies.set(factoryFunction, dependencies);else this.modules.logger.logError(argumentsList[2], "Third argument to define function is present but not a concrete array.");
      } else this.modules.logger.logError(factoryFunction, "First argument to define function is not a function value.");
      let moduleId = argumentsList[1];
      if (moduleId instanceof _index2.NumberValue || moduleId instanceof _index2.StringValue) this.modules.moduleIds.add(moduleId.value);else this.modules.logger.logError(moduleId, "Second argument to define function is not a number or string value.");
    }
    return undefined;
  }
}

exports.ModuleTracer = ModuleTracer;
class Modules {
  constructor(realm, logger, statistics, logModules, delayUnsupportedRequires, accelerateUnsupportedRequires) {
    this.realm = realm;
    this.logger = logger;
    this.statistics = statistics;
    this._require = realm.intrinsics.undefined;
    this._define = realm.intrinsics.undefined;
    this.factoryFunctionDependencies = new Map();
    this.moduleIds = new Set();
    this.initializedModules = new Map();
    realm.tracers.push(this.moduleTracer = new ModuleTracer(this, statistics, logModules));
    this.delayUnsupportedRequires = delayUnsupportedRequires;
    this.accelerateUnsupportedRequires = accelerateUnsupportedRequires;
    this.disallowDelayingRequiresOverride = false;
  }

  resolveInitializedModules() {
    this.initializedModules.clear();
    let globalInitializedModulesMap = this._getGlobalProperty("__initializedModules");
    (0, _invariant2.default)(globalInitializedModulesMap instanceof _index2.ObjectValue);
    for (let moduleId of globalInitializedModulesMap.properties.keys()) {
      let property = globalInitializedModulesMap.properties.get(moduleId);
      (0, _invariant2.default)(property);
      let moduleValue = property.descriptor && property.descriptor.value;
      if (moduleValue instanceof _index2.Value) this.initializedModules.set(moduleId, moduleValue);
    }
    this.statistics.initializedModules = this.initializedModules.size;
    this.statistics.totalModules = this.moduleIds.size;
  }

  _getGlobalProperty(name) {
    if (this.active) return this.realm.intrinsics.undefined;
    this.active = true;
    try {
      let realm = this.realm;
      return this.logger.tryQuery(() => (0, _index.Get)(realm, realm.$GlobalObject, name), realm.intrinsics.undefined);
    } finally {
      this.active = false;
    }
  }

  getRequire() {
    if (!(this._require instanceof _index2.FunctionValue)) this._require = this._getGlobalProperty("require");
    return this._require;
  }

  getDefine() {
    if (!(this._define instanceof _index2.FunctionValue)) this._define = this._getGlobalProperty("__d");
    return this._define;
  }

  // Returns a function that checks if a call node represents a call to a
  // known require function, and if so, what module id that call indicates.
  // A known require function call is either of the form
  //   ... require(42) ...
  // where require resolves to the global require function, or
  //   factoryFunction(, require, , , dependencies) {
  //     ...
  //       ... require(dependencies[3]) ...
  // where factoryFunction and dependencies were announced as part of the
  // global code execution via a global module declaration call such as
  //   global.__d(factoryFunction, , [0,2,4,6,8])
  getGetModuleIdIfNodeIsRequireFunction(formalParameters, functions) {
    let realm = this.realm;
    let logger = this.logger;
    let modules = this;
    return (scope, node) => {
      // Are we calling a function that has a single name and a single argument?
      if (!t.isIdentifier(node.callee) || node.arguments.length !== 1) return undefined;
      let argument = node.arguments[0];
      if (!argument) return undefined;

      if (!t.isNumericLiteral(argument) && !t.isStringLiteral(argument) && !t.isMemberExpression(argument)) return undefined;

      (0, _invariant2.default)(node.callee);
      let innerName = node.callee.name;

      let moduleId;

      // Helper function used to give up if we ever come up with different module ids for different functions
      let updateModuleId = newModuleId => {
        if (moduleId !== undefined && moduleId !== newModuleId) return false;
        moduleId = newModuleId;
        return true;
      };

      // Helper function that retrieves module id from call argument, possibly chasing dependency array indirection
      const getModuleId = dependencies => {
        if (t.isMemberExpression(argument)) {
          if (dependencies !== undefined) {
            let memberExpression = argument;
            if (t.isIdentifier(memberExpression.object)) {
              let scopedBinding = scope.getBinding(memberExpression.object.name);
              if (scopedBinding && formalParameters[4] === scopedBinding.path.node) {
                if (t.isNumericLiteral(memberExpression.property)) {
                  let dependencyIndex = memberExpression.property.value;
                  if (Number.isInteger(dependencyIndex) && dependencyIndex >= 0 && dependencyIndex < dependencies.length) {
                    let dependency = dependencies[dependencyIndex];
                    if (dependency instanceof _index2.NumberValue || dependency instanceof _index2.StringValue) return dependency.value;
                  }
                }
              }
            }
          }
        } else {
          return argument.value;
        }
      };

      // Let's consider each of the function instances (closures for the same code)
      for (let f of functions) {
        // 1. Let's check if we have a match for a factory function like
        //      factoryFunction(, require, , , [dependencies])
        //    which is used with the Metro bundler
        let scopedBinding = scope.getBinding(innerName);
        if (scopedBinding) {
          let dependencies = modules.factoryFunctionDependencies.get(f);
          if (dependencies !== undefined && formalParameters[1] === scopedBinding.path.node) {
            (0, _invariant2.default)(scopedBinding.kind === "param");
            let newModuleId = getModuleId(dependencies);
            if (newModuleId !== undefined && !updateModuleId(newModuleId)) return undefined;
            continue;
          }

          // The name binds to some local entity, but nothing we'd know what exactly it is
          return undefined;
        }

        // 2. Let's check if we can resolve the called function just by looking at the
        //    function instance environment.
        //    TODO: We should not do this if the current node is in a nested function!

        // We won't have a dependency map here, so this only works for literal arguments.
        if (!t.isNumericLiteral(argument) && !t.isStringLiteral(argument)) return undefined;

        let doesNotMatter = true;
        let reference = logger.tryQuery(() => _singletons.Environment.ResolveBinding(realm, innerName, doesNotMatter, f.$Environment), undefined);
        if (reference === undefined) {
          // We couldn't resolve as we came across some behavior that we cannot deal with abstractly
          return undefined;
        }
        if (_singletons.Environment.IsUnresolvableReference(realm, reference)) return undefined;
        let referencedBase = reference.base;
        let referencedName = reference.referencedName;
        if (typeof referencedName !== "string") return undefined;
        let value;
        if (reference.base instanceof _environment.GlobalEnvironmentRecord) {
          value = logger.tryQuery(() => (0, _index.Get)(realm, realm.$GlobalObject, innerName), realm.intrinsics.undefined);
        } else {
          (0, _invariant2.default)(referencedBase instanceof _environment.DeclarativeEnvironmentRecord);
          let binding = referencedBase.bindings[referencedName];
          if (!binding.initialized) return undefined;
          value = binding.value;
        }
        if (value !== modules.getRequire()) return undefined;
        const newModuleId = getModuleId();
        (0, _invariant2.default)(newModuleId !== undefined);
        if (!updateModuleId(newModuleId)) return undefined;
      }

      return moduleId;
    };
  }

  recordModuleInitialized(moduleId, value) {
    this.realm.assignToGlobal(t.memberExpression(t.memberExpression(t.identifier("global"), t.identifier("__initializedModules")), t.identifier("" + moduleId)), value);
  }

  tryInitializeModule(moduleId, message) {
    let realm = this.realm;
    let previousDisallowDelayingRequiresOverride = this.disallowDelayingRequiresOverride;
    this.disallowDelayingRequiresOverride = true;
    return downgradeErrorsToWarnings(realm, () => {
      try {
        let node = t.callExpression(t.identifier("require"), [t.valueToNode(moduleId)]);

        let effects = realm.evaluateNodeForEffectsInGlobalEnv(node);
        realm.applyEffects(effects, message);
        return effects;
      } catch (err) {
        if (err instanceof _errors.FatalError) return undefined;else throw err;
      } finally {
        this.disallowDelayingRequiresOverride = previousDisallowDelayingRequiresOverride;
      }
    });
  }

  initializeMoreModules() {
    // partially evaluate all factory methods by calling require
    let count = 0;
    for (let moduleId of this.moduleIds) {
      if (this.initializedModules.has(moduleId)) continue;
      let effects = this.tryInitializeModule(moduleId, `Speculative initialization of module ${moduleId}`);
      if (effects === undefined) continue;
      let result = effects[0];
      if (!(result instanceof _index2.Value)) continue; // module might throw
      count++;
      this.initializedModules.set(moduleId, result);
    }
    if (count > 0) console.log(`=== speculatively initialized ${count} additional modules`);
  }

  isModuleInitialized(moduleId) {
    let realm = this.realm;
    let oldReadOnly = realm.setReadOnly(true);
    let oldDisallowDelayingRequiresOverride = this.disallowDelayingRequiresOverride;
    this.disallowDelayingRequiresOverride = true;
    try {
      let node = t.callExpression(t.identifier("require"), [t.valueToNode(moduleId)]);

      let [compl, generator, bindings, properties, createdObjects] = realm.evaluateNodeForEffectsInGlobalEnv(node);
      // for lint unused
      (0, _invariant2.default)(bindings);

      if (compl instanceof _completions.AbruptCompletion) return undefined;
      (0, _invariant2.default)(compl instanceof _index2.Value);

      if (!generator.empty() || compl instanceof _index2.ObjectValue && createdObjects.has(compl)) return undefined;
      // Check for escaping property assignments, if none escape, we got an existing object
      let escapes = false;
      for (let [binding] of properties) {
        let object = binding.object;
        (0, _invariant2.default)(object instanceof _index2.ObjectValue);
        if (!createdObjects.has(object)) escapes = true;
      }
      if (escapes) return undefined;

      return compl;
    } catch (err) {
      if (err instanceof _errors.FatalError) return undefined;
      throw err;
    } finally {
      realm.setReadOnly(oldReadOnly);
      this.disallowDelayingRequiresOverride = oldDisallowDelayingRequiresOverride;
    }
  }
}
exports.Modules = Modules;
//# sourceMappingURL=modules.js.map