Provide a basic recovery mechanism for for and while loops (#2118)

Summary:
Release notes: standard for loops and while loops have a recovery mechanism in pure scope

This PR provides a bail-out recovery mechanism in pure scope for FatalErrors thrown and caught from for/while loops. Until now, if a FatalError occurs from trying to evaluate abstract for/while loops, we'd have to recover at a higher point in the callstack, which wasn't always possible (the loop may be at the root of a function/component).

The ideal long-term strategy is to properly model out the different cases for loops, but this is a complex and time-consuming process. This PR adds a recovery mechanism that serializes out the original for loop, but within a newly created wrapper function containing the loop logic and a function call to that newly created function wrapper. This allows us to still run the same code at runtime, where we were unable to evaluate and optimize it at build time.

For now, this PR only adds recovery support for standard `for` and `while` loops (as they go through the same code path). We already have some basic evaluation for `do while` loops, but trying to adapt that code to work with the failing test case (https://github.com/facebook/prepack/issues/2055) didn't work for me – we have so many strange problems to deal with first before we can properly handle that issue.

In cases where the loop uses `this`, the context is found and correctly called with the wrapper function. In cases of usage of `return`, `arguments` and labelled `break/continue` we bail out again as this is not currently supported in the scope of this PR (can be added in a follow up PR, but I wanted to keep the scope of this PR limited).

For example, take this failing case on master:

```js
function fn(props, splitPoint) {
  var text = props.text || "";

  text = text.replace(/\s*$/, "");

  if (splitPoint !== null) {
    while (text[splitPoint - 1] === "\n") {
      splitPoint--;
    }
  }
  return splitPoint;
}
```

This serializes out to:

```js
  var _0 = function (props, splitPoint) {
    var __scope_0 = new Array(1);

    var __get_scope_binding_0 = function (__selector) {
      var __captured;

      switch (__selector) {
        case 0:
          __captured = [_G, _E];
          break;
      }

      __scope_0[__selector] = __captured;
      return __captured;
    };

    var _C = function () {
      var __captured__scope_1 = __scope_0[0] || __get_scope_binding_0(0);

      for (; __captured__scope_1[1][__captured__scope_1[0] - 1] === "\n";) {
        __captured__scope_1[0]--;
      }
    };

    var _$0 = props.text;

    var _$2 = (_$0 || "").replace(/\s*$/, "");

    var _6 = splitPoint !== null;

    var _G = _6 ? void 0 : splitPoint;

    var _E = _6 ? void 0 : _$2;

    if (_6) {
      (__scope_0[0] || __get_scope_binding_0(0))[0] = splitPoint;
      (__scope_0[0] || __get_scope_binding_0(0))[1] = _$2;

      var _$5 = _C();
    }

    var _$6 = (__scope_0[0] || __get_scope_binding_0(0))[0];

    return _$6;
  };
```

Furthermore, an idea that might be a good follow up PR would be to break the wrapper function into two functions, depending on some heuristics. If we can detect that the loop body does not have any unsupported side-effects like writing to variables that are havoced etc, we can then tell Prepack to optimize the inner function wrapper for the loop body. That way, at least some parts of the loop get optimized (the outer bindings will be havoced before this point, so it should work okay). I think I suggested this in person with hermanventer and sebmarkbage in Seattle as a potential way to optimize complex loops that we can't compute the fixed point for right now.

Fixes #2055
Closes https://github.com/facebook/prepack/pull/2118

Differential Revision: D8410341

Pulled By: trueadm

fbshipit-source-id: ee7e6b1bc1feadf0c924e4f82506ca32ca1dadc9
This commit is contained in:
Dominic Gannaway 2018-06-25 11:08:18 -07:00 committed by Facebook Github Bot
parent 38392a5499
commit 5a5a100483
19 changed files with 589 additions and 4 deletions

View File

@ -11,7 +11,7 @@
import type { LexicalEnvironment } from "../environment.js";
import type { Realm } from "../realm.js";
import { Value, EmptyValue } from "../values/index.js";
import { AbstractValue, Value, EmptyValue, ECMAScriptSourceFunctionValue } from "../values/index.js";
import {
AbruptCompletion,
BreakCompletion,
@ -20,14 +20,29 @@ import {
ForkedAbruptCompletion,
PossiblyNormalCompletion,
ReturnCompletion,
SimpleNormalCompletion,
ThrowCompletion,
} from "../completions.js";
import traverse from "babel-traverse";
import type { BabelTraversePath } from "babel-traverse";
import { TypesDomain, ValuesDomain } from "../domains/index.js";
import { CompilerDiagnostic, FatalError } from "../errors.js";
import { UpdateEmpty } from "../methods/index.js";
import { LoopContinues, InternalGetResultValue, TryToApplyEffectsOfJoiningBranches } from "./ForOfStatement.js";
import { Environment, Functions, Join, To } from "../singletons.js";
import { Environment, Functions, Havoc, Join, To } from "../singletons.js";
import invariant from "../invariant.js";
import type { BabelNodeForStatement } from "babel-types";
import * as t from "babel-types";
import type { FunctionBodyAstNode } from "../types.js";
import type { BabelNodeExpression, BabelNodeForStatement, BabelNodeBlockStatement } from "babel-types";
type BailOutWrapperInfo = {
usesArguments: boolean,
usesThis: boolean,
usesReturn: boolean,
usesGotoToLabel: boolean,
usesThrow: boolean,
varPatternUnsupported: boolean,
};
// ECMA262 13.7.4.9
export function CreatePerIterationEnvironment(realm: Realm, perIterationBindings: Array<string>) {
@ -268,6 +283,194 @@ function ForBodyEvaluation(
}
}
let BailOutWrapperClosureRefVisitor = {
ReferencedIdentifier(path: BabelTraversePath, state: BailOutWrapperInfo) {
if (path.node.name === "arguments") {
state.usesArguments = true;
}
},
ThisExpression(path: BabelTraversePath, state: BailOutWrapperInfo) {
state.usesThis = true;
},
"BreakStatement|ContinueStatement"(path: BabelTraversePath, state: BailOutWrapperInfo) {
if (path.node.label !== null) {
state.usesGotoToLabel = true;
}
},
ReturnStatement(path: BabelTraversePath, state: BailOutWrapperInfo) {
state.usesReturn = true;
},
ThrowStatement(path: BabelTraversePath, state: BailOutWrapperInfo) {
state.usesThrow = true;
},
VariableDeclaration(path: BabelTraversePath, state: BailOutWrapperInfo) {
let node = path.node;
// If our parent is a for loop (there are 3 kinds) we do not need a wrapper
// i.e. for (var x of y) for (var x in y) for (var x; x < y; x++)
let needsExpressionWrapper =
!t.isForStatement(path.parentPath.node) &&
!t.isForOfStatement(path.parentPath.node) &&
!t.isForInStatement(path.parentPath.node);
const getConvertedDeclarator = index => {
let { id, init } = node.declarations[index];
if (t.isIdentifier(id)) {
return t.assignmentExpression("=", id, init);
} else {
// We do not currently support ObjectPattern, SpreadPattern and ArrayPattern
// see: https://github.com/babel/babylon/blob/master/ast/spec.md#patterns
state.varPatternUnsupported = true;
}
};
if (node.kind === "var") {
if (node.declarations.length === 1) {
let convertedNodeOrUndefined = getConvertedDeclarator(0);
if (convertedNodeOrUndefined === undefined) {
// Do not continue as we don't support this
return;
}
path.replaceWith(
needsExpressionWrapper ? t.expressionStatement(convertedNodeOrUndefined) : convertedNodeOrUndefined
);
} else {
// convert to sequence, so: `var x = 1, y = 2;` becomes `x = 1, y = 2;`
let expressions = [];
for (let i = 0; i < node.declarations.length; i++) {
let convertedNodeOrUndefined = getConvertedDeclarator(i);
if (convertedNodeOrUndefined === undefined) {
// Do not continue as we don't support this
return;
}
expressions.push(convertedNodeOrUndefined);
}
let sequenceExpression = t.sequenceExpression(((expressions: any): Array<BabelNodeExpression>));
path.replaceWith(needsExpressionWrapper ? t.expressionStatement(sequenceExpression) : sequenceExpression);
}
}
},
};
function generateRuntimeForStatement(
ast: BabelNodeForStatement,
strictCode: boolean,
env: LexicalEnvironment,
realm: Realm,
labelSet: ?Array<string>
): AbstractValue {
let wrapperFunction = new ECMAScriptSourceFunctionValue(realm);
let body = ((t.cloneDeep(t.blockStatement([ast])): any): BabelNodeBlockStatement);
((body: any): FunctionBodyAstNode).uniqueOrderedTag = realm.functionBodyUniqueTagSeed++;
wrapperFunction.$ECMAScriptCode = body;
wrapperFunction.$FormalParameters = [];
wrapperFunction.$Environment = env;
// We need to scan to AST looking for "this", "return", "throw", labels and "arguments"
let functionInfo = {
usesArguments: false,
usesThis: false,
usesReturn: false,
usesGotoToLabel: false,
usesThrow: false,
varPatternUnsupported: false,
};
traverse(
t.file(t.program([t.expressionStatement(t.functionExpression(null, [], body))])),
BailOutWrapperClosureRefVisitor,
null,
functionInfo
);
traverse.clearCache();
let { usesReturn, usesThrow, usesArguments, usesGotoToLabel, varPatternUnsupported, usesThis } = functionInfo;
if (usesReturn || usesThrow || usesArguments || usesGotoToLabel || varPatternUnsupported) {
// We do not have support for these yet
let diagnostic = new CompilerDiagnostic(
`failed to recover from a for/while loop bail-out due to unsupported logic in loop body`,
realm.currentLocation,
"PP0037",
"FatalError"
);
realm.handleError(diagnostic);
throw new FatalError();
}
let args = [wrapperFunction];
if (usesThis) {
let thisRef = env.evaluate(t.thisExpression(), strictCode);
let thisVal = Environment.GetValue(realm, thisRef);
Havoc.value(realm, thisVal);
args.push(thisVal);
}
// We havoc the wrapping function value, which in turn invokes the havocing
// logic which is transitive. The havocing logic should recursively visit
// all bindings/objects in the loop and its body and mark the associated
// bindings/objects that do havoc appropiately.
Havoc.value(realm, wrapperFunction);
let wrapperValue = AbstractValue.createTemporalFromBuildFunction(
realm,
Value,
args,
([func, thisExpr]) =>
usesThis
? t.callExpression(t.memberExpression(func, t.identifier("call")), [thisExpr])
: t.callExpression(func, [])
);
invariant(wrapperValue instanceof AbstractValue);
return wrapperValue;
}
function tryToEvaluateForStatementOrLeaveAsAbstract(
ast: BabelNodeForStatement,
strictCode: boolean,
env: LexicalEnvironment,
realm: Realm,
labelSet: ?Array<string>
): Value {
let effects;
let savedSuppressDiagnostics = realm.suppressDiagnostics;
try {
realm.suppressDiagnostics = true;
effects = realm.evaluateForEffects(
() => evaluateForStatement(ast, strictCode, env, realm, labelSet),
undefined,
"tryToEvaluateForStatementOrLeaveAsAbstract"
);
} catch (error) {
if (error instanceof FatalError) {
realm.suppressDiagnostics = savedSuppressDiagnostics;
return realm.evaluateWithPossibleThrowCompletion(
() => generateRuntimeForStatement(ast, strictCode, env, realm, labelSet),
TypesDomain.topVal,
ValuesDomain.topVal
);
} else {
throw error;
}
} finally {
realm.suppressDiagnostics = savedSuppressDiagnostics;
}
// Note that the effects of (non joining) abrupt branches are not included
// in effects, but are tracked separately inside completion.
realm.applyEffects(effects);
let completion = effects.result;
if (completion instanceof PossiblyNormalCompletion) {
// in this case one of the branches may complete abruptly, which means that
// not all control flow branches join into one flow at this point.
// Consequently we have to continue tracking changes until the point where
// all the branches come together into one.
completion = realm.composeWithSavedCompletion(completion);
}
// return or throw completion
if (completion instanceof AbruptCompletion) throw completion;
if (completion instanceof SimpleNormalCompletion) completion = completion.value;
invariant(completion instanceof Value);
return completion;
}
// ECMA262 13.7.4.7
export default function(
ast: BabelNodeForStatement,
@ -275,6 +478,20 @@ export default function(
env: LexicalEnvironment,
realm: Realm,
labelSet: ?Array<string>
): Value {
if (realm.isInPureScope()) {
return tryToEvaluateForStatementOrLeaveAsAbstract(ast, strictCode, env, realm, labelSet);
} else {
return evaluateForStatement(ast, strictCode, env, realm, labelSet);
}
}
function evaluateForStatement(
ast: BabelNodeForStatement,
strictCode: boolean,
env: LexicalEnvironment,
realm: Realm,
labelSet: ?Array<string>
): Value {
let { init, test, update, body } = ast;

View File

@ -11,7 +11,7 @@
import { Realm, Effects } from "../realm.js";
import { ValuesDomain } from "../domains/index.js";
import { AbruptCompletion, PossiblyNormalCompletion } from "../completions.js";
import { AbruptCompletion, PossiblyNormalCompletion, SimpleNormalCompletion } from "../completions.js";
import type { BabelNode, BabelNodeJSXIdentifier, BabelNodeExpression } from "babel-types";
import { parseExpression } from "babylon";
import {
@ -843,6 +843,7 @@ export function getValueFromFunctionCall(
}
// return or throw completion
if (completion instanceof AbruptCompletion) throw completion;
if (completion instanceof SimpleNormalCompletion) completion = completion.value;
invariant(completion instanceof Value);
return completion;
}
@ -1101,6 +1102,7 @@ export function applyObjectAssignConfigsForReactElement(realm: Realm, to: Object
}
// return or throw completion
if (completion instanceof AbruptCompletion) throw completion;
if (completion instanceof SimpleNormalCompletion) completion = completion.value;
};
if (realm.isInPureScope()) {

View File

@ -0,0 +1,18 @@
function fn(props, splitPoint) {
var text = props.text || "";
text = text.replace(/\s*$/, "");
if (splitPoint !== null) {
while (text[splitPoint - 1] === "\n") {
splitPoint--;
}
}
return splitPoint;
}
global.__optimize && __optimize(fn);
inspect = function() {
return fn({text: "foo\nfoo"}, 5);
};

View File

@ -0,0 +1,15 @@
function fn(x, counter) {
var i = 0;
for (; i !== x;) {
counter.x++;
i++;
var val1 = counter.x, val2 = val1 + 1;
}
return val2;
}
global.__optimize && __optimize(fn);
inspect = function() {
return fn(100, {x: 2});
};

View File

@ -0,0 +1,16 @@
function fn(x, counter) {
var i = 0;
var val2 = undefined;
for (; i !== x;) {
counter.x++;
i++;
val2 = i;
}
return val2;
}
global.__optimize && __optimize(fn);
inspect = function() {
return fn(100, {x: 2});
};

View File

@ -0,0 +1,17 @@
function fn(x, counter) {
var i = 0;
var val2 = undefined;
for (; i !== x;) {
var foo = {};
counter.x++;
i++;
foo.x = x;
}
return foo;
}
global.__optimize && __optimize(fn);
inspect = function() {
return fn(100, {x: 2}).x;
};

View File

@ -0,0 +1,17 @@
function fn(x, counter) {
var i = 0;
var val2 = undefined;
for (; i !== x;) {
var foo = {};
counter.x++;
i++;
foo.counter = counter;
}
return foo;
}
global.__optimize && __optimize(fn);
inspect = function() {
return fn(100, {x: 2}).counter.x;
};

View File

@ -0,0 +1,17 @@
function fn(x, counter) {
var i = 0;
var val2 = undefined;
for (; i !== x;) {
var foo = {};
counter.x++;
i++;
foo.counter = counter;
}
return foo.counter.x + 10;
}
global.__optimize && __optimize(fn);
inspect = function() {
return fn(100, {x: 2});
};

View File

@ -0,0 +1,18 @@
function fn(secondaryPattern, replaceWith, wholeNumber) {
var replaced;
while (
(replaced = wholeNumber.replace(
secondaryPattern,
replaceWith
)) != wholeNumber
) {
wholeNumber = replaced;
}
return wholeNumber;
}
global.__optimize && __optimize(fn);
inspect = function() {
return fn("1", "2", "121341");
};

View File

@ -0,0 +1,16 @@
function fn(x, total, val) {
var replaced;
let wholeNumber = val.toString();
if (x === true) {
for (var i = 0; i < total; i++) {
wholeNumber++;
}
}
return wholeNumber;
}
global.__optimize && __optimize(fn);
inspect = function() {
return fn(false, 10, 5);
};

View File

@ -0,0 +1,111 @@
(function() {
global._DateFormatConfig = {
formats: {
'l, F j, Y': 'l, F j, Y',
},
}
var DateFormatConfig = global.__abstract ? __abstract({}, "(global._DateFormatConfig)") : global._DateFormatConfig;
global.__makeSimple && __makeSimple(DateFormatConfig);
var MONTH_NAMES = void 0;
var WEEKDAY_NAMES = void 0;
var DateStrings = {
getWeekdayName: function getWeekdayName(weekday) {
if (!WEEKDAY_NAMES) {
WEEKDAY_NAMES = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
];
}
return WEEKDAY_NAMES[weekday];
},
_initializeMonthNames: function _initializeMonthNames() {
MONTH_NAMES = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
];
},
getMonthName: function getMonthName(month) {
if (!MONTH_NAMES) {
DateStrings._initializeMonthNames();
}
return MONTH_NAMES[month - 1];
},
};
function formatDate(date, format, options) {
options = options || {};
if (typeof date === "string") {
date = parseInt(date, 10);
}
if (typeof date === "number") {
date = new Date(date * 1000);
}
var localizedFormat = DateFormatConfig.formats[format];
var prefix = "getUTC";
var dateDay = date[prefix + "Date"]();
var dateDayOfWeek = date[prefix + "Day"]();
var dateMonth = date[prefix + "Month"]();
var dateYear = date[prefix + "FullYear"]();
var output = "";
for (var i = 0; i < localizedFormat.length; i++) {
var character = localizedFormat.charAt(i);
switch (character) {
case "j":
output += dateDay;
break;
case "l":
output += DateStrings.getWeekdayName(dateDayOfWeek);
break;
case "F":
case "f":
output += DateStrings.getMonthName(dateMonth + 1);
break;
case "Y":
output += dateYear;
break;
default:
output += character;
}
}
return output;
}
function fn(a, b) {
return formatDate(a, "l, F j, Y");
}
global.fn = fn;
global.__optimize && __optimize(fn);
global.inspect = function() {
return JSON.stringify(global.fn("1529579851072"));
}
})();

View File

@ -0,0 +1,14 @@
function fn(x, oldItems) {
var items = [];
for (let i = 0; i < x; i++) {
var oldItem = oldItems[i];
items.push(oldItem + 2);
}
return items;
}
global.__optimize && __optimize(fn);
inspect = function() {
return JSON.stringify(fn(5, [1,2,3,4,5]));
};

View File

@ -0,0 +1,16 @@
function Model(x, oldItems) {
this.oldItems = oldItems;
var items = [];
for (let i = 0; i < x; i++) {
var oldItem = this.oldItems[i];
items.push(oldItem + 2);
}
this.items = items;
}
global.__optimize && __optimize(Model);
inspect = function() {
var model = new Model(5, [1,2,3,4,5]);
return JSON.stringify(model.items);
};

View File

@ -0,0 +1,15 @@
function Model(x, oldItems) {
this.items = [];
for (let i = 0; i < x; i++) {
var oldItem = oldItems[i];
this.items.push(oldItem + 2);
}
}
global.__optimize && __optimize(Model);
inspect = function() {
var model = new Model(5, [1,2,3,4,5]);
return JSON.stringify(model.items);
};

View File

@ -0,0 +1,16 @@
function Model(x, oldItems) {
this.items = [];
for (let i = 0; i < 10; i++) {
for (let i = 0; i < x; i++) {
var oldItem = oldItems[i];
this.items.push(oldItem + 2);
}
}
}
global.__optimize && __optimize(Model);
inspect = function() {
var model = new Model(5, [1,2,3,4,5]);
return JSON.stringify(model.items);
};

View File

@ -0,0 +1,16 @@
function fn(x, oldItems) {
var items = [];
var i = 0;
while (i < x) {
var oldItem = oldItems[i];
items.push(oldItem + 2);
i++;
}
return items;
}
global.__optimize && __optimize(fn);
inspect = function() {
return JSON.stringify(fn(5, [1,2,3,4,5]));
};

View File

@ -0,0 +1,15 @@
function fn(x, oldItems) {
var items = [];
for (; i !== x;) {
var oldItem = oldItems[i];
items.push(oldItem + 2);
i++;
}
return items;
}
global.__optimize && __optimize(fn);
inspect = function() {
return JSON.stringify(fn(5, [1,2,3,4,5]));
};

View File

@ -0,0 +1,14 @@
function fn(x, counter) {
var i = 0;
for (; i !== x;) {
counter.x++;
i++;
}
return counter.x;
}
global.__optimize && __optimize(fn);
inspect = function() {
return fn(100, {x: 2});
};

View File

@ -0,0 +1,15 @@
function fn(x, counter) {
var i = 0;
for (; i !== x;) {
counter.x++;
i++;
var val = counter.x
}
return val;
}
global.__optimize && __optimize(fn);
inspect = function() {
return fn(100, {x: 2});
};