Skip to content
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

Normative: Make non-writable prototype properties not prevent assigning to instance #1320

Closed
wants to merge 1 commit into from

Conversation

littledan
Copy link
Member

@littledan littledan commented Oct 9, 2018

TC39 is discussing making inherited non-writable properties overridable with Set,
in #1307. We have not yet assessed the web compatibility of the change or written
tests, so this PR is not yet ready to merge, but this PR gives some concreteness
to how the specification may be done, to help move the discussion forward.

This patch implements the approach described in #1307 (comment)

…ng to instance

TC39 is discussing making inherited non-writable properties overridable with Set,
in tc39#1307. We have not yet assessed the web compatibility of the change or written
tests, so this PR is not yet ready to merge, but this PR gives some concreteness
to how the specification may be done, to help move the discussion forward.

This patch implements the approach described in tc39#1307 (comment)
@littledan littledan changed the title Normative: Make non-writable prototype properties not prevent assigni… Normative: Make non-writable prototype properties not prevent assigning to instance Oct 9, 2018
@ljharb ljharb added normative change Affects behavior required to correctly evaluate some ECMAScript source text needs consensus This needs committee consensus before it can be eligible to be merged. needs data This PR needs more information; such as web compatibility data, “web reality” (what all engines do)… needs test262 tests The proposal should specify how to test an implementation. Ideally via github.com/tc39/test262 labels Oct 9, 2018
aarongable pushed a commit to chromium/chromium that referenced this pull request Oct 19, 2018
tc39/ecma262#1320 proposes changing the JS
language so that the following:

```
let P = {};
Object.defineProperty('prop', { writable: false, value: 1 });
let O = { __proto__: P };
O.prop = 2;

console.log(O.prop);
```

will print '2' (instead of '1' in sloppy mode, or throwing a TypeError
in strict mode).

We intend to measure the frequency of both cases to determine how web
compatible the change will be.

This is the Chromium side of required changes.
The V8-side CL: https://crrev.com/c/1255549

BUG=v8:8175

