mirror of
https://github.com/facebookarchive/prepack.git
synced 2024-09-20 03:27:09 +03:00
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:
parent
14e015c0ac
commit
6cb22dd91e
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
|
13
src/realm.js
13
src/realm.js
@ -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;
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user