-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Tree shake logical expressions #2098
Conversation
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. The main thing to clarify is the indirect call handling in strict mode that may not be having an effect as far as I can tell.
Also I've been thinking about the tests and it's starting to seem really redundant to have all module format outputs for cases like this that aren't really testing module format output. .Perhaps worth thinking about a new test section that just outputs es modules that can shorten the diffs for unit testing these cases?
(0, wrapper.foo)(); | ||
(0, wrapper.foo)(); | ||
(0, wrapper.foo)(); | ||
(0, wrapper.foo)(); |
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'm not sure this is true for strict mode actually, are you sure about this? In the following case:
(function () {
"use strict";
var a = { b () { console.log(this) } };
a.b();
var c = a.b;
c();
(0, c)();
})()
the output I get is a, undefined, undefined
, not a, undefined, 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.
Not sure what you expect. If c = a.b
, then c()
and (0, c)()
will always have the same output. In non-strict mode it is the global object while in strict mode (which rollup always adds to the output) it is undefined
. The important problem is rather the following (with a
as defined in your example):
'use strict';
(0, a.b)(); // logs undefined
(true && a.b)(); // logs undefined
(true ? a.b : a.b)(); // logs undefined
(a.b)(); // logs a, so this is not equivalent to any of the above
a.b(); // logs a as well
This is one issue that is solved by this transformation. If you remove 'use strict'
, undefined
is just replaced by global
.
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.
BTW I added tests both to the form
as well as the function
section where the latter "prove" the effect of the transformation. I would suggest to play around with these tests.
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.
(Admittedly, I did not trust this myself)
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 for explaining.
src/ast/nodes/Identifier.ts
Outdated
} | ||
} | ||
|
||
if (hasBecomeCallee && name === 'eval') { |
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.
In theory eval
can be a variable name. It is probably worth adding an extra check here up the scope hierarchy to be sure it isn't.
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 think the check is isGlobalVariable
just like in CallExpression
bindNode.
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.
Usually, doing the (0, ...)
transformation does no harm to an identifier except that it is a little inefficient but you are right, we have the information available and should probably go all the way.
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, as we always have strict mode, this check is actually unnecessary, cf. here: https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Strict_mode (in "Making eval and arguments simpler")
First, the names eval and arguments can't be bound or assigned in language syntax
Just checked it myself: One cannot create a variable named eval
. So any eval
must be the global eval
and no additional check is necessary. Will add a comment to explain 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.
Ah yes strict mode reserves eval, don't know how I forgot that. Hmm in theory we shouldn't need this check in CallExpression then either... I wonder if we could remove thiis.
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.
Removed
if (this.consequent.includeInBundle()) addedNewNodes = true; | ||
if (this.alternate.includeInBundle()) addedNewNodes = true; | ||
} else if (testValue ? this.consequent.includeInBundle() : this.alternate.includeInBundle()) | ||
addedNewNodes = true; |
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've always worked with the code style that if the if
has curlies, so should the else clause.
render( | ||
code: MagicString, | ||
options: RenderOptions, | ||
{ hasBecomeCallee, hasBecomeStatement }: NodeRenderOptions = BLANK |
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.
Just wondering if we could simplify this interface and if we passed through the parent itself as a consistent third argument and then deduced these from that?
Eg a isStatementOfParent(this, parent)
check, although perhaps that is unnecessary repeated work.
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 guess some of this could be handled via a parameter renderedParent
. Might also come in handy once we start inlining expressions. Will experiment a little with this.
branchToRetain.render(code, options, { | ||
hasBecomeStatement, | ||
hasBecomeCallee: | ||
hasBecomeCallee || (isCallExpression(this.parent) && this.parent.callee === 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.
This seems to be acting on old parent information, which would then simplify to eg isCallExpression(curParent) && curParent.callee === this
?
hasBecomeStatement, | ||
hasBecomeCallee: | ||
hasBecomeCallee || (isCallExpression(this.parent) && this.parent.callee === this), | ||
hasDifferentParent: true |
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.
Similarly this would reduce to parent !== this.parent
?
|
||
// Indirectly invoked eval is executed in the global scope | ||
function testEval() { | ||
console.log((0, eval)('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.
Indirect eval still holds though of course.
Have been thinking about something like this myself for some time. Either a new section or we let the My favourite would be to not add a new section but
|
This sounds nice to me :) Folders could perhaps have an options.js default template that applies in that subfolder precedence. |
Very good idea! Thus, sub-folders can really become coherent test suites that cover certain situations with little boilerplate. Hope to find some time to look into this for the next PR (but probably not for this one just yet). |
3c8771a
to
80cfff0
Compare
Ok, this is now ready for another review. I actually took up your suggestion and made the logic now much more generic and future-proof (think: large-scale expression inlining):
I also found and fixed another bug when e.g. an object expression (which always need round brackets when rendered as a statement) becomes a statement due to tree-shaking simplifications. |
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.
Very nice to see the simplifications, glad it could work out!
Only worry here is the repeated work in lookups for parent and field information if this approach does start to scale out more. Interested to hear your thoughts on this, but this is trickier to implement I know, although you did mention you're looking into performance next, so maybe can come as part of that.
src/ast/nodes/CallExpression.ts
Outdated
import { ObjectPath } from '../values'; | ||
|
||
export function isCallExpression(node: Node | { type?: string }): node is CallExpression { |
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 to be defined but not actually used?
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.
Is removed👍
@@ -1,14 +1,17 @@ | |||
import CallOptions from '../CallOptions'; | |||
import ExecutionPathOptions from '../ExecutionPathOptions'; | |||
import SpreadElement from './SpreadElement'; | |||
import { isGlobalVariable } from '../variables/GlobalVariable'; |
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.
There seems to be one other use of this in TaggedTemplateExpression. If doing both, that actually removes all usages of this function entirely as well, although can be left to tree shaking too :)
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.
Removed
src/ast/nodes/Identifier.ts
Outdated
name === 'eval' && | ||
renderedParent && | ||
renderedParent.type === 'CallExpression' && | ||
fieldOfRenderedParent === 'callee' |
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.
Nice! Slightly shorter names might be renderParent
and renderParentField
, but perhaps these are more descriptive anyway.
let firstStart = 0, | ||
lastEnd, | ||
includedNodes = 0; | ||
for (const { node, start, end } of getCommaSeparatedNodesWithBoundaries( |
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.
Nice!
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.
When I wrote getCommaSeparatedNodesWithBoundaries
, I always hoped I could use it not only for variable declarations but also for sequence expressions. So should you decide to write your entire app using sequence expressions, you now get really nice comment handling 😉
src/utils/renderHelpers.ts
Outdated
|
||
export function childIsStatement(parent: { type?: string }) { | ||
return ( | ||
parent.type === 'Program' || parent.type === 'ExpressionStatement' // e.g. default exports rendered for side-effects only |
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 take it these will expand over time?
An alternative here might be Program.prototype.isStatementContainer = true
and ExpressionStatement.prototype.isStatementContainer = true
where NodeBase.prototype.isStatementContainer = false
.
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.
Actually not. In fact at the moment if an expression is simplified so that it becomes a statement, there will always be an expression statement left as the actual parent except for simplified default exports. Changed this now to get rid of this exception.
src/utils/renderHelpers.ts
Outdated
if (Array.isArray(value)) { | ||
for (const nestedValue of value) { | ||
if (nestedValue === child) return key; | ||
} |
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 does worry me a little - eg unnecessary lookup for long sequence expressions. This information is available to the render
call itself, so we could just enforce it being passed for all render calls getting us back to a monomorphic render function, but I know that is a lot of work and may not even be itself desirable for performance (although maybe it would...).
If I remember correctly there are only two node types with more than one array field - template elements and tagged template elements, so that knowledge could permit an optimization here, but perhaps worth seeing if it becomes a bottleneck. I just prefer to avoid unnecessary loops that is all, and the keys loop is already unnecessary in theory due to this being parent contextual information the render function could have passed.
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.
Would running Rollup on uglify code that is rewritten to use long sequence expressions hit this loop?
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.
At the moment, expression tree-shaking is powerful but very rare, and the field will only be determined if the child is actually simplified. I do not like adding this information for every render simply because it is totally unnecessary 98% of the time. The only information currently used is the fact if it is a callee so I simplified it accordingly and removed the loop. Should we require more information at some point, we can certainly revisit that.
@@ -4,7 +4,8 @@ import * as rollup from 'rollup'; | |||
import batchWarnings from './batchWarnings'; | |||
import relativeId from '../../../src/utils/relativeId'; | |||
import { handleError, stderr } from '../logging'; | |||
import { InputOptions, OutputChunk } from '../../../src/rollup/index'; | |||
import { InputOptions } from '../../../src/rollup/index'; | |||
import { Bundle } from '../../../src/rollup'; |
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.
rollup/index?
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 guess the /index
is unnecessary due to some TypeScript weirdness (which mirrors a node weirdness) 😉
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.
Ok, it seems removing the /index
does not really play nice without our setup so I'll leave it.
Posted another version to check out @guybedford. |
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.
Yeah, does seem better to use more specific render options. Can always reconsider more general ones in future too.
fix issues around simplified object expressions
* Simplify rendered parent logic to remove unnecessary checks * Fix some weird type issues
2ffea8f
to
b047228
Compare
I've just hit the (0, eval) bug in the latest SystemJS release with the Rollup build... great timing on this one :) |
This was inspired by, supersedes and closes #2007 and therefore resolves #2004.
Also resolves #2091.
Originally I wanted to collect some ideas on how to improve #2007 to get it merged before I started implementing larger refactorings to our tree-shaking logic. As it turned out, the changes necessary were a little more than I anticipated so I instead created a generic solution which should now be able to handle all combinations of the now three expressions that support tree-shaking:
a, b
)a || b
,a && b
)a ? b : c
)For now, tree-shaking will only occur for conditional and logical expressions if
a
is a literal or calculated from literals, not variables. This is usually the case when using something like https://github.com/rollup/rollup-plugin-replace to set compiler flags. However the new generic solution is designed to easily handle more generic situations as well provided thegetValue
functionality will be extended at some point (which I decided was out of scope for this PR). This PR is focused on functionality only and not speed, which will be the focus of an upcoming PR. Nevertheless, performance seems to be about on par with the previous version.This enables tree-shaking things like
Also, sequence expressions have been refactored to resolve some lingering bugs that no-one has stumbled upon probably because most people are sensible enough not to use a lot of sequence expressions. In case one of the three expression types is simplified and the result is the callee of a call expression and the context would be changed by the simplification, the result is wrapped in a
(0, expression)
babel style. This works even in nested situations:Also, handling of default exports that are rendered for side-effects only has been refined and synced with these changes.