tc39 / proposal-call-this

A proposal for a simple call-this operator in JavaScript.
https://tc39.es/proposal-call-this/
MIT License
122 stars 5 forks source link

Suggestion: Loosen restriction on RHS of bind-this operator to allow dotted names #14

Closed rbuckton closed 3 years ago

rbuckton commented 3 years ago

The current explainer expects the RHS of :: to be either:

This works well for existing variables like this:

import { fn } from "foo";
obj::fn

But seems to require excess ceremony when used with module namespace imports:

import * as foo from "foo";
obj::foo.fn // syntax error
obj::(foo.fn) // ok

It would be nice to support a dotted name, especially since there are some methods built into the JS core API that intentionally support tear-offs (i.e., Array.prototype.*):

obj::Array.prototype.map
obj::Array.prototype.filter
ljharb commented 3 years ago

I think the latter example is pretty compelling; i don't find any value in supporting it for module namespace objects (but obv it wouldn't make any sense to treat one differently than the other).

js-choi commented 3 years ago

How could we structure such a grammar such that a.b::c.d() is not ambiguous between ((a.b)::(c.d))() (this issue’s goal) versus (a.b)::((c.d)()) (useless) and ((a.b)::c).d)() (the current behavior)?

theScottyJam commented 3 years ago

The new operator does something similar somehow. It'll keep grabbing stuff to the right of the new until it reaches function-call syntax, or something of lower precedence than property access (.).

Would it be possible to employ a similar trick? The :: token will keep grabbing stuff to its right, until it reaches call syntax or something with precedence lower than property access?

We would just need to also add special handling when :: and new is found together in the same space, like with obj::new f() and new obj::f(). Both are pretty useless, so I would be ok making them into syntax errrors. But, I'm also fine with them still having runtime semantics, e.g. the second one be interpreted as "first bind obj to f, then new f (causing the bound obj to immediately be chucked - so, it's useless to do this, but it doesn't necessarily have to be a syntax error)".

js-choi commented 3 years ago

I think it might be possible, yeah. And I think that new a::b and new a::b() would probably both be SyntaxErrors.

Expression Current grouping Desired grouping
a.b::c.d (a.b::c).d (a.b)::(c.d)
a.b::c.d() ((a.b::c).d)() ((a.b)::(c.d))()
a.b()::c.d (((a.b)())::c).d ((a.b)())::(c.d)
new a.b new (a.b) new (a.b)
new a.b()() (new (a.b)())() (new (a.b)())()
new a.b().c (new (a.b)()).c (new (a.b)()).c
new a.b::c new ((a.b)::c) SyntaxError?
new a.b::c() new ((a.b)::c)() SyntaxError?
new a.b()::c (new (a.b)())::c (new (a.b)())::c
a?.b::c.d ((a?.b)::c).d (a?.b)::(c.d)
a.b::c?.d (a?.b)::c)?.d ((a.b)::c)?.d SyntaxError?

I’ll think about this more.

If we do end up figuring out how to make a.b::c.d() unambiguously group as ((a.b)::(c.d))(), then I think that the RHS chain-of-property-identifiers expression is similar enough to decorators’ syntax that they should share a production, named something like SimpleMemberExpression.

js-choi commented 3 years ago

I think the following grammar unambiguously fulfills the table in the preceding comment.

I will file a pull request later.

Grammar (Note: I replaced `CallExpression : CoverCallExpressionAndAsyncArrowHead` with `CallExpression : MemberExpression Arguments` for clarity.) ``` MemberExpression : PrimaryExpression MemberExpression `[` Expression `]` MemberExpression `.` IdentifierName MemberExpression TemplateLiteral SuperProperty MetaProperty `new` MemberExpression Arguments MemberExpression `.` PrivateIdentifier SimpleMemberExpression : IdentifierReference `(` Expression `)` SimpleMemberExpression `.` IdentifierName NewExpression : MemberExpression `new` NewExpression BindThisExpression : MemberExpression `::` SimpleMemberExpression MemberExpression `::` TemplateLiteral CallExpression : MemberExpression Arguments SuperCall ImportCall CallExpression Arguments CallExpression `[` Expression `]` CallExpression `.` IdentifierName CallExpression TemplateLiteral CallExpression `.` PrivateIdentifier CallExpression `::` SimpleMemberExpression CallExpression `::` TemplateLiteral OptionalExpression : MemberExpression OptionalChain BindThisExpression OptionalChain CallExpression OptionalChain OptionalExpression OptionalChain OptionalChain : `?.` Arguments `?.` [ Expression ] `?.` IdentifierName `?.` TemplateLiteral `?.` PrivateIdentifier OptionalChain Arguments OptionalChain `[` Expression `]` OptionalChain `.` IdentifierName OptionalChain TemplateLiteral OptionalChain `.` PrivateIdentifier OptionalChain `::` SimpleMemberExpression OptionalChain `::` TemplateLiteral LeftHandSideExpression : NewExpression BindThisExpression CallExpression OptionalExpression ```
Examples * LeftHandSideExpression `a.b::c.d` * BindThisExpression `a.b::c.d` * MemberExpression `a.b` * `::` * SimpleMemberExpression `c.d` * LeftHandSideExpression `a.b::c.d()` * CallExpression `a.b::c.d()` * CallExpression `a.b::c.d` * MemberExpression `a.b` * `::` * SimpleMemberExpression `c.d` * Arguments `()` * LeftHandSideExpression `new a.b::c` * NewExpression `new a.b::c` * `new` * MemberExpression `a.b::c` * *SyntaxError* * LeftHandSideExpression `new a.b::c()` * MemberExpression `new a.b::c()` * `new` * MemberExpression `a.b::c` * *SyntaxError* * Arguments `()` * LeftHandSideExpression `new a.b()::c` * BindThisExpression `new a.b()::c` * MemberExpression `new a.b()` * `new` * MemberExpression `a.b` * Arguments `()` * `::` * SimpleMemberExpression `c` * LeftHandSideExpression `a?.b::c.d` * OptionalExpression `a?.b::c.d` * MemberExpression `a` * OptionalChain `?.b::c.d` * OptionalChain `?.b` * `::` * SimpleMemberExpression `c.d` * LeftHandSideExpression `a.b::c?.d` * OptionalExpression `a.b::c?.d` * BindThisExpression `a.b::c` * OptionalChain `?.d`
js-choi commented 3 years ago

The grammar in my previous comment would parse a.b::c?.d as ((a.b)::c)?.d.

I don’t like this. a.b::c.d, the version without the ?, groups very differently into (a.b)::(c.d). Having the grouping silently change so much simply by adding ? is probably a footgun.

So I think a.b::c?.d should be a SyntaxError without parentheses. (Of course, a.b::c()?.d would still be valid and would still group into (((a.b)::c)())?.d.)

This change should be doable by removing the OptionalExpression : BindThisExpression OptionalChain production from the previous comment’s grammar, which would prohibit optional chains from immediately following bind-this expressions without parentheses.

This SyntaxError probably wouldn’t affect everyday usage anyway. Developers probably wouldn’t try to put an optional chain in the bind-this expression’s RHS, since a.b::(null) would throw a TypeError anyway. And developers probably wouldn’t try to put an optional chain immediately after a bind-this expression, since a bind-this expression will always create a bound function and will never create a nullish value.