Change-Id: If74e7aebcac01e034757c0c639bfb5518cd95c91
Reviewed-on: https://chromium-review.googlesource.com/c/1280618
Reviewed-by: Marja Hölttä <marja@chromium.org>
Reviewed-by: Jochen Eisinger <jochen@chromium.org>
Commit-Queue: Caitlin Potter <caitp@igalia.com>
Cr-Commit-Position: refs/heads/master@{#601120}
@bmeck
Copy link
Member

bmeck commented Oct 24, 2018

It looks like this would make all writable properties ignored? ownDesc could be on the receiver in which case we do want it to fail.

@littledan
Copy link
Member Author

@bmeck Is the line If _existingDescriptor_.[[Writable]] is *false*, return *false*. insufficient for that check against own properties?

@littledan
Copy link
Member Author

@caitp 's counters to assess the web compatibility have landed in Chrome (the initial figure seems to be invalid; the percentage can't be that high when it's in canary). These counters check how often there is a Set to an inherited, non-writable property, in strict and sloppy mode.

@caitp
Copy link
Contributor

caitp commented Oct 27, 2018

that does look like quite a high figure... @mathiasbynens is this normal?

@bmeck
Copy link
Member

bmeck commented Nov 30, 2018

http://jsfiddle.net/ is at least one site that seems to reproduce this consistently as seen in Chrome canary (via 2609 in chrome://histograms/Blink.UseCounter.Features). Haven't managed to track down what is causing the counter yet. Seems related to the JS bundle with their editor code which is using MooTools. MooTools itself does not fire this usage counter by merely loading up the library.

@mathiasbynens
Copy link
Member

mathiasbynens commented Dec 1, 2018

I’m a little surprised by the UseCounter results as well.

https://jsfiddle.net/js/_dist-editor.js contains a few occurrences of configurable: false (well, configurable:!1). I don’t see any writable: false, though.

@zalun, do you know where/how JSFiddle relies on prototype properties that are writable: false for its editor?

@mathiasbynens
Copy link
Member

I only see a single writable: false in https://jsfiddle.net/js/_dist-editor.js, and it’s an implicit one, in Babel’s output for export:

Object.defineProperty(exports, '__esModule', {
  value: true
});

Note that writable: false is the implicit default when using Object.defineProperty.

If Babel’s output is the culprit, that would certainly explain the higher-than-expected use counter results! However, I can’t find any code that later tries to write to the __esModule property in this script. 🤔

I could imagine Babel’s loose mode doing something like exports.__esModule = true instead, but not after Object.defineProperty()@jridgewell In which circumstances would Babel do this? When re-exporting a module?

@loganfsmyth
Copy link
Contributor

Re-exporting should always skip __esModule https://github.com/babel/babel/blob/806e1334737f4fbf29615c04d227076b76d7f041/packages/babel-helper-module-transforms/src/index.js#L243 so I wouldn't expect collisions from that.

@jridgewell
Copy link
Member

jridgewell commented Dec 1, 2018

I just converted jsfiddle to strict mode, and paused on every error. It's:

                        function Zr(e) {
                            return null == e ? e === r ? re : Y : Dn && Dn in tt(e) ? function(e) {
                                var t = ct.call(e, Dn)
                                  , n = e[Dn];
                                try {
                                    e[Dn] = r;
                                    var i = !0
                                } catch (e) {}
                                var o = dt.call(e);
                                return i && (t ? e[Dn] = n : delete e[Dn]),
                                o
                            }(e) : function(e) {
                                return dt.call(e)
                            }(e)
                        }

Particularly, it's e[Dn] = r, where e is a DataView and Dn is Symbol.toStringTag

@mathiasbynens
Copy link
Member

@loganfsmyth Ah, that code seems only necessary for the loose mode code. When exports.__esModule is defined using defineProperty, the property is enumerable, and thus it doesn’t show up in Object.keys(exports). (Note that enumerable: false is the implicit default when using Object.defineProperty). And since in the loose mode code, the override mistake doesn’t apply, __esModule is likely not the issue after all.

@mathiasbynens
Copy link
Member

@jridgewell Woah, nice catch (pun not intended)!

@zalun Could you please tell us where the code Justin pointed out originates? Is this part of some kind of library, or is it custom code?

@jridgewell
Copy link
Member

Is this lodash? 😢

@jridgewell
Copy link
Member

jridgewell commented Dec 1, 2018

Yup, it's lodash:

@jdalton
Copy link
Member

jdalton commented Dec 1, 2018

@jridgewell
I'm lurking on this issue but the lodash mention caught my eye. It looks like

Particularly, it's e[Dn] = r, where e is a DataView and Dn is Symbol.toStringTag

That's code within a try- of a try-catch block though if that makes a difference.

@jridgewell
Copy link
Member

It is within a try-catch. But unfortunately, the proposal would change the behavior of the surrounding code:

// Current semantics, overwrite will fail silently
baseGetTag(new DataView(new ArrayBuffer(1)));
// => "[object DataView]"

// With new semantics
baseGetTag(new DataView(new ArrayBuffer(1)));
// => "[object Object]"

Fortunately, these are feature tests. When one fails (as it would with the new behavior), lodash falls back onto a known implementation of getTag. So it'll be a little less optimized (it'll use a little wrapper around native Object.p.toString instead of just Object.p.toString), but everything will still work fine.

I think our counter is useless, though. 😞

@mathiasbynens
Copy link
Member

mathiasbynens commented Dec 1, 2018

@jdalton For full context, we’re trying to change the spec so that…

const prototype = {};
Object.defineProperty(prototype, 'property', {
  writable: false,
  value: 1,
});
const object = Object.create(prototype);
object.property = 2;
console.log(object.property);

…logs 2 (instead of logging 1 in sloppy mode, or throwing a TypeError in strict mode).

The lodash code appears to be in strict mode (the source code is a module) but is indeed within a try-catch block. Can you imagine this particular example actually breaking anything?

In the case of DataView[Symbol.toStringTag] = foo (in strict mode), this went from throwing to actually going through with the assignment. By itself, that doesn’t break anything.

Update: Woah, I clearly hadn’t refreshed when I posted that comment.

@jridgewell
Copy link
Member

Actually, there is a breakage in lodash: isTypedArray will no longer return true on typed arrays.

The fallback getTag does not correct the typed array instances' tag, so getTag will return "[object Object]" instead of "[object Uint8Array]" and that will fail the typed-array regex.

But, the code currently fails anyway! The regex matches for "Uint8", not "Uint8Array". So it may still be ok?

@jdalton
Copy link
Member

jdalton commented Dec 1, 2018

@jridgewell

That's in the master branch which is a bit of a mess, untested, and unreleased.
Check out a release branch like 4.17.11 when investigating.

@jridgewell
Copy link
Member

Ok, so this will break lodash's isTypedArray.

@littledan
Copy link
Member Author

Interesting! We had been hoping that the strict mode case would be "easier" to fix, but that seems to not be the case.

Crazy idea: what if we made toStringTag a getter with no setter, so it always throws to assign to it, unless you do Object.defineProperty (including sloppy mode and with this patch)?

@littledan
Copy link
Member Author

Great work, @jridgewell!

@jridgewell
Copy link
Member

@erights: This code has changed behavior.

Calling baseGetTag with a new Uint8Array, setting value[symToStringTag] = undefined used to:

  • In strict mode, throw. This makes unmasked === false and result === "[object Uint8Array"
  • In loose mode, nothing. This makes unmasked === true and result === "[object Uint8Array"

Now:

  • In strict mode, change. This makes unmasked === true and result === "[object Object"]
  • In loose mode, change. This makes unmasked === true and result === "[object Object"]

@caitp
Copy link
Contributor

caitp commented Dec 11, 2018

fwiw, lodash merged lodash/lodash@aa1d7d8 today, which eliminates the weird overriding behaviour.

@mathiasbynens
Copy link
Member

@jdalton Could you cut a new lodash release? I imagine that'd make it easier for sites like JSFiddle to update.

@jdalton
Copy link
Member

jdalton commented Dec 12, 2018

I can port the removal to the 4.17.12-pre branch for a 4.x release. Though if we're looking at the toothpaste out of the tube issues Lodash is used by a lot of websites (cdn hit stats) already.

@caitp
Copy link
Contributor

caitp commented Dec 12, 2018

Though if we're looking at the toothpaste out of the tube issues Lodash is used by a lot of websites (cdn hit stats) already.

Realistically, I don't expect that slight observable change to break most websites in a noticeable way (please don't prove me wrong!)

@littledan
Copy link
Member Author

@caitp I bet we could stand the observable change in #1320 (comment) , but it might be really breaking if isTypedArray returns false on TypedArrays (as @jridgewell reported above).

I'm wondering if it's possible to empirically investigate further whether @gibson042's minor change would be enough, or if there are other issues.

@bmeck
Copy link
Member

bmeck commented Jan 3, 2019

Are we thinking of investigating how that change would affect the web / are there ways we can check compatibility of what happens since lodash is in userland instead of at the vm level?

@littledan
Copy link
Member Author

OK, due to the very high counters in #1320 (comment) , and the practical cases traced in this thread, it seems that we don't have much hope of changing semantics here in either strict or sloppy mode. Great teamwork in figuring this out, @bmeck @caitp @jridgewell.

@littledan littledan closed this Feb 5, 2019
@dead-claudia
Copy link

How many of those uses would actually break after this? Like, how many are legitimately expecting an error, rather than just trying to assign and ignoring errors if it fails?

@erights
Copy link

erights commented Feb 5, 2019

@bmeck from our last verbal conversation, it seemed there was still hope here. Still? If so, please reopen. Thanks.

@lll000111
Copy link

lll000111 commented Mar 24, 2019

Does this discussion include frozen prototype objects?

Here is an example: Moddable (run JS compile to bytecode on small embedded systems) freezes all prototype objects to allow their compiler to put such unmodifiable objects into ROM.

