tc39 / ecma262

Status, process, and documents for ECMA-262

Home Page:https://tc39.es/ecma262/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Reality and spec differ on property key resolution timing for `o[p] = f()`

rkirsling opened this issue · comments

This test for evaluation order of the assignment operator is quite interesting, as no browser-hosted engines pass it.

Specifically, we see the following:

var o = {};
var p = { toString() { print('p'); return 3; } };
var f = () => { print('f'); return 4; };
o[p] = f();
#### JavaScriptCore, SpiderMonkey, V8
f
p

But the above is a bit misleading. It's not really the case that engines aren't evaluating subexpressions from left to right; what actually differs is the timing of property key resolution:

var o = {};
var p = { toString() { print('resolve property key'); return 3; } };
var f = () => { print('eval rhs'); return 4; };
var pf = () => { print('eval lhs subscript'); return p; };

o[pf()] = f();
#### JavaScriptCore, SpiderMonkey, V8
eval lhs subscript
eval rhs
resolve property key

That is, engines perform ToPropertyKey as part of (get-by-value and) put-by-value, whereas the spec expects it to be performed as part of LHS evaluation.

For comparison, if we change the expression to o[pf()] += f(), we get:

#### V8
eval lhs subscript
resolve property key
eval rhs
resolve property key

#### SpiderMonkey
eval lhs subscript
resolve property key
eval rhs

#### JavaScriptCore
<omitted; we currently match V8 but will match SM soon>

Again, from the engine perspective, property key resolution is part of get- and put-by-value operations, so if we approach this naively, it'll happen twice. To fix this, we front (RequireObjectCoercible and) ToPropertyKey, such that get- and put-by-value ops can be provided with a pre-resolved property key. Regardless, we can consider reality and spec in alignment for "compound" assignment, because the get-by-value precedes RHS evaluation.


To summarize, when encountering the expression o[p] = f():

  • The spec effectively sees put(getReference(o, p), f())
  • Browser-hosted engines effectively see put(o, p, f())

And I'm not sure that I'd call either of these "unintuitive" per se! But I do think the most important question is whether the current spec is web compatible—if it is, it's probably most sensible not to change it.

I'd be very hesitant to change the evaluation order in the spec, introducing an inconsistency in the standard left-to-right order observed everywhere else. It would make the language harder to understand and learn.

I doubt this is a "web-reality issue" as in "websites would break if browser engines decided to fix this bug in their implementations".

I mean, it's 100% reality that this has been the behavior that JS users could consistently rely upon; whether it's compatible to overturn it is an open question (which seems worth finding an answer to).

commented

non-browser engines seem to get this correct, would be unfortunate to change this up on them.

Ah sorry, I left out a rather important detail. It's probably misleading to call this "eval order", because what we're actually concerned about is the timing of property key resolution.

If we change my example above to the following:

var o = {};
var p = { toString() { print('resolve property key'); return 3; } };
var f = () => { print('eval rhs'); return 4; };
var pf = () => { print('eval lhs subscript'); return p; };

o[pf()] = f();

Then we see:

#### JavaScriptCore, SpiderMonkey, V8
eval lhs subscript
eval rhs
resolve property key

And for o[pf()] += f():

#### V8
eval lhs subscript
resolve property key
eval rhs
resolve property key

#### SpiderMonkey
eval lhs subscript
resolve property key
eval rhs

V8's behavior is what happens when we evaluate left-to-right but let ToPropertyKey occur as part of get-by-value and put-by-value operations. SM's (and imminently, JSC's) behavior is similar, except that double-resolution is avoided: (RequireObjectCoercible and) ToPropertyKey are fronted for +=, such that get-by-value and put-by-value ops can be provided with a pre-resolved property key.

Since = only involves a put and not a get, engines just do ToPropertyKey at the timing of the put.

It definitely seems like what we would prefer is a single "resolve property key" observable coercion. Which non-browser engines would need to change along with v8 to make that into reality?

I've rewritten the original post to incorporate the findings of my previous comment. Should be much clearer now, I hope. 🙇🤞

From an implementation perspective, resolving the property key as part of the put-by-value allows us to represent them as a single operation, instead of splitting them up. It isn't likely to matter in higher tiers, but the current browser behaviour is likely a little bit faster in interpreters / baseline compilers. All else being equal, I think SpiderMonkey would lean towards updating the spec to match web reality.

@syg Any thoughts from V8 here?

@syg Any thoughts from V8 here?

+1 to @iainireland's comment above. Would prefer spec to match engine reality. The V8 bug for compound assignment is a longstanding one, and we (V8) should fix it, though I can't promise it'll be high priority to do so.

Thanks for the feedback! I guess the question then is how we'd want the proposed spec change to look.

Basically, we need to pull ToPropertyKey out of EvaluatePropertyAccessWithExpressionKey. But if we literally do just that, it seems we'll need to redefine what a Reference Record is, such that it's always able to hold a value which has yet to be resolved to a property key...even though this is only needed for = itself.

Alternatively, we could special-case Evaluation of = such that in the non-destructuring, non-"named anonymous function" path, we perform some new "SpecialEvaluation" of LeftHandSideExpression.

One way or another, one of these two sentences can't remain true:

... For example, the left-hand operand of an assignment is expected to produce a Reference Record.

A Reference Record is a resolved name or property binding; ...

Okay I've confirmed that it's fine to just loosen the type of [[ReferencedName]] in Reference Records and then unconditionally force the value everywhere it's accessed. It doesn't introduce any "interesting" new user code annotation points. I still think it's cleaner to have a separate kind of Reference Record (or a boolean) to track whether the value has been forced to a property key, but either is fine.

For more context, @bakkot and I prefer loosening the type of [[ReferencedName]] in Reference Records.

I think this change (i.e. #3307), by side effect, also affects destructuring assignments. Namely, I believe the following test262 tests would have to be updated to reflect the new evaluation order:

  1. language/expressions/assignment/destructuring/keyed-destructuring-property-reference-target-evaluation-order.js
  2. language/expressions/assignment/destructuring/iterator-destructuring-property-reference-target-evaluation-order.js

i.e. for 1. (and similarly for 2.):

({[sourceKey()]: target()[targetKey()]} = source());

evaluation order would change from:
source, source-key, source-key-tostring, target, target-key, target-key-tostring, get, set
to:
source, source-key, source-key-tostring, target, target-key, get, target-key-tostring, set
due to ToPropertyKey now being evaluated as part of 6. Return ? PutValue(lref, rhsValue).

I suppose this is intentional? since it appears to match web reality (for V8 and SM at least).
If you agree, please update these test262 tests, too.

Note that everyone disagrees on the former test:

#### expected
source,source-key,source-key-tostring,target,target-key,target-key-tostring,get,set

#### JavaScriptCore
source,source-key,source-key-tostring,get,target,target-key,target-key-tostring,set

#### SpiderMonkey
source,target,target-key,source-key,source-key-tostring,get,target-key-tostring,set

#### V8
source,source-key,source-key-tostring,target,target-key,get,target-key-tostring,set

...but SM and V8 do agree on the latter:

#### expected
source,iterator,target,target-key,target-key-tostring,iterator-step,iterator-done,set

#### JavaScriptCore
source,iterator,iterator-step,iterator-done,target,target-key,target-key-tostring,set

#### SpiderMonkey, V8
source,iterator,target,target-key,iterator-step,iterator-done,target-key-tostring,set

...and I do think you're correct that V8's behavior should be the newly expected one.