augment evaluate to extract within objects#1425
Conversation
|
I don't see any reason to have this optimization under safe flag on long term if the feature is well maintained and done on literals only. But to be sure, there need some tests on literals containing non-constant expressions or variable access (since access on variables can be modified in js in very surprising ways) |
|
@avdg I concur - though since |
|
I think it's worth doing an investigation on that if time permits. |
|
@avdg let's leave that for another PR, as I need incremental understanding on all the corner cases before I'm comfortable enough to tackle the general case 😅 Thanks for the review! |
|
No worries patch is fine as is and understanding risk takes time. So I understand the precaution. |
lib/compress.js
Outdated
| return str.length; | ||
| if (compressor.option("unsafe")) { | ||
| var val = ev(this.expression, compressor); | ||
| if (HOP(val, this.property)) { |
There was a problem hiding this comment.
Before calling HOP() must check if first argument is an object literal:
$ echo 'a = null.p;' | bin/uglifyjs -c unsafe
undefined:6244
if (ex !== def) throw ex;
^
TypeError: Cannot convert undefined or null to object
at hasOwnProperty (<anonymous>)
We want to preserve bad javascript code as is. Garbage in, garbage out.
There was a problem hiding this comment.
Hmm, your example seems to work over here: 😓
C:\UglifyJS2>echo a = null.p; | node bin\uglifyjs -c unsafe
a=null.p;
C:\UglifyJS2>node
> var minify = require('uglify-js').minify;
undefined
> minify('a = null.p;', { fromString: true, compress: { unsafe: true } });
{ code: 'a=null.p;', map: null }
Regardless, do you suggest I follow this line to fix this up?
There was a problem hiding this comment.
@kzc fixed, added some tests for that as well
There was a problem hiding this comment.
It just shows that hasOwnProperty is implemented differently on different node versions or platforms.
I suggest something like this:
if (val instanceof AST_Object && HOP(val, this.property)) {
There was a problem hiding this comment.
Sorry, val instanceof AST_Object doesn't work here.
There was a problem hiding this comment.
@kzc except val is an actual plain object at this point, not an AST_Node?
There was a problem hiding this comment.
Just to be clear - does my latest commit address your concern? 😅
There was a problem hiding this comment.
Maybe this is sufficient:
if (val && HOP(val, this.property)) {
|
@avdg If this PR is ever merged to harmony, then an |
|
uh yeah, if we get more of these patches that behave totally different on harmony, it would be better that I would have repo access, so I can quickly fix the incompatibles, I don't think @rvanvelzen would be able to do this alone, though I don't want to do this alone myself (as it impacts user code in the end). For now I might want to delay this pr until I finish my other pr's I'm currently working on, so we have patches for harmony as well. |
|
@avdg @kzc if you are concerned with ... and may be move that |
|
I don't know if we should get any issues for harmony actually, but I'm working on something very related (AST_ArrayElement) so I might trip some wires and complicate code merging (but I might also not create these kind of conflicts). Concerning computed properties, these are formatted That said, we might need to keep track of optimizations that have to be implemented on harmony later. I do keep a list for few issues myself, but that's a list only I can access to as that is the project board on https://github.com/avdg/UglifyJS2/projects/ |
Is |
|
@kzc that would be my understanding, as anything other than a constant key would be an |
|
Just to clarify my comment - in |
|
edit: @kzc is more correct than me, everything is an |
|
on Just |
|
If a computed prop is a constant expression reduced to a string literal in compress, then |
|
@kzc updated |
lib/compress.js
Outdated
| for (var i = this.properties.length; --i >= 0;) { | ||
| var prop = this.properties[i]; | ||
| if (prop instanceof AST_ObjectKeyVal) { | ||
| if (typeof prop.key === "string") { |
There was a problem hiding this comment.
I don't think that's advisable because it could skip a side effect for key/value in a computed prop or a situation where there's a non-string literal key.
There was a problem hiding this comment.
On both master and harmony ev() always has to be evaluated for prop.value in case it throws (indicating a side effect).
There was a problem hiding this comment.
@kzc would you mind giving me an example of what you mean? 😅
There was a problem hiding this comment.
prop.key could be a Number literal with a value with a side effect.
Let's not worry about harmony for this PR. It complicates things.
There was a problem hiding this comment.
Within this loop I'm constructing an object which I can evaluate the values of, filtering out all the others.
The idea is to keep the evaluation process forward, and then at the end see if we hit anything constant. If we don't (or there isn't a key match due to its leaf fails to evaluate), we just bail out.
Now that I'm thinking out loud, I think try-catch-ing the ev(prop.value, compressor); here would make sense as I want to skip over those failures.
There was a problem hiding this comment.
@kzc ah, good point - I'll revert back to checking AST_ObjectKeyVal then.
There was a problem hiding this comment.
I think try-catch-ing the ev(prop.value, compressor); here would make sense as I want to skip over those failures.
No, you must not catch those exceptions - they indicate a side effect and must be allowed to be thrown.
ev() always has to be evaluated on the prop.value regardless of the key.
For ES5/master the key is always constant AFAIK, so ev() does not have to be run on the key. But in harmony (if supported) then ev() must always be run on AST_Node prop.key values in case it throws.
There was a problem hiding this comment.
I'll revert back to checking AST_ObjectKeyVal then
I don't think that check is needed at all.
There was a problem hiding this comment.
@kzc thanks for bearing with me - I think I get your point now.
Can you check if the tests in the latest commit is good enough to cover this path?
3bd8ddc to
b363786
Compare
lib/compress.js
Outdated
| if (prop instanceof AST_ObjectKeyVal) { | ||
| val[prop.key] = ev(prop.value, compressor); | ||
| } | ||
| val[prop.key] = ev(prop.value, compressor); |
There was a problem hiding this comment.
I think that's the correct way to do it for ES5/master.
Can you please add a comment above this live that harmony/ES6 will additionally have to run ev() on prop.key which will throw on side effects?
There was a problem hiding this comment.
@alexlamsl Will the following line work in ES5/master?
val[ev(prop.key, compressor)] = ev(prop.value, compressor);
If so, maybe that's the answer for both branches.
There was a problem hiding this comment.
Sorry for the the stream of thoughts. Just thinking out loud. Implementing this optimization is more complicated than it first appears.
There was a problem hiding this comment.
@kzc not directly - but I can do:
var key = prop.key;
if (key instanceof AST_Node) {
key = ev(key, compressor);
}
val[key] = ev(prop.value, compressor);would that be acceptable?
There was a problem hiding this comment.
@kzc no worries! I set aside this weekend to tackle this. I run into this case often enough to want to work on it. 😃
|
@alexlamsl I think your "ensure compatibility with harmony branch" fix is okay, but I found a new problem... expected: without your PR: with your PR: Could you please prepend [WIP] to this issue's title while we mull over other potential issues? |
|
@kzc fix pushed, title updated. |
|
@alexlamsl Could you please add a few tests with integer and floating point keys - with and without side effects in the value? A key of |
test/compress/evaluate.js
Outdated
| console.log( | ||
| ({a:{b:1},a:1}) + 1, | ||
| 2, | ||
| ({a:{b:1},a:1}).b + 1, |
There was a problem hiding this comment.
Is 1..b the correct expected result?
Hard to compare the input with expected because of the length of the tests. Would you mind splitting all the various types of tests into separate named tests - each testing one particular facet? i.e., object_literal_repeated_props, prop_on_null, object_literal_with_side_effect_in_prop_value, etc.
There was a problem hiding this comment.
@kzc indeed this is getting incomprehensible - I'm splitting them out as we speak 😅
1..b is correct as far as I can tell - one dot would cause syntax error as it confuses with numeral literals.
There was a problem hiding this comment.
The test names I gave are just examples - please use whatever test names you think are appropriate.
lib/compress.js
Outdated
| def(AST_Object, function(compressor){ | ||
| var val = {}; | ||
| for (var i = this.properties.length; --i >= 0;) { | ||
| for (var i = 0, l = this.properties.length; i < l; i++) { |
There was a problem hiding this comment.
This is a personal preference I admit, but I'd prefer l not be used as a variable name because it looks like 1. Could you please use len instead?
|
@kzc nice suggestion for trying out numbers as property keys - as they cannot be I suppose this wasn't an issue with ES5 as things like Also split out the tests, added new ones, and refactored the eye-sore variable as per request. |
Yeah, I just happened to coincidentally notice that during testing this PR as well. ;-) The tests are much clearer now. Thanks. I don't have the time to further review this PR right now unfortunately. Sometimes better to wait on a complex PR like this. Other unforeseen cases usually come up. |
No doubt. Javascript is a big collection of corner cases. |
Be warned - there be dragons in Javascript inequality statements. Talk about corner cases. |
|
By |
|
Anyway, give me a shout when new error cases are found and I'll work on them some more. Otherwise I'll leave this PR here for you to merge at your leisure 😎 |
|
I guess that specific example is fine, but variable access can have weird stuff installed (rarely used though). Things like https://github.com/tc39/test262/blob/master/test/language/expressions/less-than-or-equal/S11.8.3_A2.3_T1.js |
|
@alexlamsl I think this PR is in good shape but it would be wise to wait a week before removing the [WIP] just in case someone finds some breakage. |
|
[wip] just means that there are planned changes to take into account. If there is nothing planned, [wip] isn't necessary. There is still a lot that can go wrong before (and sadly enough just after) merging. |
|
Regarding inequality comparisons, see |
I personally take the bug report frequency into account before removing [WIP] on complicated far-reaching enhancements. If no reports for a while, it's usually safer. But because this feature is mostly behind the |
|
@alexlamsl Here's a new bug in code I found in the wild: Edit: some background... It's part of a function that checks whether an object is a host object in IE8 and earlier, so the author presumably wouldn't want it optimized away. But they wouldn't likely want to use the |
|
@kzc interesting hack - fixed 😉 |
4026a26 to
d2a4f29
Compare
|
Assuming no additional bugs are found please squash PR into a single commit when [WIP] is removed. Thanks. |
- gated by `unsafe` - replaces previous optimisation specific to String.length - "123"[0] => 1 - [1, 2, 3][0] => 1 - [1, 2, 3].length => 3 - does not apply to objects with overridden prototype functions
d2a4f29 to
af0a5fb
Compare
|
rebased & squashed |
|
This is looking very good, thanks a lot for the hard work. :) |
|
If I'm correct this should resolve #787 |
This replaces and extends the existing
"string".length => 6optimisation, and is also hidden behindunsafecompress option flag.Basically this enables
evaluateto navigate into object property on a best effort basis, and bails out if the final form isn't constant.Allows for constants hidden inside configuration objects to propagate, which is often encountered in the field. Does not address #1416 directly but may form part of the final solution.