Early termination for fatal errors

Summary:
If an error is reported to the host application and it returns a value that indicates that the error is fatal, the evaluator now throws a FatalError exception rather than causing an IntrospectionError.

This terminates things quickly and cleanly but uncovered a bug in the way effect tracking is cleaned up in the face of exceptions that are not throw completions. To fix this, the code that pops an evaluation context off the stack now checks if there are any dangling effects in the context and, if there are, it folds them into the effects of the outer context.

The effects then propagate to the most closely nested evaluateForEffects call, where they are
rolled back from the global state and incorporated into the effects returned from the call.

When they propagate all the way to a test runner, as happens when there is an IntrospectionError in global code, the state is never rolled back and thus the error can be logged in the state that applied when it was created.

DEPRECATED API
The InitializationError constructor in prepack-standalone.js is going to go away in a future release. Please use FatalError instead. For now, InitializationError.prototype is on the prototype chain of FatalError.prototype, so instances of FatalError will still be instances of InitializationError, so this should not be a breaking change in the next release.
Closes https://github.com/facebook/prepack/pull/740

Differential Revision: D5286107

Pulled By: hermanventer

fbshipit-source-id: 05c7f9197acaa0ba922d136f122968b2f41a4b82
This commit is contained in:
Herman Venter 2017-06-20 20:18:43 -07:00 committed by Facebook Github Bot
parent 14e015c0ac
commit 6cb22dd91e
10 changed files with 84 additions and 37 deletions

View File

@ -9,6 +9,8 @@
/* @flow */
import type { CompilerDiagnostics, ErrorHandlerResult } from "../lib/errors.js";
import type { BabelNodeSourceLocation } from "babel-types";
import { prepack } from "../lib/prepack-node.js";
let chalk = require("chalk");
@ -39,6 +41,13 @@ function search(dir, relative) {
let tests = search(`${__dirname}/../test/internal`, "test/internal");
let errors: Map<BabelNodeSourceLocation, CompilerDiagnostics> = new Map();
function errorHandler(diagnostic: CompilerDiagnostics): ErrorHandlerResult {
if (diagnostic.location)
errors.set(diagnostic.location, diagnostic);
return "Fail";
}
function runTest(name: string, code: string): boolean {
console.log(chalk.inverse(name));
try {
@ -49,7 +58,8 @@ function runTest(name: string, code: string): boolean {
mathRandomSeed: "0",
serialize: true,
speculate: true,
});
},
errorHandler);
if (!serialized) {
console.log(chalk.red("Error during serialization"));
return false;
@ -59,6 +69,10 @@ function runTest(name: string, code: string): boolean {
} catch (e) {
console.log(e);
return false;
} finally {
for (let [loc, error] of errors) {
console.log(`${loc.start.line}:${loc.start.column} ${error.errorCode} ${error.message}`);
}
}
}

View File

