Allow required modules to throw conditionally

Summary:
Instead of delaying modules that throw conditionally, let the exception bubble up to the require call and then forget it (after emitting a warning). This allows more global state to be optimized and should be OK if it is understood that throwing an unhandled exception in module initialization code is not a supported scenario.

Probably, the temporal point where the require call happens should contain a conditional throw statement, which would be equivalent to current behavior. For now, this causes invariants to fire in the serializer, probably because of bugs in how the state at the time of the exception is restored and presented to the throw statement.

It is also an option to let the exception escape the require call itself and possibly bubble all the way to the top level. This would be more correct than the current behavior since it should match the runtime behavior of the unprepacked code. This too is currently buggy. It also a bit of performance concern because it uses much more saved state.
Closes https://github.com/facebook/prepack/pull/1104

Differential Revision: D6189032

Pulled By: hermanventer

fbshipit-source-id: 71c6352eddca4d88ae030fdcbfb76e7a39839e73
This commit is contained in:
Herman Venter 2017-10-30 12:54:06 -07:00 committed by Facebook Github Bot
parent d7dce73e80
commit 80b0757f67
8 changed files with 419 additions and 22 deletions

View File

@ -55,6 +55,7 @@ function runTest(name: string, code: string): boolean {
let recover = code.includes("// recover-from-errors");
let additionalFunctions = code.includes("// additional functions");
let delayUnsupportedRequires = code.includes("// delay unsupported requires");
let expectedErrors = code.match(/\/\/\s*expected errors:\s*(.*)/);
invariant(expectedErrors);
@ -67,10 +68,11 @@ function runTest(name: string, code: string): boolean {
try {
let options = {
internalDebug: false,
delayUnsupportedRequires,
mathRandomSeed: "0",
errorHandler: errorHandler.bind(null, recover ? "Recover" : "Fail", errors),
serialize: true,
initializeMoreModules: true,
initializeMoreModules: false,
};
if (additionalFunctions) (options: any).additionalFunctions = ["global.additional1", "global['additional2']"];
prepackFileSync([name], options);

View File

@ -326,6 +326,12 @@ export class Realm {
}
popContext(context: ExecutionContext): void {
let modifiedBindings = this.modifiedBindings;
if (modifiedBindings !== undefined) {
for (let b of modifiedBindings.keys()) {
if (b.environment.$FunctionObject === context.function) modifiedBindings.delete(b);
}
}
let c = this.contextStack.pop();
invariant(c === context);
let savedEffects = context.savedEffects;
@ -529,6 +535,14 @@ export class Realm {
return [v, this.generator, this.modifiedBindings, this.modifiedProperties, this.createdObjects];
}
stopEffectCapture() {
let e = this.getCapturedEffects();
if (e !== undefined) {
this.stopEffectCaptureAndUndoEffects();
this.applyEffects(e);
}
}
stopEffectCaptureAndUndoEffects() {
// Roll back the state changes
this.restoreBindings(this.modifiedBindings);

View File

@ -1453,7 +1453,7 @@ export class ResidualHeapSerializer {
let { unstrictFunctionBodies, strictFunctionBodies, requireStatistics } = this.residualFunctions.spliceFunctions(
rewrittenAdditionalFunctions
);
if (requireStatistics.replaced > 0 && !this.residualHeapValueIdentifiers.collectValToRefCountOnly) {
if (this.modules.moduleIds.size > 0 && !this.residualHeapValueIdentifiers.collectValToRefCountOnly) {
console.log(
`=== ${this.modules.initializedModules.size} of ${this.modules.moduleIds
.size} modules initialized, ${requireStatistics.replaced} of ${requireStatistics.count} require calls inlined.`

View File

@ -13,7 +13,7 @@ import { GlobalEnvironmentRecord, DeclarativeEnvironmentRecord } from "../enviro
import { CompilerDiagnostic, FatalError } from "../errors.js";
import { Realm, Tracer } from "../realm.js";
import type { Effects } from "../realm.js";
import { IsUnresolvableReference, ResolveBinding, Get } from "../methods/index.js";
import { incorporateSavedCompletion, IsUnresolvableReference, ResolveBinding, Get } from "../methods/index.js";
import { AbruptCompletion, Completion, PossiblyNormalCompletion, ThrowCompletion } from "../completions.js";
import { AbstractValue, Value, FunctionValue, ObjectValue, NumberValue, StringValue } from "../values/index.js";
import * as t from "babel-types";
@ -120,6 +120,17 @@ export class ModuleTracer extends Tracer {
try {
let value = performCall();
this.modules.recordModuleInitialized(moduleIdValue, value);
let completion = incorporateSavedCompletion(realm, value);
if (completion instanceof PossiblyNormalCompletion) {
realm.stopEffectCapture();
let warning = new CompilerDiagnostic(
"Module import may fail with an exception",
completion.location,
"PP0018",
"Warning"
);
realm.handleError(warning);
}
return value;
} finally {
invariant(this.requireStack.pop() === moduleIdValue);
@ -202,14 +213,6 @@ export class ModuleTracer extends Tracer {
}
} while (acceleratedModuleIds.length > 0);
if (effects !== undefined) {
result = effects[0];
invariant(result instanceof Value || result instanceof Completion);
if (result instanceof PossiblyNormalCompletion) {
effects = undefined;
}
}
if (effects === undefined) {
console.log(`delaying require(${moduleIdValue})`);
this.statistics.delayedModules = previousNumDelayedModules + 1;
@ -242,11 +245,22 @@ export class ModuleTracer extends Tracer {
t.callExpression(t.identifier("require"), [t.valueToNode(moduleIdValue)])
);
} else {
invariant(result);
if (!(result instanceof Completion)) {
result = effects[0];
if (result instanceof Value) {
realm.applyEffects(effects, `initialization of module ${moduleIdValue}`);
this.modules.recordModuleInitialized(moduleIdValue, result);
} else if (result instanceof PossiblyNormalCompletion) {
let warning = new 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}`);
this.modules.recordModuleInitialized(moduleIdValue, result);
}
realm.applyEffects(effects, `initialization of module ${moduleIdValue}`);
}
} finally {
let popped = this.requireStack.pop();
@ -416,14 +430,6 @@ export class Modules {
let effects = realm.evaluateNodeForEffectsInGlobalEnv(node);
realm.applyEffects(effects, message);
let result = effects[0];
if (result instanceof Completion) {
console.log(`=== UNEXPECTED ERROR during ${message} ===`);
//this.logger.logCompletion(result);
return undefined;
}
if (result instanceof Value) this.recordModuleInitialized(moduleId, result);
return effects;
} catch (err) {
if (err instanceof FatalError) return undefined;

View File

@ -0,0 +1,117 @@
// recover-from-errors
// expected errors: [{"location":{"start":{"line":84,"column":4},"end":{"line":84,"column":12},"source":"test/error-handler/require_throws.js"},"severity":"Warning","errorCode":"PP0018"}]
let b = global.__abstract ? __abstract("boolean", "true") : true;
var modules = Object.create(null);
__d = define;
function require(moduleId) {
var moduleIdReallyIsNumber = moduleId;
var module = modules[moduleIdReallyIsNumber];
return module && module.isInitialized ? module.exports : guardedLoadModule(moduleIdReallyIsNumber, module);
}
function define(factory, moduleId, dependencyMap) {
if (moduleId in modules) {
return;
}
modules[moduleId] = {
dependencyMap: dependencyMap,
exports: undefined,
factory: factory,
hasError: false,
isInitialized: false
};
var _verboseName = arguments[3];
if (_verboseName) {
modules[moduleId].verboseName = _verboseName;
verboseNamesToModuleIds[_verboseName] = moduleId;
}
}
var inGuard = false;
function guardedLoadModule(moduleId, module) {
if (!inGuard && global.ErrorUtils) {
inGuard = true;
var returnValue = void 0;
try {
returnValue = loadModuleImplementation(moduleId, module);
} catch (e) {
global.ErrorUtils.reportFatalError(e);
}
inGuard = false;
return returnValue;
} else {
return loadModuleImplementation(moduleId, module);
}
}
function loadModuleImplementation(moduleId, module) {
var nativeRequire = global.nativeRequire;
if (!module && nativeRequire) {
nativeRequire(moduleId);
module = modules[moduleId];
}
if (!module) {
throw unknownModuleError(moduleId);
}
if (module.hasError) {
throw moduleThrewError(moduleId);
}
module.isInitialized = true;
var exports = module.exports = {};
var _module = module,
factory = _module.factory,
dependencyMap = _module.dependencyMap;
try {
var _moduleObject = { exports: exports };
factory(global, require, _moduleObject, exports, dependencyMap);
module.factory = undefined;
return module.exports = _moduleObject.exports;
} catch (e) {
module.hasError = true;
module.isInitialized = false;
module.exports = undefined;
throw e;
}
}
function unknownModuleError(id) {
var message = 'Requiring unknown module "' + id + '".';
return Error(message);
}
function moduleThrewError(id) {
return Error('Requiring module "' + id + '", which threw an exception.');
}
// === End require code ===
define(function(global, require, module, exports) {
var obj = global.__abstract ? __makeSimple(__abstract({unsupported: true}, "({unsupported: true})")) : ({unsupported: true});
if (obj.unsupported) {
exports.magic = 42;
} else {
exports.magic = 23;
}
if (!b) throw "something bad";
exports.notmagic = 666;
}, 0, null);
define(function(global, require, module, exports) {
var x = require(0);
module.exports = function() { return x.notmagic; }
}, 1, null);
var f = require(1);
inspect = function() { return f().magic; }

View File

@ -0,0 +1,118 @@
// delay unsupported requires
// recover-from-errors
// expected errors: [{"location":{"start":{"line":85,"column":4},"end":{"line":85,"column":12},"source":"test/error-handler/require_throws2.js"},"severity":"Warning","errorCode":"PP0018"}]
let b = global.__abstract ? __abstract("boolean", "true") : true;
var modules = Object.create(null);
__d = define;
function require(moduleId) {
var moduleIdReallyIsNumber = moduleId;
var module = modules[moduleIdReallyIsNumber];
return module && module.isInitialized ? module.exports : guardedLoadModule(moduleIdReallyIsNumber, module);
}
function define(factory, moduleId, dependencyMap) {
if (moduleId in modules) {
return;
}
modules[moduleId] = {
dependencyMap: dependencyMap,
exports: undefined,
factory: factory,
hasError: false,
isInitialized: false
};
var _verboseName = arguments[3];
if (_verboseName) {
modules[moduleId].verboseName = _verboseName;
verboseNamesToModuleIds[_verboseName] = moduleId;
}
}
var inGuard = false;
function guardedLoadModule(moduleId, module) {
if (!inGuard && global.ErrorUtils) {
inGuard = true;
var returnValue = void 0;
try {
returnValue = loadModuleImplementation(moduleId, module);
} catch (e) {
global.ErrorUtils.reportFatalError(e);
}
inGuard = false;
return returnValue;
} else {
return loadModuleImplementation(moduleId, module);
}
}
function loadModuleImplementation(moduleId, module) {
var nativeRequire = global.nativeRequire;
if (!module && nativeRequire) {
nativeRequire(moduleId);
module = modules[moduleId];
}
if (!module) {
throw unknownModuleError(moduleId);
}
if (module.hasError) {
throw moduleThrewError(moduleId);
}
module.isInitialized = true;
var exports = module.exports = {};
var _module = module,
factory = _module.factory,
dependencyMap = _module.dependencyMap;
try {
var _moduleObject = { exports: exports };
factory(global, require, _moduleObject, exports, dependencyMap);
module.factory = undefined;
return module.exports = _moduleObject.exports;
} catch (e) {
module.hasError = true;
module.isInitialized = false;
module.exports = undefined;
throw e;
}
}
function unknownModuleError(id) {
var message = 'Requiring unknown module "' + id + '".';
return Error(message);
}
function moduleThrewError(id) {
return Error('Requiring module "' + id + '", which threw an exception.');
}
// === End require code ===
define(function(global, require, module, exports) {
var obj = global.__abstract ? __makeSimple(__abstract({unsupported: true}, "({unsupported: true})")) : ({unsupported: true});
if (obj.unsupported) {
exports.magic = 42;
} else {
exports.magic = 23;
}
if (!b) throw "something bad";
exports.notmagic = 666;
}, 0, null);
define(function(global, require, module, exports) {
var x = require(0);
module.exports = function() { return x.notmagic; }
}, 1, null);
var f = require(1);
inspect = function() { return f().magic; }

View File

@ -0,0 +1,26 @@
// delay unsupported requires
let x = global.__abstract ? __abstract("boolean", "true") : true;
function __d(factory, moduleId) {
}
function foo() {
let r = {xyz: 123};
if (!x) throw "something bad";
return r;
}
function require(i) {
try {
return foo();
} catch (e) {
throw e;
}
}
__d(foo, 0);
z = require(0);
inspect = function() { return z; }

View File

@ -0,0 +1,114 @@
let b = global.__abstract ? __abstract("boolean", "true") : true;
var modules = Object.create(null);
__d = define;
function require(moduleId) {
var moduleIdReallyIsNumber = moduleId;
var module = modules[moduleIdReallyIsNumber];
return module && module.isInitialized ? module.exports : guardedLoadModule(moduleIdReallyIsNumber, module);
}
function define(factory, moduleId, dependencyMap) {
if (moduleId in modules) {
return;
}
modules[moduleId] = {
dependencyMap: dependencyMap,
exports: undefined,
factory: factory,
hasError: false,
isInitialized: false
};
var _verboseName = arguments[3];
if (_verboseName) {
modules[moduleId].verboseName = _verboseName;
verboseNamesToModuleIds[_verboseName] = moduleId;
}
}
var inGuard = false;
function guardedLoadModule(moduleId, module) {
if (!inGuard && global.ErrorUtils) {
inGuard = true;
var returnValue = void 0;
try {
returnValue = loadModuleImplementation(moduleId, module);
} catch (e) {
global.ErrorUtils.reportFatalError(e);
}
inGuard = false;
return returnValue;
} else {
return loadModuleImplementation(moduleId, module);
}
}
function loadModuleImplementation(moduleId, module) {
var nativeRequire = global.nativeRequire;
if (!module && nativeRequire) {
nativeRequire(moduleId);
module = modules[moduleId];
}
if (!module) {
throw unknownModuleError(moduleId);
}
if (module.hasError) {
throw moduleThrewError(moduleId);
}
module.isInitialized = true;
var exports = module.exports = {};
var _module = module,
factory = _module.factory,
dependencyMap = _module.dependencyMap;
try {
var _moduleObject = { exports: exports };
factory(global, require, _moduleObject, exports, dependencyMap);
module.factory = undefined;
return module.exports = _moduleObject.exports;
} catch (e) {
module.hasError = true;
module.isInitialized = false;
module.exports = undefined;
throw e;
}
}
function unknownModuleError(id) {
var message = 'Requiring unknown module "' + id + '".';
return Error(message);
}
function moduleThrewError(id) {
return Error('Requiring module "' + id + '", which threw an exception.');
}
// === End require code ===
define(function(global, require, module, exports) {
var obj = global.__abstract ? __makeSimple(__abstract({unsupported: true}, "({unsupported: true})")) : ({unsupported: true});
if (obj.unsupported) {
exports.magic = 42;
} else {
exports.magic = 23;
}
if (!b) throw "something bad";
exports.notmagic = 666;
}, 0, null);
define(function(global, require, module, exports) {
var x = require(0);
module.exports = function() { return x.notmagic; }
}, 1, null);
var f = require(1);
inspect = function() { return f().magic; }