Here is an example where that breaks: The not uncommon pattern to assign to name of Error objects fails - silently - because name exists in the frozen prototype.

Moddable-OpenSource/moddable#144

To me, that was quite unexpected and illogical. The "freeze" semantic seems to me to be orthogonal, finding that it also has this side-effect was unexpected. The prototype was frozen, not the instance, the extension of the prototype-freeze to prevent assignment to the instance object was a surprise. Things like this are not even mentioned on common pages like the MDN page for Object.freeze, one has to find this by chance or by reading the spec, which few people do (I only did and do it when it in a targeted way, but this issue here was unexpected so I did not know I should have read the spec). I would have expected the example code above to work (same for a frozen prototype object).


By the way, I still don't understand this inconsistency (from a logic PoV): Direct assignment fails, but if I do a Object.defineProperty() on the instance object I actually can override the non-writable prototype property (see linked issue).

@bakkot
Copy link
Contributor

bakkot commented Mar 25, 2019

@lll000111, yes it does, and Moddable (which is on TC39) has presented that use case to the rest of the committee.

Unfortunately, we can't make this change, however weird the current behavior might be, unless we're confident it won't break the web.

@lll000111
Copy link

@bakkot

Unfortunately, we can't make this change

Well, sure, but in the example I gave code that was supposed to work and is in common broke as a consequence of this poorly documented effect, so I'm not sure that "breaking" doesn't already happen. Especially when the failure to assign to an instance property does not result in an error but is silent.

@erights
Copy link

erights commented Mar 25, 2019

I suggest that we move the existing behavior into annex b, where legacy crap that browsers dare not change gathers, and then proceed to fix this in the main spec.

@erights
Copy link

erights commented Mar 25, 2019

That way, non browser engines that already drop some annex b stuff can drop this as well and remain conformant.

@littledan
Copy link
Member Author

I'm not a big fan of moving things from the main specification to Annex B; I'd rather us make a decision one way or another on these things. Otherwise, I worry that the JavaScript ecosystem could become forked and incompatible.

@guybedford
Copy link

I was quite surprised to see this closed I must admit - I was really hoping there might still be a spec path forward here. Are we really sure we've exhausted all the options? So that the best thing to do at this point would be to define a new form of Object.freeze or similar to get frozen intrinsics? I haven't dug into the details on this, but would gladly do so if there might still be hope here.

@erights
Copy link

erights commented May 12, 2019

A different form of freezing may indeed be the way to overcome this. See https://github.com/Agoric/harden which is what the SES shim uses to freeze the intrinsics. It would be very natural to propose a harden that freezes things in this special way. Were a proposal written up, I would happily champion it.

I haven't dug into the details on this, but would gladly do so if there might still be hope here.

Please dig; thanks!!!

@billiegoose
Copy link

Just stumbling across this to say - wow yeah I just discovered that Object.freeze on prototypes is next to useless 😭 , so we'll need something else. Looking forward to harden or whatever helps make immutable intrinsics possible! 🚀

@Jamesernator
Copy link

@erights I've written a strawman gist for an Object.harden which allows non-writable properties to optionally be overridable. If this something you would be interesting in championing I would be happy to write up a full proposal with spec text.

Strawman here: https://gist.github.com/Jamesernator/dc9831f9c13d17ac9a75d1fe25a2e015

@erights
Copy link

erights commented Sep 22, 2020

Yes! I'd love to work with you on advancing such a proposal. Please!

See some of my thoughts on this at https://gist.github.com/Jamesernator/dc9831f9c13d17ac9a75d1fe25a2e015#gistcomment-3462493

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs consensus This needs committee consensus before it can be eligible to be merged. needs data This PR needs more information; such as web compatibility data, “web reality” (what all engines do)… needs test262 tests The proposal should specify how to test an implementation. Ideally via github.com/tc39/test262 normative change Affects behavior required to correctly evaluate some ECMAScript source text
Projects
None yet
Development

Successfully merging this pull request may close these issues.