@ -9,8 +9,8 @@
/* @flow */
let FatalError = require("../lib/errors.js").FatalError;
let prepack = require("../lib/prepack-node.js").prepack;
let InitializationError = require("../lib/prepack-node.js").InitializationError;
let Serializer = require("../lib/serializer/index.js").default;
let construct_realm = require("../lib/construct_realm.js").default;
@ -116,7 +116,7 @@ function runTest(name, code, args) {
console.log(chalk.red("Test should have caused introspection error!"));
}
} catch (err) {
if (err instanceof Success) return true;
if (err instanceof Success || err instanceof FatalError) return true;
console.log("Test should have caused introspection error, but instead caused a different internal error!");
console.log(err);
}
@ -125,7 +125,7 @@ function runTest(name, code, args) {
try {
prepack(code, options);
} catch (err) {
if (err instanceof InitializationError) {
if (err instanceof FatalError) {
return true;
}
}

View File

@ -31,4 +31,17 @@ export class CompilerDiagnostics extends Error {
errorCode: string;
}
// This error is thrown to exit Prepack when an ErrorHandler returns 'FatalError'
// This should just be a class but Babel classes doesn't work with
// built-in super classes.
export function FatalError() {
let self = new Error("A fatal error occurred while prepacking.");
Object.setPrototypeOf(self, FatalError.prototype);
return self;
}
Object.setPrototypeOf(FatalError, Error);
Object.setPrototypeOf(FatalError.prototype, Error.prototype);
export const fatalError = new FatalError();
export type ErrorHandler = (error: CompilerDiagnostics) => ErrorHandlerResult;

View File

@ -11,8 +11,8 @@
import type { Realm } from "../realm.js";
import type { LexicalEnvironment } from "../environment.js";
import { CompilerDiagnostics } from "../errors.js";
import { Value, AbstractValue, AbstractObjectValue, UndefinedValue, NullValue, BooleanValue, NumberValue, ObjectValue, StringValue } from "../values/index.js";
import { CompilerDiagnostics, fatalError } from "../errors.js";
import { Value, AbstractValue, AbstractObjectValue, ConcreteValue, UndefinedValue, NullValue, BooleanValue, NumberValue, ObjectValue, StringValue } from "../values/index.js";
import { GetValue } from "../methods/index.js";
import { HasProperty, HasSomeCompatibleType } from "../methods/index.js";
import { Add, AbstractEqualityComparison, StrictEqualityComparison, AbstractRelationalComparison, InstanceofOperator, IsToPrimitivePure, GetToPrimitivePureResultType, IsToNumberPure } from "../methods/index.js";
@ -39,8 +39,8 @@ let unknownValueOfOrToString = "might be an object with an unknown valueOf or to
// Returns result type if binary operation is pure (terminates, does not throw exception, does not read or write heap), otherwise undefined.
export function getPureBinaryOperationResultType(
realm: Realm, op: BabelBinaryOperator, lval: Value, rval: Value, lloc: ?BabelNodeSourceLocation, rloc: ?BabelNodeSourceLocation
): void | typeof Value {
function reportErrorIfNotPure(purityTest: (Realm, Value) => boolean, typeIfPure: typeof Value): void | typeof Value {
): typeof Value {
function reportErrorIfNotPure(purityTest: (Realm, Value) => boolean, typeIfPure: typeof Value): typeof Value {
let leftPure = purityTest(realm, lval);
let rightPure = purityTest(realm, rval);
if (leftPure && rightPure) return typeIfPure;
@ -50,20 +50,23 @@ export function getPureBinaryOperationResultType(
// Assume that an unknown value is actually a primitive or otherwise a well behaved object.
return typeIfPure;
}
return undefined;
throw fatalError;
}
if (op === "+") {
let ltype = GetToPrimitivePureResultType(realm, lval);
let rtype = GetToPrimitivePureResultType(realm, rval);
if (ltype === undefined || rtype === undefined) {
let [loc, type] = ltype === undefined ? [lloc, rtype] : [rloc, ltype];
let loc = ltype === undefined ? lloc : rloc;
let error = new CompilerDiagnostics(unknownValueOfOrToString, loc, 'PP0002', 'RecoverableError');
if (realm.handleError(error) === 'Recover') {
// Assume that the unknown value is actually a primitive or otherwise a well behaved object.
// Also assume that it does not convert to a string if type is a number.
return type;
ltype = lval.getType();
rtype = rval.getType();
if (ltype === StringValue || rtype === StringValue) return StringValue;
if (ltype === NumberValue && rtype === NumberValue) return NumberValue;
return Value;
}
return undefined;
throw fatalError;
}
if (ltype === StringValue || rtype === StringValue) return StringValue;
return NumberValue;
@ -82,7 +85,7 @@ export function getPureBinaryOperationResultType(
// Assume that the object is actually a well behaved object.
return BooleanValue;
}
return undefined;
throw fatalError;
}
if (rval instanceof ObjectValue || rval instanceof AbstractObjectValue) {
// Simple object won't throw here, aren't proxy objects or typed arrays and do not have @@hasInstance properties.
@ -94,7 +97,7 @@ export function getPureBinaryOperationResultType(
// Assume that the object is actually a well behaved object.
return BooleanValue;
}
return undefined;
throw fatalError;
}
invariant(false, "unimplemented " + op);
}
@ -111,14 +114,12 @@ export function computeBinary(
if ((lval instanceof AbstractValue) || (rval instanceof AbstractValue)) {
let type = getPureBinaryOperationResultType(realm, op, lval, rval, lloc, rloc);
if (type !== undefined) {
return realm.createAbstract(new TypesDomain(type), ValuesDomain.topVal, [lval, rval],
([lnode, rnode]) => t.binaryExpression(op, lnode, rnode));
}
return realm.createAbstract(new TypesDomain(type), ValuesDomain.topVal, [lval, rval],
([lnode, rnode]) => t.binaryExpression(op, lnode, rnode));
}
lval = lval.throwIfNotConcrete();
rval = rval.throwIfNotConcrete();
invariant(lval instanceof ConcreteValue);
invariant(rval instanceof ConcreteValue);
if (op === "+") {
// ECMA262 12.8.3 The Addition Operator

View File

@ -11,7 +11,8 @@
/* eslint-disable no-shadow */
import { prepackStdin, prepackFileSync, InitializationError } from "./prepack-node.js";
import { FatalError } from "./errors.js";
import { prepackStdin, prepackFileSync } from "./prepack-node.js";
import { CompatibilityValues, type Compatibility } from './types.js';
import fs from "fs";
@ -20,7 +21,7 @@ declare var __residual : any;
// Currently we need to explictly pass the captured variables we want to access.
// TODO: In a future version of this can be automatic.
function run(Object, Array, console, JSON, process, prepackStdin, prepackFileSync, InitializationError, CompatibilityValues, fs) {
function run(Object, Array, console, JSON, process, prepackStdin, prepackFileSync, FatalError, CompatibilityValues, fs) {
let HELP_STR = `
input The name of the file to run Prepack over (for web please provide the single js bundle file)
@ -125,8 +126,8 @@ function run(Object, Array, console, JSON, process, prepackStdin, prepackFileSyn
);
processSerializedCode(serialized);
} catch (x) {
if (x instanceof InitializationError) {
// Ignore InitializationError since they have already logged
if (x instanceof FatalError) {
// Ignore FatalError since an error has already logged
// their errors to the console, but exit with an error code.
process.exit(1);
}
@ -151,7 +152,7 @@ function run(Object, Array, console, JSON, process, prepackStdin, prepackFileSyn
if (typeof __residual === 'function') {
// If we're running inside of Prepack. This is the residual function we'll
// want to leave untouched in the final program.
__residual('boolean', run, Object, Array, console, JSON, process, prepackStdin, prepackFileSync, InitializationError, CompatibilityValues, fs);
__residual('boolean', run, Object, Array, console, JSON, process, prepackStdin, prepackFileSync, FatalError, CompatibilityValues, fs);
} else {
run(Object, Array, console, JSON, process, prepackStdin, prepackFileSync, InitializationError, CompatibilityValues, fs);
run(Object, Array, console, JSON, process, prepackStdin, prepackFileSync, FatalError, CompatibilityValues, fs);
}

View File

@ -17,7 +17,7 @@ import { Value } from "./values";
import construct_realm from "./construct_realm.js";
import initializeGlobals from "./globals.js";
import { getRealmOptions, getSerializerOptions } from "./options";
import { InitializationError } from "./prepack-standalone";
import { fatalError } from "./errors.js";
import initializeBootstrap from "./intrinsics/node/bootstrap.js";
import initializeProcess from "./intrinsics/node/process.js";
@ -108,7 +108,7 @@ export function prepackNodeCLISync(filename: string, options: Options = defaultO
// Serialize
let serialized = serializer.init("", "", "", options.sourceMaps);
if (!serialized) {
throw new InitializationError();
throw fatalError;
}
return serialized;
}

View File

@ -14,7 +14,7 @@ import initializeGlobals from "./globals.js";
import fs from "fs";
import { AbruptCompletion } from "./completions.js";
import { getRealmOptions, getSerializerOptions } from "./options";
import { InitializationError } from "./prepack-standalone";
import { fatalError } from "./errors.js";
import { prepackNodeCLI, prepackNodeCLISync } from "./prepack-node-environment";
import type { Options } from "./options";
@ -41,7 +41,7 @@ export function prepackString(filename: string, code: string, sourceMap: string,
options.sourceMaps
);
if (!serialized) {
throw new InitializationError();
throw fatalError;
}
if (!options.residual) return serialized;
let result = realm.$GlobalEnv.executePartialEvaluator(

View File

@ -12,15 +12,16 @@ import Serializer from "./serializer/index.js";
import construct_realm from "./construct_realm.js";
import initializeGlobals from "./globals.js";
import * as t from "babel-types";
import { FatalError } from "./errors.js";
import { getRealmOptions, getSerializerOptions } from "./options";
import { type ErrorHandler } from "./errors.js";
import { type ErrorHandler, fatalError } from "./errors.js";
import type { Options } from "./options";
import { defaultOptions } from "./options";
import type { BabelNodeFile, BabelNodeProgram } from "babel-types";
// This should just be a class but Babel classes doesn't work with
// built-in super classes.
// IMPORTANT: This function is now deprecated and will go away in a future release.
// Please use FatalError instead.
export function InitializationError() {
let self = new Error("An error occurred while prepacking. See the error logs.");
Object.setPrototypeOf(self, InitializationError.prototype);
@ -28,6 +29,7 @@ export function InitializationError() {
}
Object.setPrototypeOf(InitializationError, Error);
Object.setPrototypeOf(InitializationError.prototype, Error.prototype);
Object.setPrototypeOf(FatalError.prototype, InitializationError.prototype);
export function prepack(code: string, options: Options = defaultOptions, errorHandler?: ErrorHandler) {
let filename = options.filename || 'unknown';
@ -40,7 +42,7 @@ export function prepack(code: string, options: Options = defaultOptions, errorHa
let serializer = new Serializer(realm, getSerializerOptions(options));
let serialized = serializer.init(filename, code, "", options.sourceMaps);
if (!serialized) {
throw new InitializationError();
throw fatalError;
}
return serialized;
}
@ -60,7 +62,7 @@ export function prepackFromAst(ast: BabelNodeFile | BabelNodeProgram, code: stri
let serializer = new Serializer(realm, getSerializerOptions(options));
let serialized = serializer.init("", code, "", options.sourceMaps);
if (!serialized) {
throw new InitializationError();
throw fatalError;
}
return serialized;
}

View File

@ -231,6 +231,15 @@ export class Realm {
popContext(context: ExecutionContext): void {
let c = this.contextStack.pop();
invariant(c === context);
let savedEffects = context.savedEffects;
if (savedEffects !== undefined && this.contextStack.length > 0) {
// when unwinding the stack after a fatal error, saved effects are not incorporated into completions
// and thus must be propogated to the calling context.
let ctx = this.getRunningContext();
if (ctx.savedEffects !== undefined)
this.addPriorEffects(ctx.savedEffects, savedEffects);
ctx.savedEffects = savedEffects;
}
}
// Evaluate the given ast in a sandbox and return the evaluation results
@ -290,11 +299,15 @@ export class Realm {
// add prior effects that are not already present
this.addPriorEffects(savedEffects, result);
this.updateAbruptCompletions(savedEffects, c);
context.savedEffects = undefined;
}
}
return result;
} finally {
// Roll back the state changes
if (context.savedEffects !== undefined) {
this.stopEffectCaptureAndUndoEffects();
}
this.restoreBindings(this.modifiedBindings);
this.restoreProperties(this.modifiedProperties);
context.savedEffects = savedContextEffects;

View File

@ -10,6 +10,7 @@
/* @flow */
import { GlobalEnvironmentRecord, DeclarativeEnvironmentRecord } from "../environment.js";
import { FatalError } from "../errors.js";
import { Realm, ExecutionContext, Tracer } from "../realm.js";
import type { Effects } from "../realm.js";
import { IsUnresolvableReference, ResolveBinding, ToStringPartial, Get } from "../methods/index.js";
@ -288,6 +289,8 @@ export class Modules {
}
return effects;
} catch (err) {
if (err instanceof FatalError) return undefined;
} finally {
realm.popContext(context);
this.delayUnsupportedRequires = oldDelayUnsupportedRequires;
@ -305,7 +308,7 @@ export class Modules {
moduleId,
`Speculative initialization of module ${moduleId}`);
if (effects === undefined) break;
if (effects === undefined) continue;
let result = effects[0];
if (result instanceof IntrospectionThrowCompletion) {
invariant(result instanceof IntrospectionThrowCompletion);