prepack
Version:
Execute a JS bundle, serialize global state and side effects to a snapshot that can be quickly restored.
545 lines (484 loc) • 25.1 kB
JavaScript
;
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._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);
}
}
_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