-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
enhance collapse_vars
#1862
enhance collapse_vars
#1862
Conversation
Do you still run the fuzzer from time to time? Not clear if it'd catch problems in optimizations like this though. |
Haven't started running This thing is actually quite scary, for instance it can reach within the first |
x = i += 2, | ||
y = i += 3; | ||
log(x, i += 4, y, i); | ||
console.log.bind(console)(x, i += 4, y, i); |
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.
While you and I can deduce this optimization is correct, how can collapse_vars
know that the side effects in console.log.bind
do not interact with the modification of i
above?
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 does this:
- begin scanning on
log
- collect all assignments within the value (none in this case, but if say
log = i = 10
it would have markedi
down) - realise
console.log.bind(console)
may have side-effects - every time it hits
AST_SymbolRef
(i
in this case), make sure all references toi
are within this scope, i.e.f4()
So collapse_vars
move past each i
safely because it knows whatever side-effects console.log.bin(console)
may have, it can't possibly modify i
which is exclusively modified within f4()
.
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.
Thanks for confirming that.
return { | ||
get b() { return 7; }, | ||
r: z | ||
r: x + y |
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 in collapse_vars prevented this particular optimization before? I don't see any side effects above it.
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.
AST_Accessor.has_side_effects()
on get b() { return 7; }
, which aborts the scan previously.
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.
Ah - of course! Thanks.
That was certainly not the case in 2.7.5 at least. I explicitly rejected such nodes in |
Yeah - I basically wrote another version in parallel to test between the two back and forth. |
I guess I should put that |
In the first |
Even |
Here's a list of cases that would terminate the scan:
|
Perhaps should add |
I'll confess I never used that feature of JavaScript before... 😅 Better safe than sorry, indeed. |
OT: TIL |
That's whack, but it sort of makes of makes sense from a JS interpreter point of view. It doesn't consider the implications of the expression until the subscript is evaluated. Keep in mind that the expression is still evaluated before the subscript:
|
lib/compress.js
Outdated
@@ -677,7 +677,7 @@ merge(Compressor.prototype, { | |||
|| node instanceof AST_IterationStatement && !(node instanceof AST_For) | |||
|| node instanceof AST_SymbolRef | |||
&& (node.undeclared() | |||
|| lvalues && node.name in lvalues | |||
|| lvalues && lvalues[node.name] |
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 may be more readable, but it's slower because the value needs to be evaluated in a boolean context rather than merely checking for key existence.
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 not an optimisation - I do need the boolean value here. lvalues
stores true
if variable is modified (same as before), but false
when the variable is read-only.
This is to facilitate the use case below when dealing with var a=b; return b++ + a
where the modifying case is encountered during scanning as opposed to the assigned value. This case used to be taken care of by the post-order has_side_effects()
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.
My mistake. I did not realize you changed its use.
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.
No worries - always good to make sure we know what I'm doing 😉
OT: Node.js oddity of the day $ node
> !function f(){console.log(f, f ^= 0, f)}()
[Function: f] 0 [Function: f]
true
> !function f(){console.log(typeof f, f ^= 0, f)}()
function 0 function f(){console.log(typeof f , f ^= 0, f)}
true So in that sense, |
How a JS engine prints out a function is a crap shoot in the best of circumstances. |
Right, I'll leave |
Every sub-domain of angularjs.org is down at the moment - will restart CI jobs once they sort themselves out. |
Where did all the comments in the collapse_vars implementation go? No matter - it's beyond my ability to understand it at this point. Please fuzz this PR for a few hundred CPU hours since it's bound to have unforeseen issues. |
They went the same way as
Any suggestions to improve readability? 😅
The last few commits are bugs found by |
Issues fixed so far are discovered around 10~30kFuzz. |
On the comment front all I can suggest is for you to ask yourself the question - what would someone unfamiliar with the project want to know about particularly dense sections of code? And answer those questions with brief comments just to say where you're going. Clearly you don't need comments to understand the code, but mortals like the rest of us do. :-) Before I forget, there was one other section of code unrelated to this PR in which variables were renamed in a confusing manner: var name = d.name.length;
var overhead = 0;
if (compressor.option("unused") && (!d.global || compressor.toplevel(d))) {
overhead = (name + 2 + value) / d.references.length;
}
d.should_replace = value <= name + overhead ? fn : false;
I'm going to take this opportunity to step back from this project since it's in very capable hands. I think it's never been in better shape. You've whittled the Issues down from a dozen pages to just 4. With the upcoming 3.x release you've simplified the interface and made it more consistent. In the event you need a second opinion on some issue, feel free to @kzc me. |
Thanks for all the help so far - much appreciated 👍
Will do.
I'd be one of said mortals when I come back to read that code in a few weeks' time, so better write something down now before I regret it. 😅 |
Wrote a summary and added short descriptions to cut up the main code section in 6c3a7b0 |
- extend expression types - `a++` - `a=x;` - extend scan range - `for(init;;);` - `switch(expr){case expr:}` - `a = x; a = a || y;` - terminate upon `debugger;` fixes mishoo#27 fixes mishoo#1858
@alexlamsl I haven't benchmarked anything on master in a while, but going forward if you could keep an eye on the default With default options Uglify users have grown to expect simplicity, decent minification and speed - otherwise they'd be using one of the other minifiers. Thanks again and good luck with the 3.x release! |
var s = "" + function(){...};
for (var a in s) {
// this will loop through every character of the string `s`, so `c` is going to be dependent on the function body
c = 1 + c;
} And all hell broke loose for that false positive of failed test case 😓 |
…yJS@5a25d24b5 This is the commit right before mishoo/UglifyJS#1862 landed
@alexlamsl, I've run into a problem with a web app (the Calendar part of the One.com Webmail suite), and several levels of bisecting landed me here:
The symptom in the application is that recurring calendar events start appearing in the wrong places, so it seems like this transformation doesn't preserve the semantics of the code, so the math comes out wrong. Here's a diff between the output of uglify-js before and after this change landed: papandreou/uglifyproblem@7cffa47 -- I'll try to skim through it to see if I can spot the error myself, but help would be appreciated :) |
Further debugging indicates that it's this change that causes the application to break: diff --git a/http-pub-universal/static/calendarfrontend.js b/http-pub-universal/static/calendarfrontend.js
index e948030d2..e5ad03505 100644
--- a/http-pub-universal/static/calendarfrontend.js
+++ b/http-pub-universal/static/calendarfrontend.js
@@ -20585,7 +20585,7 @@ _gaq.push([ "_trackPageLoadTime" ]), _gaq.push([ "_gat._anonymizeIp" ]), functio
increment_monthday: function(inc) {
for (var i = 0; i < inc; i++) {
var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
- this.last.day++, this.last.day > daysInMonth && (this.last.day -= daysInMonth, this.increment_month());
+ ++this.last.day > daysInMonth && (this.last.day -= daysInMonth, this.increment_month());
}
},
increment_month: function() { |
The original code looks like this: increment_monthday: function increment_monthday(inc) {
for (var i = 0; i < inc; i++) {
var daysInMonth = ICAL.Time.daysInMonth(this.last.month, this.last.year);
this.last.day++;
if (this.last.day > daysInMonth) {
this.last.day -= daysInMonth;
this.increment_month();
}
}
}, |
@papandreou your example seems to work as intended: $ cat test.js
var obj = {
last: {
day: 0
},
increment_month: function() {
console.log("next month");
},
inc: function(inc) {
for (var i = 0; i < inc; i++) {
var daysInMonth = 28;
this.last.day++;
if (this.last.day > daysInMonth) {
this.last.day -= daysInMonth;
this.increment_month();
}
}
}
};
obj.inc(100);
console.log(obj.last.day); $ uglifyjs test.js -cb bracketize
var obj = {
last: {
day: 0
},
increment_month: function() {
console.log("next month");
},
inc: function(inc) {
for (var i = 0; i < inc; i++) {
++this.last.day > 28 && (this.last.day -= 28, this.increment_month());
}
}
};
obj.inc(100), console.log(obj.last.day); $ cat test.js | node
next month
next month
next month
16
$ uglifyjs test.js -cb bracketize | node
next month
next month
next month
16 Please narrow the issue down to an reproducible case, then file a new report (thanks in advance for your efforts!) |
Ah, it seems to happen because |
This outputs var foo = { _bar: 0 };
Object.defineProperty(foo, 'bar', {
get: function () {
this._bar += 1;
return this._bar;
},
set: function (bar) {
this._bar = bar;
}
});
foo.bar++;
if (foo.bar === 3) {
console.log('yay');
} |
@papandreou Please open a new ticket for this issue. |
a++
a=x;
for(init;;);
switch(expr){case expr:}
a = x; a = a || y;
debugger;
fixes #27
fixes #1858
Another break-out of #1821, as mentioned in #1850 (comment)
I have managed not to overlap with
cascade
orreduce_vars
too much, and they should not interfere with each other too badly. This can be viewed ascascade
but across multipleAST_Statement
s instead of within a singleAST_Sequence
, with the additional support forAST_VarDef
(since you can't have that withinAST_Sequence
anyway).