-
Notifications
You must be signed in to change notification settings - Fork 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
[CS2] Fix #4651: object spread destructuring bug #4652
Conversation
src/nodes.coffee
Outdated
# {a, r...} = {a:1, b:2, c:3} | ||
# {a, r...} = {a:1, obj...} | ||
shouldCache = (value) -> | ||
return yes if value.base instanceof Obj |
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.
Should we change Obj::shouldCache
to always be true? I guess there may be unintended consequences of that, but I can't think of a situation where we'd rather duplicate an object literal rather than reusing 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.
I've already tried that, but then tests fail.
I also think object destructuring with spreads is the only place where this is necessary.
Besides, spreads might soon reach stage-4.
src/nodes.coffee
Outdated
@@ -2229,8 +2229,11 @@ exports.Assign = class Assign extends Base | |||
nestedProperties = prop.value.variable.base.properties | |||
[prop.value.value, nestedSourceDefault] = prop.value.value.cache o | |||
if nestedProperties | |||
nestedSource = new Value source.base, source.properties.concat [new Access getPropKey prop] | |||
nestedSource = new Value new Op '?', nestedSource, nestedSourceDefault if nestedSourceDefault | |||
if source.properties |
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 this another case that would work better if whatever source
is was wrapped in a Value
? @helixbass
Alternatively, should the case where there are no properties
not wrap source
in a Value
? I can't really remember how this all works, but in the case that source
has no properties
, the next level of key won't be added (source.properties.concat [new Access getPropKey prop]
). It must be working if the tests pass, but I don't really understand why...
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.
@connec I’m not sure I follow your comment. Is there something that needs to change 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.
I guess it's just not clear to me why we need to check if source.properties
is set. I imagine this is roughly equivalent to source instanceof Value
, and my 2 questions are:
- Would it be easier to ensure
source
is aValue
(e.g. in the grammar/whereversource
is created)? - If
source
is not a value (e.g.not source.properties
) then we do not execute the line where we concat the property key. Why does this work?
I'll try and find some time to poke around to answer my own questions, since it's probably just my own misunderstandings. Since the tests work etc. this can probably go in, and if I find any issues I can submit a new ticket/fix.
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.
@connec you're right there's some bugginess here, I'm poking around and ran into eg this:
{c:{a...}} = d: 1
compiles to this:
({
c: {}
} = ref = {
d: 1
});
a = objectWithoutKeys(ref, []);
This would seem to be a case of nestedSource
not getting set correctly (the first arg to objectWithoutKeys()
shouldn't be ref
, I guess it should be ref.c
?)
If I can figure it out I'll push a fix to this branch, otherwise I'll at least push breaking tests
@connec @helixbass you're both right. Now it should work as expected. In case when {c:{a...}} = d: 1
###
({
c: {}
} = ref = {
d: 1
});
a = objectWithoutKeys(ref.c, []);
###
{a: {b}, r...} = @props
###
({
a: {b}
} = ref = this.props);
r = objectWithoutKeys(ref, ['a']);
### |
@helixbass would you mind reviewing my last commit? I think it should work now. |
@zdenko I was also working on a fix, I think it's a little cleaner so I merged it in (along with tests) - instead of preserving the Added to
From what I could tell, that currently-failing case
I tried changing the condition inside So I'm not sure how you can distinguish between an |
@helixbass It's fine with me. I'll also take a look later. # If this object is the left-hand side of an assignment, all its children
# are too.
if @lhs
for prop in props when prop instanceof Assign
{value} = prop
unwrappedVal = value.unwrapAll()
if unwrappedVal instanceof Arr or unwrappedVal instanceof Obj
unwrappedVal.lhs = yes
else if unwrappedVal instanceof Assign
unwrappedVal.nestedLhs = yes |
fix assign in nested properties
@helixbass I've made some changes and it seems to be working. {a = {b...}} = c
###
({a = Object.assign({}, b)} = c)
###
{c:{a...}} = d: 1
###
({
c: {}
} = ref = {
d: 1
});
a = objectWithoutKeys(ref.c, [])
### |
@GeoffreyBooth these cases are covered by tests. @helixbass added them in the previous commit. I noticed that unnecessary {a = {b...}} = c:1
###
({a = Object.assign({}, b)} = ref = {
c: 1
})
###
{p: {m, q..., t = {obj...}}, r...} = c: 2
###
({
p: {m, t = Object.assign({}, obj)}
} = ref1 = ref = {
c: 2
});
q = objectWithoutKeys(ref.p, ['m', 't']);
r = objectWithoutKeys(ref, ['p'])
### After: {a = {b...}} = c:1
###
({a = Object.assign({}, b)} = {
c: 1
})
###
{p: {m, q..., t = {obj...}}, r...} = c: 2
###
({
p: {m, t = Object.assign({}, obj)}
} = ref = {
c: 2
});
q = objectWithoutKeys(ref.p, ['m', 't']);
r = objectWithoutKeys(ref, ['p'])
### |
@zdenko nice, I'd just say the conditional based on
|
So @helixbass and @connec, how does the current version look? @zdenko and @helixbass, your efforts in tackling this have been heroic! |
@helixbass, does the last commit satisfy all your notes? Can I merge this in? |
@GeoffreyBooth I'd defer to @connec on whether it's ready to be merged but the cases I'd encountered are looking good |
@zdenko @GeoffreyBooth not specifically related to this issue (and sorry if it's already been addressed) but just saw that apparently |
@helixbass Are you sure? ✦ mkdir object-destructuring
✦ cd object-destructuring
✦ npm i babel-cli babel-preset-stage-3
+ babel-preset-stage-3@6.24.1
+ babel-cli@6.26.0
added 248 packages in 24.522s
✦ echo 'var {a = {...b}} = {c: 1}' > test.js
✦ ./node_modules/babel-cli/bin/babel.js test.js --presets=stage-3
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var { a = _extends({}, b) } = { c: 1 }; |
I've given this another look over and it seems to solve the problem. In the process I logged #4673, but that also existed before this PR. |
@GeoffreyBooth but try the same test where |
@GeoffreyBooth so if my understanding is correct, we should be outputting/using an |
I ended up playing with an alternative approach that treats the (Ab)using There's not really much between the two options in that they both ultimately allow |
# Cache the value for reuse with rest elements | ||
[@value, valueRef] = @value.cache o | ||
# Cache the value for reuse with rest elements. | ||
if @value.shouldCache() |
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 don't think this is needed any more? I was having no problems without any of the valueRefTemp
stuff.
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.
Also - in the PR description you mention that object literals were not being cached, however they do appear to be on 2
currently:
http://coffeescript.org/v2/#try:%7Ba%2C%20r...%7D%20%3D%20%7Ba%3A1%2C%20b%3A2%7D
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.
@connec how did you get rid of the valueRefTemp
references? How did you revise the
restElements = traverseRest @variable.base.properties, valueRefTemp
line?
@zdenko please ignore the polyfill discussion, that’s been taken care of in a separate PR. Do you have time to address these last few notes so that we can merge 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.
@GeoffreyBooth yes, I'll take a look a.s.a.p.
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 the PR description you mention that object literals were not being cached
@connec I was wrong regarding the caching. It was probably my mistake in the code.
::shouldCache()
and valueRefTemp
are used to assign object literal value
to ref
variable, e.g. {p: {m, q..., t = {obj...}}, r...} = c: 2
=> ({p: {m, t = Object.assign({}, obj)}} = ref = { c: 2})
.
The reason for use valueRefTemp
here, are cases where there are no rest elements in the destructuring, e.g. {a = {b...}} = c:1
=> ({a = Object.assign({}, b)} = {c: 1})
.
So, tests will pass if you remove valueRefTemp
and use @value
as a parameter for the traverseRest()
, only the compiled code will be different.
With traverseRestTemp
and ::shouldCache()
{p: {m, q..., t = {obj...}}, r...} = c: 2
###
({
p: {m, t = Object.assign({}, obj)}
} = ref = {
c: 2
});
q = objectWithoutKeys(ref.p, ['m', 't']);
r = objectWithoutKeys(ref, ['p']);
###
and without
{p: {m, q..., t = {obj...}}, r...} = c: 2
###
({
p: {m, t = Object.assign({}, obj)}
} = {
c: 2
});
q = objectWithoutKeys({
c: 2
}.p, ['m', 't']);
r = objectWithoutKeys({
c: 2
}, ['p']);
###
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.
Sorry @zdenko, I think I must be misunderstanding something. If I remove the valueTempRef
stuff:
diff --git a/src/nodes.coffee b/src/nodes.coffee
index 2e30bfe4..5e331f34 100644
--- a/src/nodes.coffee
+++ b/src/nodes.coffee
@@ -2253,16 +2253,12 @@ exports.Assign = class Assign extends Base
restElements
# Cache the value for reuse with rest elements.
- if @value.shouldCache()
- valueRefTemp = new IdentifierLiteral o.scope.freeVariable 'ref', reserve: false
- else
- valueRefTemp = @value.base
+ [@value, valueRef] = @value.cache o
# Find all rest elements.
- restElements = traverseRest @variable.base.properties, valueRefTemp
+ restElements = traverseRest @variable.base.properties, valueRef
return no unless restElements and restElements.length > 0
- [@value, valueRef] = @value.cache o
result = new Block [@]
for restElement in restElements
And run the example you gave:
echo '{p: {m, q..., t = {obj...}}, r...} = c: 2' | bin/coffee -bcs
I get the desired output:
// Generated by CoffeeScript 2.0.0-beta4
var m, q, r, ref, ref1, t,
objectWithoutKeys = function(o, ks) { var res = {}; for (var k in o) ([].indexOf.call(ks, k) < 0 && {}.hasOwnProperty.call(o, k)) && (res[k] = o[k]); return res; };
({
p: {m, t = Object.assign({}, obj)}
} = ref1 = ref = {
c: 2
});
q = objectWithoutKeys(ref.p, ['m', 't']);
r = objectWithoutKeys(ref, ['p']);
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.
Edit: in fact, I see that that output 'double-cached' {c:2}
(as ref
and ref1
). I guess this is because @value
is assigned even if the function returns. The following patch avoids the double-cache:
diff --git a/src/nodes.coffee b/src/nodes.coffee
index 2e30bfe4..081c2a71 100644
--- a/src/nodes.coffee
+++ b/src/nodes.coffee
@@ -2253,16 +2253,13 @@ exports.Assign = class Assign extends Base
restElements
# Cache the value for reuse with rest elements.
- if @value.shouldCache()
- valueRefTemp = new IdentifierLiteral o.scope.freeVariable 'ref', reserve: false
- else
- valueRefTemp = @value.base
+ [cachedValue, valueRef] = @value.cache o
# Find all rest elements.
- restElements = traverseRest @variable.base.properties, valueRefTemp
+ restElements = traverseRest @variable.base.properties, valueRef
return no unless restElements and restElements.length > 0
- [@value, valueRef] = @value.cache o
+ @value = cachedValue
result = new Block [@]
for restElement in restElements
Sorry to be fussy about this, but I'd rather not proliferate manual caching code (e.g. new IndentifierLiteral o.scope.freeVariable
) when Node::cache
could be used instead.
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 tried that before. With this approach ref
variable is still declared in the scope by call from ::cache
method:
if complex
ref = new IdentifierLiteral o.scope.freeVariable 'ref'
sub = new Assign ref, this
Check the full output:
{a = {b...}} = c:1
###
var a, ref;
^^^
({a = Object.assign({}, b)} = {
c: 1
});
###
{p: {m, q..., t = {obj...}}, r...} = c: 2
###
var m, q, r, ref, ref1, t,
^^^^
objectWithoutKeys = function(o, ks) { var res = {}; for (var k in o) ([].indexOf.call(ks, k) < 0 && {}.hasOwnProperty.call(o, k)) && (res[k] = o[k]); return res; };
({
p: {m, t = Object.assign({}, obj)}
} = ref = {
c: 2
});
q = objectWithoutKeys(ref.p, ['m', 't']);
r = objectWithoutKeys(ref, ['p']);
###
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.
Aha, good catch 😢
In that case, this LGTM!
@helixbass I think |
Internet Explorer (versions 11 and prior) don’t support CoffeeScript 2 takes the position that the tests should run in Node 7.6+, the first version of Node to support So the question is, is EDIT: Split off the polyfill discussion into #4674. Let’s please handle that in a separate PR, however we decide to address it. |
Thanks everyone! |
FIxes #4651.
Compiler was throwing an error for object spread destructuring of values with properties (e.g.
@props
,a.b.c
) or object literals.Besides that, caching of the object literal value is improved:
Before:
After: