-
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] Compile all super calls to ES2015 super #4424
Conversation
I think calling |
I'm all for option 1. See #3796. |
I personally never used |
I think since this is already the big breaking change release, option #1 is the way to go. |
OK, I'll go for option 1., and we can discuss about @jashkenas' comment as a separate new feature. @mitar calling super within anonymous functions is possible, so long as they are arrow functions. class A {
method () { console.log('A#method') }
}
class B extends A {
constructor () {
(() => super())()
}
method () {
(() => super.method())()
console.log('B#method')
}
}
new B().method() // A#method
// B#method |
I also think we should go with option 1, since we can always add the more elaborate versions later. This PR has a bug though I think. Take this input: class A
foo: ->
console.log 'woohoo!'
class B extends A
bar: ->
super.foo() This branch produces: var A, B;
A = class A {
foo() {
return console.log('woohoo!');
}
};
B = class B extends A {
bar() {
return super.bar(...arguments).foo();
}
}; but it should produce: var A, B;
A = class A {
foo() {
return console.log('woohoo!');
}
};
B = class B extends A {
bar() {
return super.foo();
}
}; |
Not a bug - that's how CoffeeScript's Interestingly, it seems that Personally, I think being able to look up arbitrary methods on the superclass is an undesirable feature. An overridden method shouldn't need to be aware that other methods may be overridden. I.e. if It also encourages people to think that |
I agree with this (naturally), and I think I've made that argument in these pages before. But! If we're switching over to ES6 semantics for classes for CS2, shouldn't we switch over to ES6 semantics for super at the same time? |
A |
Well, I'd argue that with classes we've changed very few semantics - we've adopted, by necessity, the @GeoffreyBooth it's the other form that was planned not implemented ( I'm not sure I agree that we need to support it. I feel like we're just adding needless redundancy since 99%1 of uses are just Whilst maintaining that it would be best not to, we could make both uses live side-by-side. If we do go down that route we might want to talk about letting 'bare 1. Made up. |
Using |
I think that once again, it'll be a step backwards — but we should make it work like JS |
OK, lets add Next question is bare To outline the suggested compilation: class Base
class SuperExamples extends Base
# Should throw a SyntaxError along the lines of "'super' must be called or accessed"
bareSuper: -> super
# Should compile to `super.shortSuper()`
shortSuper: -> super()
# Should compile to `super.esSuper()`
esSuper: -> super.esSuper() |
Yes, we should make it work like ES |
It looks like I was hasty to say "it's a 'syntactic form' that is only valid within a class initializer" - it seems it's also valid within methods in object initializers: ({
foo () { super.foo() } // OK
})
({
foo: function () { super.foo() } // SyntaxError: 'super' keyword unexpected here
}) Of course we don't currently compile object assigns with method values to that syntax. I had planned to look into that at some point to clean up the code I had to put in |
But how are you supposed to get that "method" back onto the prototype from outside? |
Heh, I was just writing a similar example in node, and got the same result. The MDN example shows using it via Object.setPrototypeOf(a, Lion)
new a().speak() |
This works: function A () {}
A.prototype = {
foo () { console.log('A#foo') }
}
function B () {}
B.prototype = {
foo () {
super.foo()
console.log('B#foo')
}
}
Object.setPrototypeOf(B.prototype, A.prototype)
new B().foo() // A#foo
// B#foo So, it seems that Edit: It seems the methods must be within their original object as well, the following doesn't work: A.prototype.foo = ({ foo () { console.log('A#foo') } }).foo
...
B.prototype.foo = ({ foo () { super.foo() ; console.log('B#foo') } }).foo
Object.setPrototypeOf(B.prototype, A.prototype)
new B().foo() // TypeError: (intermediate value).foo is not a function |
No, as you demonstrate below, there's still no way to add methods to an existing prototype and make use of |
So it basically seems that base = { foo () { return 'base' } }
child1 = { foo () { return super.foo() + ' -> child1' } }
child2 = { foo () { return super.foo() + ' -> child2' } }
try {
child1.foo()
} catch (e) {
console.log(e) // TypeError: (intermediate value).foo is not a function
}
Object.setPrototypeOf(child1, base)
console.log(child1.foo()) // base -> child1
Object.setPrototypeOf(child2, base)
console.log(child2.foo()) // base -> child2
Object.setPrototypeOf(child2, child1)
console.log(child2.foo()) // base -> child1 -> child2
method = child2.foo
console.log(method()) // base -> child1 -> child2
function A () {}
A.prototype.method = child2.foo
console.log(new A().method()) // base -> child1 -> child2 |
OK, I've pushed some more updates that adds support for // Generated by CoffeeScript 2.0.0-alpha
var A, B;
A = class A {
foo() {
return console.log('woohoo!');
}
};
B = class B extends A {
bar() {
return super.foo();
}
}; I've also fixed a couple of incidental bugs that were exposed by these changes, including:
|
Awesome work! Assuming all the tests pass, is there anything else to be done for Aside from adding support for |
@GeoffreyBooth I think we need to have a discussion about Regarding |
We can discuss |
I'd love to keep this sort of thing, and avoid the word |
@GeoffreyBooth I think that would be sufficient, yeh. I'll review those tests asap and double check there's nothing else we can throw out. Also if @lydell or @jashkenas (or anyone with more familiarity with the grammar) have any comments on the grammar changes they'd be very welcome. |
Super: [ | ||
o 'SUPER', -> new SuperCall | ||
o 'SUPER Arguments', -> new SuperCall $2 | ||
o 'SUPER OptFuncExist Arguments', -> new SuperCall LOC(1)(new Super), $3, $2 |
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 double-checking: Was the
'SUPER Arguments'
case redundant because of'SUPER OptFuncExist Arguments'
? - What about the
'SUPER'
case?
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.
Whoa there — I don't know about all of the ES6 grammar rules and wrinkles ... but wouldn't it make more sense to bring SUPER
in as a Value
, instead of special-casing all of the different ways you might access it?
Edit: Nevermind -- looking more than just cursorily, you did exactly that!
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.
Yeh, so SUPER Arguments
has been directly subsumed by SUPER OptFuncExists Arguments
(which allows super?()
to parse).
The SUPER
case has been dropped as, in light of supporting super
with accessors, it becomes confusing whichever way we spin it:
-
Keep bare
super
as a super call to the method of the same name, forwarding arguments. This is fine until we have to deal withsuper.foo
, which now looks ambiguous: is itsuper.foo
as it appears, orsuper().foo
? What about(super).foo
? Tbh this use ofsuper
is useful enough that I'd be happy to leave it in, but it does lead to a number of ways of doing the same thing withsuper
. -
Allow bare
super
, and handle it the same waysuper()
is handled (compiler error in aconstructor
, and otherwise a reference tosuper.<current method>
). This would be safe and consistent, except it could lead to subtle bugs and misunderstandings for people upgrading to CS2 from CS1.
SO, a bare super
will no longer parse. Unfortunately the error message isn't very friendly, and any tips to improve that would be great!
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 see, makes sense. 👍
@lydell did you have any further notes on this or is it ready to merge? |
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 a few really minor things that shouldn't hold up a merge.
|
||
test "soaked 'super' in constructor", -> | ||
assertErrorFormat 'class extends A then constructor: -> super?()', ''' | ||
[stdin]:1:38: error: Unsupported reference to 'super' |
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 about illegal instead of unsupported? (The latter almost sounds like this is a TODO comment, doesn't 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.
Hrm, I copied the current node.js error message verbatim. I suspect that might be because if is a bit of a TODO in JS (given past discussions on ES about allowing bare super
, amongst other things).
Since we're explicitly banning it, happy to change to "illegal" 👍
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 see. It’s fine either way then, I guess.
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 decided to leave this as Unsupported
for consistency with JS.
test/error_messages.coffee
Outdated
|
||
test "bare 'super' is no longer allowed", -> | ||
assertErrorFormat 'class extends A then constructor: -> super', ''' | ||
[stdin]:1:35: error: unexpected -> |
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.
We should add a TODO comment about improving this error message..
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.
👍
eq 2, super/ b/g | ||
eq 2, super() / b/g | ||
eq 2, super()/b/g | ||
eq 2, super()/ b/g | ||
eq true, super /b/g |
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 line still has a bare super
? Also, I'm not sure if these tests makes any sense any longer when the parentheses are needed?
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 either tbh, I guess it depends whether we consider "super()
" a 'callable token'. The test on this line is still valid, though, although I guess it's testing general parens-less calling, rather than anything regex-specific (though, I guess it's checking that a regex in that format is parsed as an arguments...).
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 not made any change for this. This test is pretty large, and seems concerned with testing the parsing of regexen vs. division. This section was added to check that things worked properly when the preceding node is a SuperCall
, so from that point of view the test is the same (except now it could just as easily use any other function besides super
).
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 see. I think this test makes sense the way it is, then. 👍
I'll aim to address those last comments and review the class super tests this evening. |
@connec along with making the changes discussed recently, can you please update https://github.com/jashkenas/coffeescript/wiki/%5BWIP%5D-Breaking-changes-in-CoffeeScript-2? Once the info about breaking changes for classes is in there, I think I'll move it into the docs proper. |
This breaks using `super` in non-methods, meaning several tests are failing. Self-compilation still works.
`super` can only be called directly in a method, or in an arrow function.
This behaviour worked 'for free' when the parent reference was being cached by the executable class body wrapper. There now needs to be special handling in place to check if the parent name matches the class name, and if so to cache the parent reference.
This removes syntax support for 'bare' super calls, e.g.: class B extends A constructor: -> super `super` must now always be followed with arguments like a regular function call. This also removes the capability of implicitly forwarding arguments. The above can be equivalently be written as: class B extends A constructor: -> super arguments...
`super` with following accessor(s) is now compiled to ES2015 equivalents. In particular, expressions such as `super.name`, `super[name]`, and also `super.name.prop` are all now valid, and can be used as expected as calls (i.e. `super.name()`) or in expressions (i.e. `if super.name? ...`). `super` without accessors is compiled to a constructor super call in a constructor, and otherwise, as before, to a super call to the method of the same name, i.e. speak: -> super() ...is equivalent to speak: -> super.speak() A neat side-effect of the changes is that existential calls now work properly with super, meaning `super?()` will only call if the super property exists (and is a function). This is not valid for super in constructors.
This fixes a bug in the previous super handling whereby using the `new` operator with a `super` call would silently drop the `new`. This is now an explicit compiler error, as it is invalid JS at runtime.
This was mostly code for tracking the source classes and variables for methods, which were needed to build the old lookups on `__super__`.
@GeoffreyBooth / @lydell I've responded to the review issues. I've left some additional TODOs for the class tests that were added in #4354, rather than trying to tackle them in this PR (all the tests pass, but some should now 'fail' that were testing for broken things that now work...). In short, I think this is ready to go. |
Good job, @connec! |
Super quick first pass. This breaks using
super
in non-methods, meaning several tests are failing. Self-compilation still works.As I mentioned in coffeescript6/discuss#22 (comment), the main decision to be made here is around support for
super
in non-class methods. Opinions from @jashkenas, @GeoffreyBooth, @lydell or anyone else very welcome!Here are a couple of the options:
super
outside of methods in class initializers. This would clean up some code in the compiler for tracking the classes of methods, special handling of prototype assignments in class bodies, etc.super
, otherwise, compile it as we do now. This would just add a couple of special cases in the handling ofSuperCall
nodes.__super__
, method class recording, etc. and replace it all withObject.getPrototypeOf(this.constructor)
. This could optionally turnsuper
into a fully runtime feature that will compile anywhere, and would provide clean ES2015 output.Personally, I'm leaning towards 1. for the significant impact I expect it will have on the compiler, and the fact that, realistically, the prototype-based approach is gonna be rare in an ES2015 world. If really, really needed,
super
in a dynamic method can still be specified explicitly (e.g.BaseClass::method.apply(this, arguments)
).If the compatibility break of such a move is too great, I'd be interested in exploring 3. I think that 2. would not be popular as it would force us to continue using class IIFEs to ensure correct parent class caching, etc. meaning 'ugly' JS output.