47

In Node.js's REPL (tested in SpiderMonkey as well) the sequence

var foo = null;
(foo) = "bar";

is valid, with foo subsequently equal to "bar" as opposed to null.

This seems counterintuitive because one would think the parenthesis would at least dereference bar and throw Invalid left-hand side in assignment`.

Understandably, when you do anything interesting it does fail in aforementioned way.

(foo, bar) = 4
(true ? bar : foo) = 4

According to ECMA-262 on LeftHandExpressions (so far as I can interpret) are no valid non-terminals that would lead to a parenthetical being accepted.

Is there something I'm not seeing?

Peter Mortensen
  • 28,342
  • 21
  • 95
  • 123
TERMtm
  • 1,773
  • 3
  • 21
  • 27
  • i would have guessed [destructuring assignment](http://stackoverflow.com/a/26999862/1132334), but that has only square and curly brackets – Cee McSharpface May 14 '17 at 22:49
  • Guess parenthesis just force the calculation inside, `(foo) === foo`, also we have `(true ? foo : bar) === foo;` (both correct). – skobaljic May 14 '17 at 22:50
  • It's a grouping operator here, why would it be invalid? You can also do `foo = (5)` for example. – Egor Stambakio May 14 '17 at 22:51
  • 4
    @wostex "It's a grouping operator here" --- if it was - it would be syntactically incorrect. – zerkms May 14 '17 at 22:52
  • @skobaljic in your example `(foo) === foo` is because the values are equal. However, there also, the reference does not matter. `(foo)` in a perfect world would "return" the object contained, not the `foo` reference to be _overwitten_ by the `=` expression. – TERMtm May 14 '17 at 22:55
  • Correct, I voted up your question, wanna know why, too. – skobaljic May 14 '17 at 23:00
  • 3
    @TERMtm don't know if I'm really on the right track, but according to the specification (the one you linked), there is a second lexer rule to 12.2.1.5 that defines that a `CoverParenthesizedExpressionAndArrowParameterList` qualifies as `IsValidSimpleAssignmentTarget`, yielding its inner `expr`. – Cee McSharpface May 14 '17 at 23:02
  • The answer is: Because JavaScript. – naiveai May 15 '17 at 05:51
  • 1
    `(foo)` in this case is not a grouping operator, in the same way in which the method call `(obj.foo)()` does not have a grouping operator around `obj.foo`. `(0, obj.foo)()`, in contrast, does contain a grouping operator. The prior call passes `obj` as `this` context to `foo` (just like `obj.foo()`), the latter call does not. See [Why comma operator changes `this` in function call](https://stackoverflow.com/q/63737893/4642212). – Sebastian Simon Mar 23 '21 at 14:53

2 Answers2

28

It's valid indeed. You're allowed to wrap any simple assignment target in parenthesis.

The left hand part of the = operation is a LeftHandSideExpression as you correctly identified. This can be tracked down through the various precendence levels (NewExpression, MemberExpression) to a PrimaryExpression, which in turn might be a Cover­Parenthesized­Expression­And­Arrow­Parameter­List:

( Expression[In, ?Yield] )

(actually, when parsed with target PrimaryExpression, it's a ParenthesizedExpression).

So it's valid by the grammar, at least. Whether it's actually valid JS syntax is determined by another factor: early error static semantics. Those are basically prose or algorithmic rules that make some production expansions invalid (syntax errors) in certain cases. This for example allowed the authors to reuse the array and object initialiser grammars for destructuring, but only applying certain rules. In the early errors for assignment expressions we find

It is an early Reference Error if LeftHandSideExpression is neither an ObjectLiteral nor an ArrayLiteral and IsValidSimpleAssignmentTarget of LeftHandSideExpression is false.

We can also see this distinction in the evaluation of assignment expressions, where simple assignment targets are evaluated to a reference that can be assigned to, instead of getting the destructuring pattern stuff like object and array literals.

So what does that IsValidSimpleAssignmentTarget do to LeftHandSideExpressions? Basically it allows assignments to property accesses and disallows assignments to call expressions. It doesn't state anything about plain PrimaryExpressions which have their own IsValidSimpleAssignmentTarget rule. All it does is to extract the Expression between the parentheses through the Covered­Parenthesized­Expression operation, and then again check IsValidSimpleAssignmentTarget of that. In short: (…) = … is valid when … = … is valid. It'll yield true only for Identifiers (like in your example) and properties.

Bergi
  • 513,640
  • 108
  • 821
  • 1,164
  • 1
    Why would they allow such a syntax though. Would there be any particular use case where this might be helpful instead of confusing? – xji May 20 '17 at 09:02
  • 2
    @JIXiang I don't know - especially since it's not allowed around all patterns (e.g. object destructuring where it would be useful). Maybe it's just a lapse that happened when defining the arrow function parameter cover grammar. You should ask at es-discuss. – Bergi May 20 '17 at 09:21
15

As per @dlatikay's suggestion, following an existing hunch, research into CoveredParenthesizedExpression yielded a better understanding of what's happening here.

Apparently, the reason why a non-terminal cannot be found in the spec, to explain why (foo) is acceptable as a LeftHandExpression, is surprisingly simple. I'm assuming you understand how parsers work, and that they operate in two separate stages: Lexing and Parsing.

What I've learned from this little research tangent is that the construct (foo) is not technically being delivered to parser, and therefor the engine, as you might think.

Consider the following

var foo = (((bar)));

As we all know, something like this is perfectly legal. Why? Well you visually can just ignore the parenthesis while the statement remains making perfect sense.

Similarly, here is another valid example, even from a human readability perspective, because the parentheses only explicate what PEMDAS already makes implicit.

(3 + ((4 * 5) / 2)) === 3 + 4 * 5 / 2
>> true

One key observation can be loosely derived from this, given an understanding of how parsers already work. (remember, Javascript still is being parsed (read: compiled) and then run) So in a direct sense, these parentheses are "stating the obvious".

So all that being said, what exactly is going on?

Basically, parentheses (with the exception of function parameters) are collapsed into proper groupings of their containing symbols. IANAL but, in lay man's terms, that means that parentheses are only interpreted to guide the parser how to group what it reads. If the context of the parentheses is already "in order", and thus does not require any tweaking of the emitted AST, then the (machine) code is emitted as if those parentheses did not exist at all.

The parser is more or less being lazy, assuming the parens are impertinent. (which in this edge-case, is not true)

Okay, and where exactly is this happening?

According to 12.2.1.5 Static Semantics: IsValidSimpleAssignmentTarget in the spec,

PrimaryExpression: (CoverParenthesizedExpressionAndArrowParameterList)

  1. Let expr be CoveredParenthesizedExpression of CoverParenthesizedExpressionAndArrowParameterList.
  2. Return IsValidSimpleAssignmentTarget of expr.

I.E. If expecting primaryExpression return whatever is inside the parenthesis and use that.

Because of that, in this scenario, it does not convert (foo) into CoveredParenthesizedExpression{ inner: "foo" }, it converts it simply into foo which preserves the fact that it is an Identifier and thus syntactically, while not necessarily lexically, valid.

TL;DR

It's wat.

Want a little more insight?

Check out @Bergi's answer.

Community
  • 1
  • 1
TERMtm
  • 1,773
  • 3
  • 21
  • 27
  • Thanks @dlatikay! You help me find exactly what I was needing in order to confirm my nagging suspicions. – TERMtm May 14 '17 at 23:45
  • "Why? Because you can just ignore the parenthesis while the statement remains making perfect." --- this actually needs a reference to the standard. – zerkms May 14 '17 at 23:48
  • @zerkms Nowhere in the spec, so far as I can imagine, says this in plain terms. [CoverParenthesizedExpressionAndArrowParameterList](https://www.ecma-international.org/ecma-262/7.0/index.html#sec-static-semantics-coveredparenthesizedexpression) mentioned in answer, does imply this behavior however. – TERMtm May 14 '17 at 23:52
  • Then the answer is a speculation `¯\_(ツ)_/¯` Behaviour **must** be explainable by the standard, otherwise it's either implementation specific, or a bug, or an undefined behaviour. – zerkms May 14 '17 at 23:54
  • 1
    Took a closer look, [CoveredParenthesizedExpression](https://www.ecma-international.org/ecma-262/7.0/index.html#sec-static-semantics-coveredparenthesizedexpression) does explicitly state this production will return the 'lexical token stream' found within itself. Understanding what that means, the given behavior is explained. Did tweak the link, as per your comment though. – TERMtm May 15 '17 at 00:09
  • What is weird though is that they didn't allow `({ … }) = …`, i.e. a destructuring expression in parenthesis. – Bergi May 15 '17 at 00:21
  • @Bergi can you help find how it denies it? From https://www.ecma-international.org/ecma-262/7.0/index.html#sec-semantics-static-semantics-isvalidsimpleassignmenttarget it looks like it should be fine. Where is the other tricky part? – zerkms May 15 '17 at 00:40
  • @zerkms Exactly there - it yields `false` for object literals, they're no *simple* assignment target. (A test in Chrome confirmed I understood it right :-D) – Bergi May 15 '17 at 01:14
  • @Bergi but curly braces in `{ ... } = ` is not an object literal, but an object destructuring syntax. Or apparently it does not have a context outside of the parentheses. – zerkms May 15 '17 at 01:31
  • 1
    Parentheses are parser tokens, not just lexemes, and they are not used to 'force the lexer ... how to group what it reads`. The parser does all that. The fact that the production you mention is reached via the parentheses proves it. – user207421 May 15 '17 at 01:40
  • 1
    @zerkms That's exactly why this cover grammar stuff is so confusing :-/ It really is an *ObjectLiteral* in the grammar (not an "object initialiser" though) - they even [note](https://www.ecma-international.org/ecma-262/7.0/index.html#sec-object-initializer-static-semantics-early-errors) it: "*ObjectLiteral productions are also used as a cover grammar for ObjectAssignmentPattern*". And in [the evaluation](https://www.ecma-international.org/ecma-262/7.0/index.html#sec-assignment-operators-runtime-semantics-evaluation), they reparse it as an *AssignmentPattern*. No idea why it's that complicated. – Bergi May 15 '17 at 01:54
  • Out of curiosity I found [this](https://esdiscuss.org/topic/lr-1-grammar-parser-and-lookahead-restrictions) and [that](http://2ality.com/2012/04/arrow-functions.html). The reason for cover grammars is that they simplify writing a parser. Apparently they forgot the user story "Reading and underrstanding the spec". – Bergi May 15 '17 at 03:41
  • 1
    This answer sounds wrong - it's not correct that "parentheses are only interpreted to force the lexer how to group what it reads". That ("grouping" things into the correct AST) is the job of the parser, not the lexer. – Oliver Charlesworth May 15 '17 at 07:30
  • Edited to reflect @EJP's suggestions – TERMtm May 15 '17 at 17:38