Skip to content
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

fix pure_funcs & improve side_effects #1494

Closed
wants to merge 1 commit into from

Conversation

alexlamsl
Copy link
Collaborator

#1399 (comment)

Added some tests for pure_funcs in general while I'm at this.

/cc @kzc

}
expect: {
function f(a, b) {
console.log(a());
Copy link
Contributor

@kzc kzc Feb 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If console.log is deemed to be pure and its return value is not used, shouldn't the expected result should be the following?

        function f(a, b) {
            a();
        }

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's because AST_SimpleStatement isn't smart enough at the moment.

I'll investigate if this is the only site that needs improvement for AST_Call optimisation. Hopefully I won't end up with nasty surprises like I did in #1477.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexlamsl Could you please add a [WIP] prefix to the title while this is sorted out?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the real problem is that the concepts of "side effects" and "pure function" are used interchangeably in the code, when they are different things. As we see above, a pure function can have arguments with side effects.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That isn't an issue per se, because it makes sense when asking AST_Node.has_side_effects() to consider the expression as a whole and return true if any of its components have side effects.

Optimisation of individual side-effect-free components are scattered across compress.js, and in this particular case, I think OPT(AST_SimpleStatement) needs to be augmented as it's the site where we know the evaluated value of its body is being discarded.

@kzc
Copy link
Contributor

kzc commented Feb 17, 2017

Can you add an additional test like the following and add unused: true in the options?

function foo() {
    var u = pure(1);
    var x = pure(2);
    var y = pure(x);
    var z = pure(pure(side_effect()));
    return pure(3);
}

it ought to compress to:

function foo() {
    side_effect();
    return pure(3);
}

@alexlamsl
Copy link
Collaborator Author

@kzc added test as suggested above. Right now it will return:

function foo() {
    pure(pure(side_effects()));
    return pure(3);
}

I'm contemplating on whether to open a separate PR for the further optimisation or group it all in this one.

@alexlamsl alexlamsl changed the title fix pure_funcs with side effects arguments [WIP] fix pure_funcs with side effects arguments Feb 17, 2017
@kzc
Copy link
Contributor

kzc commented Feb 17, 2017

I'm contemplating on whether to open a separate PR for the further optimisation or group it all in this one.

Might as well put it into this one.

@kzc
Copy link
Contributor

kzc commented Feb 17, 2017

FYI, one of the most frequent uses of pure_funcs in the wild is for Babel generated _classCallCheck:

$ echo 'class Foo {}' | babel

"use strict";
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Foo = function Foo() {
  _classCallCheck(this, Foo);
};

That probably should be a test case.

@alexlamsl
Copy link
Collaborator Author

@kzc took me a while, but here it is 😅

Please check the babel test under test/compress/pure_funcs.js to see if that matches what you expect?

lib/compress.js Outdated
@@ -1729,12 +1734,131 @@ merge(Compressor.prototype, {
return self;
});

(function(def){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please put a comment above this line?

    // drop_value()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done with an extra line to try and explain what it does 😉

@@ -11,7 +11,6 @@ array: {
}
expect: {
function f(a, b) {
Math.floor(c / b);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the expected result be the following?

function f(a, b) { 
    c;
}

Since c is global then its access could throw if not defined.

$ cat foo.js
"use strict";
try {
    c;
} catch (x) {
    console.log("caught exception: ", x);
}
$ node ./foo.js
caught exception:  ReferenceError: c is not defined

Just confirming - in ECMAScript there are no "divide by zero" exceptions, correct? It would produce Infinity or NaN or whatever so no issue removing the divisions here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! I forgot the undefined error case.

Thanks for catching that!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

if (!(instance instanceof Constructor))
throw new TypeError("Cannot call a class as a function");
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove this empty line

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@kzc
Copy link
Contributor

kzc commented Feb 17, 2017

Nice job.

The babel test looks correct. Your other PR for -c toplevel could reduce it further I think?

I noted a different test result that I had issue with.

@alexlamsl
Copy link
Collaborator Author

I noted a different test result that I had issue with.

Thanks for catching that - I'm having a slow/distracted day 😓

}
expect: {
function foo() {
var b;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would have expected to see the global c here:

c;
c;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

}
}
expect: {
function f(a, b) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@kzc
Copy link
Contributor

kzc commented Feb 17, 2017

@alexlamsl The speed and quality of your code continues to impress me.

@alexlamsl
Copy link
Collaborator Author

alexlamsl commented Feb 17, 2017

The babel test looks correct. Your other PR for -c toplevel could reduce it further I think?

I lumped this PR on top of #1485 locally to test this theory. For starters, even without any extra options, function Foo() { got turned into function() {.

With toplevel on, I just get a blank output 😏

(Conflicts with #1448 and #1450)

@alexlamsl
Copy link
Collaborator Author

test/jetstream.js with default options passed.

@alexlamsl
Copy link
Collaborator Author

Run into a curious issue with math.js where negate_iife is being undone - investigating.

@alexlamsl
Copy link
Collaborator Author

Employed similar logic as #1451 to fix it.

@kzc
Copy link
Contributor

kzc commented Feb 17, 2017

These unmerged PRs are completely inter-tangled now.

@kzc
Copy link
Contributor

kzc commented Feb 18, 2017

Bug:

$ echo 'pure(a() ? 4 : 5)' | bin/uglifyjs -c --pure-funcs pure
WARN: Retaining side-effect statement [-:1,0]
undefined:3121
        self.right = self.right.transform(tw);
                               ^

TypeError: Cannot read property 'transform' of null

I think a distinct test is needed for every node type listed in drop_value().

lib/compress.js Outdated
return null;
}

function trim(nodes, compressor, first_in_statement) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please comment what this function does and what it returns?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@kzc
Copy link
Contributor

kzc commented Feb 18, 2017

This warning is not needed - it is too common:

$ echo 'x() && y()' | bin/uglifyjs -c
WARN: Retaining side-effect statement [-:1,0]
x()&&y();

@kzc
Copy link
Contributor

kzc commented Feb 18, 2017

Not optimal:

$ echo '(1, [2, f()], 3, {a:1, b:g()});' | bin/uglifyjs -c
[2,f()],{a:1,b:g()};

should be:

f(),g();

@alexlamsl
Copy link
Collaborator Author

This warning is not needed - it is too common:

Done.

@alexlamsl
Copy link
Collaborator Author

Not optimal: (1, [2, f()], 3, {a:1, b:g()});

Oh, I was hoping OPT(AST_Seq) would pick up the slack of that case. I'll investigate.

@alexlamsl
Copy link
Collaborator Author

Bug: pure(a() ? 4 : 5)

Fixed.

lib/compress.js Outdated
@@ -1745,6 +1745,7 @@ merge(Compressor.prototype, {
return null;
}

// drop side-effect-free elements an array of expressions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Drop side-effect-free elements from an array of expressions.
// Returns an array of expressions with side-effects or null
// if all elements were dropped. Note: original array may be
// returned if nothing changed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated - thanks! 👍

@kzc
Copy link
Contributor

kzc commented Feb 18, 2017

Even without pure functions, the AST_SimpleStatement drop_value() optimization seems to be a nice win.

@alexlamsl
Copy link
Collaborator Author

Not optimal: (1, [2, f()], 3, {a:1, b:g()});

Improved.

Even without pure functions, the AST_SimpleStatement drop_value() optimization seems to be a nice win.

This is similar to what I wanted to do in #1451 (comment)

lib/compress.js Outdated
});
});
})(function(node, func){
node.DEFMETHOD("drop_value", func);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it bother you to change the name of the function to something more descriptive like drop_irrelevant or drop_side_effect_free?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop_side_effect_free sounds good as it reduces the vocabulary surface area 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@kzc
Copy link
Contributor

kzc commented Feb 18, 2017

(1, [2, f()], 3, {a:1, b:g()});

Would you mind adding that under a new sequences test? It shows multi-level simplification.

@alexlamsl
Copy link
Collaborator Author

Would you mind adding that under a new sequences test?

It is added under test/compress/drop-unused.js - or do you want me to rename the test case?

@kzc
Copy link
Contributor

kzc commented Feb 18, 2017

It is added under test/compress/drop-unused.js - or do you want me to rename the test case?

I must have missed it - it's fine. Thanks.

(1, [2, foo()], 3, {a:1, b:bar()});
}
expect: {
foo(), bar();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 super_awesome++

@kzc
Copy link
Contributor

kzc commented Feb 18, 2017

This PR alone shaved 4,201 bytes off a 2MB app bundle I had kicking around. Not bad at all.

@alexlamsl
Copy link
Collaborator Author

test/benchmark.js

CodeTimeSize
 master#1494master#1494
jquery-3.1.1.js0.788s0.836sidentical
angular.js1.175s1.174s174,174174,175
math.js17.442s17.792s470,543470,528
bootstrap.js0.220s0.235s36,92036,909
react.js1.202s1.297s207,922207,339
ember.prod.js2.354s2.463s530,719530,714
lodash.js0.720s0.685sidentical
d3.js1.590s1.653sidentical

@alexlamsl
Copy link
Collaborator Author

Oh Shoo—

Fatal Windows exception, code 0xc0000005.
PhantomJS has crashed. Please read the bug reporting guide at
<http://phantomjs.org/bug-reporting.html> and file a bug report.
Error: JetStream failed!
   at Anonymous function (UglifyJS2\test\jetstream.js:50:23)
   at emitTwo (events.js:106:5)
   at emit (events.js:191:7)
   at _handle.onexit (internal/child_process.js:215:7)

@alexlamsl
Copy link
Collaborator Author

Seems like this PR is interfering with OutputStream(options.max_line_len). The reason why I suspect that is:

@alexlamsl
Copy link
Collaborator Author

node test/jetstream.js -b beautify=false,max_line_len=16000 -mc warnings=false completed successfully.

@alexlamsl alexlamsl mentioned this pull request Feb 18, 2017
@alexlamsl alexlamsl changed the title [WIP] fix pure_funcs with side effects arguments fix pure_funcs & improve side_effects Feb 18, 2017
alexlamsl added a commit to alexlamsl/UglifyJS that referenced this pull request Feb 18, 2017
- only drops side-effect-free arguments
- drop side-effect-free parts with discarded value from `AST_Seq` & `AST_SimpleStatement`

closes mishoo#1494
@kzc kzc mentioned this pull request Feb 18, 2017
- only drops side-effect-free arguments
- drop side-effect-free parts with discarded value from `AST_Seq` & `AST_SimpleStatement`
operator: "||",
left: this.condition,
right: alternative
}) : this.condition.drop_side_effect_free(compressor);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

condition was returned without optimised for side-effects before

});
var node = this.clone();
node.consequent = consequent;
node.alternative = alternative;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were accidentally modifying this instead of node before.

}
}

conditional: {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests beefed up to cover aforementioned changes.

alexlamsl added a commit to alexlamsl/UglifyJS that referenced this pull request Feb 19, 2017
- only drops side-effect-free arguments
- drop side-effect-free parts with discarded value from `AST_Seq` & `AST_SimpleStatement`

closes mishoo#1494
@alexlamsl alexlamsl closed this in 26fbeec Feb 23, 2017
@alexlamsl alexlamsl deleted the pure_funcs branch February 24, 2017 00:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants