-
Notifications
You must be signed in to change notification settings - Fork 425
Provide a basic recovery mechanism for for
and while
loops
#2118
Conversation
src/evaluators/ForStatement.js
Outdated
|
||
if (functionInfo.usesThis) { | ||
let thisRef = env.evaluate(t.thisExpression(), strictCode); | ||
if (thisRef instanceof Value) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And if it's not a value, why is it okay to just ignore it?
// ECMA262 13.7.4.7 | ||
export default function( | ||
ast: BabelNodeForStatement, | ||
strictCode: boolean, | ||
env: LexicalEnvironment, | ||
realm: Realm, | ||
labelSet: ?Array<string> | ||
): Value { | ||
if (realm.isInPureScope()) { | ||
return tryToEvaluateForStatementOrLeaveAsAbstract(ast, strictCode, env, realm, labelSet); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a change request, just a thought: There's a growing set of bail-outs from just regular JavaScript code. It would be good to have some kind of logging or account for when these things happen, maybe combined with some configuration as to what kind of things are allowed. (We don't want to allow bailing out for loops or arbitrary functions in InstantRender, and I guess eventually you don't want that for the React Compiler either.) Also possibly just to give de-optimization hints to users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the React Compiler this okay (for now), long term we do want to support loops fully as I stated in my summary. Maybe if you have some good ideas on how to configure such a thing, you could make an issue for it with some suggestions and we could implement it – as you're right, we definitely can't do this for IR.
On a side note though, we might be able to support parts of this for the React Compiler. When we create this wrapper function, we can also check to see if the for loop is pure or has few side-effects and contain the side-effects in the wrapper function, then create another function for the loop body, then tell Prepack to optimize the inner function for the body. I thought of doing that as part of this PR, but it would really convolute the contents for now. It's best as a follow up PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great!
src/evaluators/ForStatement.js
Outdated
if (functionInfo.usesReturn || functionInfo.usesArguments || functionInfo.usesGotoToLabel) { | ||
// We do not have support for these yet | ||
// TODO: issue number upon creation of issue | ||
throw new FatalError("TODO: #? handle more loop bail out cases"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should never throw a FatalError unless you have first reported a diagnostic message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It’s a todo. Once everyone is happy I’ll change the todo into a proper fatal
src/evaluators/ForStatement.js
Outdated
): AbstractValue { | ||
let wrapperFunction = new ECMAScriptSourceFunctionValue(realm); | ||
let body = t.blockStatement([ast]); | ||
((body: any): FunctionBodyAstNode).uniqueOrderedTag = realm.functionBodyUniqueTagSeed++; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not for this PR, but we really should encapsulate this in a helper.
src/evaluators/ForStatement.js
Outdated
if (functionInfo.usesThis) { | ||
let thisRef = env.evaluate(t.thisExpression(), strictCode); | ||
let thisVal = Environment.GetValue(realm, thisRef); | ||
Havoc.value(realm, thisVal); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems a bit like papering over a huge crack in the wall. The loop can modify many other things in a way that is totally opaque to Prepack. Yet, only the this value is havoced. Better than nothing, but not much better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Havocing is transitive. So everything reachable gets havoced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loop body may access locations that are not reachable from this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What locations are you referring to? The havoc logic recursively goes through every binding and object in the AST of the loop and its body. If something isn't being reached, we need to fix it in the havoc logic. If you mean the loop body is accessing something Prepack doesn't know about, then this isn't safe for pure scope.
src/evaluators/ForStatement.js
Outdated
args.push(thisVal); | ||
} | ||
|
||
Havoc.value(realm, wrapperFunction); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment to explain why the wrapper needs to get havoced? Preferably, there should be a test that fails if it is not havoced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The wrapper references all relevant bindings, which in turn get havoced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which answers my previous question. It would be good to point this out in the actual code. It is really not that obvious.
src/evaluators/ForStatement.js
Outdated
wrapperFunction.$ECMAScriptCode = body; | ||
wrapperFunction.$FormalParameters = []; | ||
wrapperFunction.$Environment = env; | ||
// We need to scan to AST looking for "this", "return", labels and "arguments" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not throw?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Throw where and with what? Sorry, not sure I get what you mean here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the loop body throws, it terminates early. That seems similar to a loop body with a return in it. I don't see why you check for the latter, but not the former.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand now. Thanks, added that case.
src/evaluators/ForStatement.js
Outdated
// TODO: issue number upon creation of issue | ||
throw new FatalError("TODO: #? handle more loop bail out cases"); | ||
let diagnostic = new CompilerDiagnostic( | ||
`"failed to recover from a for/while loop bail-out due to unsupported logic in loop body`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is with the starting "?
src/evaluators/ForStatement.js
Outdated
if (functionInfo.usesThis) { | ||
let thisRef = env.evaluate(t.thisExpression(), strictCode); | ||
let thisVal = Environment.GetValue(realm, thisRef); | ||
Havoc.value(realm, thisVal); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loop body may access locations that are not reachable from this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to see some comments before you land. Fix up the funky string. Perhaps actually do something about loop bodies with a throw. (At the very least comment in the code why not.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@trueadm is landing this pull request. If you are a Facebook employee, you can view this diff on Phabricator.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens when there's a var
declaration inside the loop and the value is access outside the loop?
for (...) {
var item = ...;
}
return item;
The var
has to be stripped and treated as part of the outer environment. Can you add a test?
@sebmarkbage I was just looking into the exact problem. :) Thanks for the example too. |
This comment has been minimized.
This comment has been minimized.
I'm seeing one last issue with this PR and leaked bindings. Will try and make a repro for it. |
Here is a reduced repro: (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"));
}
})(); |
@NTillmann Please can you look at this failing test? I've attached the same test to this PR, so you can check it out locally and run the serializer test to see the same failure. It looks like we're close, just this last failing lazy binding case to solve. |
Looking at it... |
Figured out the (or at least a) root cause of why this is not working. See here: #2153. It's about Prepack and Dates, and likely not related to what you are doing in this PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@trueadm is landing this pull request. If you are a Facebook employee, you can view this diff on Phabricator.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@trueadm is landing this pull request. If you are a Facebook employee, you can view this diff on Phabricator.
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
andwhile
loops (as they go through the same code path). We already have some basic evaluation fordo while
loops, but trying to adapt that code to work with the failing test case (#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 ofreturn
,arguments
and labelledbreak/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:
This serializes out to:
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