Standard builtins' prototypes and toString
Wow, what a mess. Let's forget the builtins for a moment and focus on JS classes, both the old/current patterns for coding these manually, and the new ES6 class syntax. Consider:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
},
getX() { return this.x; },
getY() { return this.y; },
toString() {
return `<${this.getX()},${this.getY()}>`;
}
}
or equivalently enough in ES5
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype = {
getX: function() { return this.x; },
getY: function() { return this.y; },
toString: function() {
return '<' + this.getX() + ',' + this.getY() + '>';
}
};
alert(Point.prototype)
alerts <undefined,undefined>
. Ok, this specific example doesn't throw, but equally simple and plausible examples would.
It was pointed out to me that I forgot to include the latest slides for async generator. I was referring to the link included earlier in the thread. Sorry for the confusion.
Here is the link again.
docs.google.com/a/netflix.com/file/d/0B4PVbLpUIdzoMDR5dWstRllXblU
Dictated using voice recognition. Please forgive the typos.
On Jun 12, 2014, at 5:26 AM, Till Schneidereit wrote:
While working on changing Date.prototype to be a plain object in SpiderMonkey, we realized that there's an issue: the way things are specced now,
alert(Date.prototype)
will throw, becauseDate.prototype.toString
isn't generic. The same applies for all builtins with non-generictoString
prototype functions.
Fortunately there aren't very many of those. I think it is only Date and RegExp that have this issue among the ES6 built-ins
To resolve this, I propose changing these
toString
to first check if thethis
value is %FooPrototype% (e.g., %DatePrototype% in the case at hand) and return the result of the equivalent of calling the original Object.prototype.toString.
that breaks if you move such methods across Realms.
I'm not sure if that is enough to cover subclasses of these builtins. Will calling
toString
on the prototype ofclass MyDate extends Date{}
still throw? If so, that would at least not be a backwards-compatibility concern, but it's probably also not desirable.
Yes, it would still throw for subclasses.
I think the pattern we should follow for such built-in toString methods is that if a branding check of this sort is performed, the fall back should be to perform the built-in Object.prototype.toString behavior rather than throwing.
Unless somebody sees issues with this fix, I'll incorporate it into the spec. for Date and RegExp.
Of course, even with this fix there is no guarantee that invoking toString on an object won't throw. Debugging tool that expect to use toString need to take that into account and provide their own fallbacks.
On Thu, Jun 12, 2014 at 5:19 PM, Allen Wirfs-Brock <allen at wirfs-brock.com>
wrote:
On Jun 12, 2014, at 5:26 AM, Till Schneidereit wrote:
While working on changing Date.prototype to be a plain object in SpiderMonkey, we realized that there's an issue: the way things are specced now,
alert(Date.prototype)
will throw, becauseDate.prototype.toString
isn't generic. The same applies for all builtins with non-generictoString
prototype functions.Fortunately there aren't very many of those. I think it is only Date and RegExp that have this issue among the ES6 built-ins
To resolve this, I propose changing these
toString
to first check if thethis
value is %FooPrototype% (e.g., %DatePrototype% in the case at hand) and return the result of the equivalent of calling the original Object.prototype.toString.that breaks if you move such methods across Realms.
Good point.
I'm not sure if that is enough to cover subclasses of these builtins. Will calling
toString
on the prototype ofclass MyDate extends Date{}
still throw? If so, that would at least not be a backwards-compatibility concern, but it's probably also not desirable.Yes, it would still throw for subclasses.
I think the pattern we should follow for such built-in toString methods is that if a branding check of this sort is performed, the fall back should be to perform the built-in Object.prototype.toString behavior rather than throwing.
That's what I was trying to say above, yes. The exact means of the branding are less important, I think.
Unless somebody sees issues with this fix, I'll incorporate it into the spec. for Date and RegExp.
Of course, even with this fix there is no guarantee that invoking toString on an object won't throw. Debugging tool that expect to use toString need to take that into account and provide their own fallbacks.
Sure. This has always been the case and is, I guess, just a consequence of how methods work in JS.
On Thu, Jun 12, 2014 at 8:19 AM, Allen Wirfs-Brock <allen at wirfs-brock.com>
wrote:
On Jun 12, 2014, at 5:26 AM, Till Schneidereit wrote:
While working on changing Date.prototype to be a plain object in SpiderMonkey, we realized that there's an issue: the way things are specced now,
alert(Date.prototype)
will throw, becauseDate.prototype.toString
isn't generic. The same applies for all builtins with non-generictoString
prototype functions.Fortunately there aren't very many of those. I think it is only Date and RegExp that have this issue among the ES6 built-ins
WeakMap, Map, Set, others?
To resolve this, I propose changing these
toString
to first check if thethis
value is %FooPrototype% (e.g., %DatePrototype% in the case at hand) and return the result of the equivalent of calling the original Object.prototype.toString.that breaks if you move such methods across Realms.
I'm not sure if that is enough to cover subclasses of these builtins. Will calling
toString
on the prototype ofclass MyDate extends Date{}
still throw? If so, that would at least not be a backwards-compatibility concern, but it's probably also not desirable.Yes, it would still throw for subclasses.
I think the pattern we should follow for such built-in toString methods is that if a branding check of this sort is performed, the fall back should be to perform the built-in Object.prototype.toString behavior rather than throwing.
Unless somebody sees issues with this fix, I'll incorporate it into the spec. for Date and RegExp.
The real problem includes ES6 classes as well. Whatever fix we choose, it should apply there as well -- not that I have a concrete proposal. This one's a real puzzler.
It would be slightly more "JavaScripty" to have Date.prototype.[[DateValue]] exist, and be set to the epoch or some such.
This problem actually seems to be an artifact of the way that [[Construct]] works in ES6 -- but the takeaway is that prototypes of a class are not themselves instances of the class. It's not surprising that methods of the class thus don't work on the prototype. I'd vote WONTFIX.
On Jun 12, 2014, at 8:24 AM, Mark Miller wrote:
On Thu, Jun 12, 2014 at 8:19 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
Fortunately there aren't very many of those. I think it is only Date and RegExp that have this issue among the ES6 built-ins
WeakMap, Map, Set, others?
Nope, they don't even have custom toString methods.
The real problem includes ES6 classes as well. Whatever fix we choose, it should apply there as well -- not that I have a concrete proposal. This one's a real puzzler.
By default they just inherit up the prototype chain, typically to Object.prototype.toSring
If a JS programmer chooses to over-ride toString, then it becomes their problem.
The same best practice should be taught to them too, if you are going to do a branding check in a toString method you should fall back to the default object behavior:
class MyClass {
toString() {
if (!myBrand(this)) return super.toString();
...
}
}
We can't do this for them because there is no universal branding concept we can apply for them.
On Jun 12, 2014, at 8:28 AM, C. Scott Ananian wrote:
It would be slightly more "JavaScripty" to have Date.prototype.[[DateValue]] exist, and be set to the epoch or some such.
This problem actually seems to be an artifact of the way that [[Construct]] works in ES6 -- but the takeaway is that prototypes of a class are not themselves instances of the class. It's not surprising that methods of the class thus don't work on the prototype. I'd vote WONTFIX. --scott
TC39 explicitly agreed that we would move away from the "a prototype is an instance of its constructor" model, except where there was known legacy usage that we had to support. That previous model make it very difficult to generalize the initialization of prototype objects created via class declarations.
On Jun 12, 2014, at 5:26 AM, Till Schneidereit wrote:
/ While working on changing Date.prototype to be a plain object in SpiderMonkey, we realized that there's an issue: the way things are specced now,
alert(Date.prototype)
will throw, becauseDate.prototype.toString
isn't generic. The same applies for all builtins with non-generictoString
prototype functions. / Fortunately there aren't very many of those. I think it is only Date and RegExp that have this issue among the ES6 built-ins
And Number.prototype
, String.prototype
, Boolean.prototype
and
Symbol.prototype
. And actually it's even worse for Symbol.prototype
because of the @@toPrimitive
override.
- André
On 6/12/14, 11:45 AM, André Bargull wrote:
And Number.prototype, String.prototype, Boolean.prototype
All of those have the relevant internal fields, so they don't have the problem.
e.g. Number.prototype.toString()
returns 0
.
and Symbol.prototype.
This one has the issue, though.
On Thu, Jun 12, 2014 at 11:42 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
TC39 explicitly agreed that we would move away from the "a prototype is an instance of its constructor" model, except where there was known legacy usage that we had to support. That previous model make it very difficult to generalize the initialization of prototype objects created via class declarations.
Right, which is why I'm saying I don't understand the problem with
Date.prototype.toString()
. It's not a Date
, it's a
Date.prototype
. Throwing an error is more informative than hiding
it and returning something arbitrary. As has been mentioned,
debuggers have to deal with the fact that toString
can have
side-effects, throw exceptions, etc, anyway. What problem are we
actually solving here?
On Jun 12, 2014, at 8:45 AM, André Bargull wrote:
On Jun 12, 2014, at 5:26 AM, Till Schneidereit wrote:
While working on changing Date.prototype to be a plain object in SpiderMonkey, we realized that there's an issue: the way things are specced now,
alert(Date.prototype)
will throw, becauseDate.prototype.toString
isn't generic. The same applies for all builtins with non-generictoString
prototype functions.Fortunately there aren't very many of those. I think it is only Date and RegExp that have this issue among the ES6 built-ins
And Number.prototype, String.prototype, Boolean.prototype and Symbol.prototype. And actually it's even worse for Symbol.prototype because of the @@toPrimitive override.
Right, I left those out because I thought we didn't have an issue with them, but you're right. I'll fix them too.
On Thu Jun 12 2014 at 11:28:12 AM, C. Scott Ananian <ecmascript at cscott.net>
wrote:
It would be slightly more "JavaScripty" to have Date.prototype.[[DateValue]] exist, and be set to the epoch or some such.
+1
- Let date be the this value.
- If Type(date) is not Object then, throw a TypeError exception.
- If date does not have a [[DateValue]] internal slot, then let tv be NaN
- Else let tv be this time value.
- Return ToDateString(tv).
Allen, what is the benefit to do super.toString()
instead?
On Thu, Jun 12, 2014 at 5:55 PM, Boris Zbarsky <bzbarsky at mit.edu> wrote:
On 6/12/14, 11:45 AM, André Bargull wrote:
And Number.prototype, String.prototype, Boolean.prototype
All of those have the relevant internal fields, so they don't have the problem.
e.g. Number.prototype.toString() returns "0".
They won't have those fields much longer if the changes in annex E.1[1] work out.
[1]: Search for "Date" in people.mozilla.org/~jorendorff/es6-draft.html#sec
FWIW, in Chrome 36:
> Date.prototype.toString()
"Invalid Date"
> ({ toString: Date.prototype.toString }).toString()
TypeError: this is not a Date object.
> Number.prototype.toString()
"0"
> String.prototype.toString()
""
> Boolean.prototype.toString()
"false"
> RegExp.prototype.toString()
"/(?:)/"
That last one is a bit unusual.
If Date.prototype is a Date, then we need additional special logic to ensure that freezing it actually makes it immutable. Otherwise, we have a hard to plug global communications channel. This was the reason why RegExp.prototype.compile had an [[Extensible]] check, and why we can remove the check if RegExp.prototype is no longer a RegExp.
On Thu, Jun 12, 2014 at 6:03 PM, C. Scott Ananian <ecmascript at cscott.net>
wrote:
FWIW, in Chrome 36:
> Date.prototype.toString() "Invalid Date" > ({ toString: Date.prototype.toString }).toString() TypeError: this is not a Date object. > Number.prototype.toString() "0" > String.prototype.toString() "" > Boolean.prototype.toString() "false" > RegExp.prototype.toString() "/(?:)/"
Yes, that's how it's supposed to work according to ES5, and does in all engines. Annex E.1 of ES6 notes that this changes.
On Thu, Jun 12, 2014 at 12:48 PM, Jason Orendorff <jorendorff at mozilla.com> wrote:
In any case, I doubt we have a choice. ES3-5 at least supported it. There is surely a Web page somewhere that calls .toString() on every object it can find, just because.
If you are concerned about compatibility, them
Date.prototype.toString()
should return "Invalid Date" (as Erik
stated, in concrete pseudo-code, above), not the result of
Object.prototype.toString()
. Similarly,
Boolean.prototype.toString()
should return false
, etc.
If we're changing the result of #toString
, then we should just throw
a TypeError
, rather than return some arbitrary value. Specifically,
I want Date#toString
to consistently throw a TypeError
if this
is not a Date
(or subclass), not to do some weird special case only
for Date.prototype
.
On Jun 12, 2014, at 8:57 AM, Erik Arvidsson wrote:
On Thu Jun 12 2014 at 11:28:12 AM, C. Scott Ananian <ecmascript at cscott.net> wrote: It would be slightly more "JavaScripty" to have Date.prototype.[[DateValue]] exist, and be set to the epoch or some such.
+1
- Let date be the this value.
- If Type(date) is not Object then, throw a TypeError exception.
- If date does not have a [[DateValue]] internal slot, then let tv be NaN
- Else let tv be this time value.
- Return ToDateString(tv).
Allen, what is the benefit to do
super.toString()
instead?
This is really getting into how much and where we want to break legacy compatibility.
ES5 actually under specifies Data.prototype.toString. It doesn't say what then the this value is not a Date instance. Actually it says that the result is implementation dependent.
But, let's assume that it was in fact specified more like the current ES6 spec which your above proposed change is derived from. The same reasoning can be applicable to the Number, String, etc. which happen to be more precisely specified in ES5.
In that case, in ES5 (where Date.prototype is a Date instance) Date.prototyjpe.toString() should produce ToDateString(NaN), and Date.prototype.toString.call({ }); should throw because 'date' does not have a [[DateValue]] internal slot.
Your proposed change would return ToDateString(NaN) for both cases. So that preserves the ES5 level result when applied to Date.prototype but changes the result for any other non-Date object.
With my proposed solution they would both produce "[object Object]" for both cases. So it changes the result of both cases, relative to ES5.
So both solutions change the result produce for non-Date objects. I also change the results for Date.prototype while your solution preserves it. Which breaking change are we willing to to risk? In making the change to non-instance prototypes we placed a bet that nobody depended upon those prototypes being instances of their constructor. That presumably means that we were also betting that nobody is dependent upon the result you get when applying toString to those prototypes. From that perspective, the current spec. language is a good match to our bet. It changes what happens for toString applied to the prototype, but it preserves other Date toString behavior including throwing when Date.prototype.toString is applied to a non-Date object.
So, I think the current spec. best matches our consensus about changing these prototypes to non-consructor instances. I think my proposed alternative toString fall back pattern is more useful, but is a bigger breaking change. Are we willing to up the bet, or should we let it ride as is?
On Thu, Jun 12, 2014 at 12:48 PM, Jason Orendorff <jorendorff at mozilla.com, mail.mozilla.org/listinfo/es-discuss> wrote:
/ In any case, I doubt we have a choice. ES3-5 at least supported it. There is />/ surely a Web page somewhere that calls .toString() on every object it can />/ find, just because. /
Or a web page converts some value to a string using "" + someValue
, in
which case adding a toString() legacy mode is not sufficient, because
valueOf() is actually invoked (well, except for Date.prototype because
of its @@toPrimitive override). Does that mean valueOf() also needs to
have a legacy mode to special case the prototype object?
On Thu, Jun 12, 2014 at 12:59 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
So, I think the current spec. best matches our consensus about changing these prototypes to non-consructor instances. I think my proposed alternative toString fall back pattern is more useful, but is a bigger breaking change. Are we willing to up the bet, or should we let it ride as is?
Just restating and naming the alternatives:
(a) #toString
throws TypeError when given a non-instance. Changes
Date#toString()
, no change to Date#toString.call({})
.
(b) #toString
is generic; invokes Object#toString
when given a
non-instance. Changes both Date#toString()
and
Date#toString.call({})
.
(c) #toString
is generic; uses a "zero" value when given a
non-instance. No change to Date#toString()
; changes
Date#toString.call({})
.
(d) #toString
returns a "zero" value when given a prototype, throws
TypeError otherwise. No change to Date#toString()
or
Date#toString.call({})
.
Option (a) is what is in the current spec.
Options (b) and (c) make the toString
method generic.
Option (d) preserves compatibility to the greatest degree possible.
--scott
ps. I prefer (a).
On Thu, Jun 12, 2014 at 8:29 PM, C. Scott Ananian <ecmascript at cscott.net>
wrote:
On Thu, Jun 12, 2014 at 12:59 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
So, I think the current spec. best matches our consensus about changing these prototypes to non-consructor instances. I think my proposed alternative toString fall back pattern is more useful, but is a bigger breaking change. Are we willing to up the bet, or should we let it ride as is?
Just restating and naming the alternatives:
(a)
#toString
throws TypeError when given a non-instance. ChangesDate#toString()
, no change toDate#toString.call({})
.(b)
#toString
is generic; invokesObject#toString
when given a non-instance. Changes bothDate#toString()
andDate#toString.call({})
.(c)
#toString
is generic; uses a "zero" value when given a non-instance. No change toDate#toString()
; changesDate#toString.call({})
.(d)
#toString
returns a "zero" value when given a prototype, throws TypeError otherwise. No change toDate#toString()
orDate#toString.call({})
.Option (a) is what is in the current spec. Options (b) and (c) make the
toString
method generic. Option (d) preserves compatibility to the greatest degree possible.
There is (e) #toString
returns "[object Object]" when invoked on the (an)
original Date.prototype (regardless of the Realm it came from). Otherwise,
it throws when invoked on a non-instance.
This is what Allen and me proposed. And I think it's the best solution for two reasons:
- I'd bet good money that (a) breaks the web so just isn't an option.
- as Jason points out, stringifying an object should succeed for as many things as possible. The script authors can do what they want, but the builtins shouldn't throw if you stringify them.
As André points out, this affects valueOf
, too.
On Thu, Jun 12, 2014 at 3:06 PM, Till Schneidereit <till at tillschneidereit.net> wrote:
On Thu, Jun 12, 2014 at 8:29 PM, C. Scott Ananian <ecmascript at cscott.net> wrote:
(a)
#toString
throws TypeError when given a non-instance. ChangesDate#toString()
, no change toDate#toString.call({})
.(b)
#toString
is generic; invokesObject#toString
when given a non-instance. Changes bothDate#toString()
andDate#toString.call({})
.(c)
#toString
is generic; uses a "zero" value when given a non-instance. No change toDate#toString()
; changesDate#toString.call({})
.(d)
#toString
returns a "zero" value when given a prototype, throws TypeError otherwise. No change toDate#toString()
orDate#toString.call({})
.There is (e)
#toString
returns "[object Object]" when invoked on the (an) original Date.prototype (regardless of the Realm it came from). Otherwise, it throws when invoked on a non-instance.This is what Allen and me proposed.
Allen said:
Your proposed change would return ToDateString(NaN) for both cases. So that preserves the ES5 level result when applied to Date.prototype but changes the result for any other non-Date object.
With my proposed solution they would both produce "[object Object]" for both cases. So it changes the result of both cases, relative to ES5.
...which is option (b). (Allen, correct me if I'm reading that wrong!)
On Jun 12, 2014, at 12:14 PM, C. Scott Ananian wrote:
On Thu, Jun 12, 2014 at 3:06 PM, Till Schneidereit <till at tillschneidereit.net> wrote:
On Thu, Jun 12, 2014 at 8:29 PM, C. Scott Ananian <ecmascript at cscott.net> wrote:
(a)
#toString
throws TypeError when given a non-instance. ChangesDate#toString()
, no change toDate#toString.call({})
.(b)
#toString
is generic; invokesObject#toString
when given a non-instance. Changes bothDate#toString()
andDate#toString.call({})
.(c)
#toString
is generic; uses a "zero" value when given a non-instance. No change toDate#toString()
; changesDate#toString.call({})
.(d)
#toString
returns a "zero" value when given a prototype, throws TypeError otherwise. No change toDate#toString()
orDate#toString.call({})
.There is (e)
#toString
returns "[object Object]" when invoked on the (an) original Date.prototype (regardless of the Realm it came from). Otherwise, it throws when invoked on a non-instance.This is what Allen and me proposed.
Allen said:
Your proposed change would return ToDateString(NaN) for both cases. So that preserves the ES5 level result when applied to Date.prototype but changes the result for any other non-Date object.
With my proposed solution they would both produce "[object Object]" for both cases. So it changes the result of both cases, relative to ES5.
...which is option (b). (Allen, correct me if I'm reading that wrong!)
you're right.
Till, In the spec. we don't actually have a good way to identify any Date.prototype object from any Realm. We'd have to brand all Date.prototype objects in some way. It could be done, but it isn't something I would expect anybody to every bother to do for user defined classes. If such cross realm detection is actually important for Date why isn't it also important for the classes that a JS programmer defines.
On Thu, Jun 12, 2014 at 9:30 PM, Allen Wirfs-Brock <allen at wirfs-brock.com>
wrote:
On Jun 12, 2014, at 12:14 PM, C. Scott Ananian wrote:
On Thu, Jun 12, 2014 at 3:06 PM, Till Schneidereit <till at tillschneidereit.net> wrote:
On Thu, Jun 12, 2014 at 8:29 PM, C. Scott Ananian < ecmascript at cscott.net>
wrote:
(a)
#toString
throws TypeError when given a non-instance. ChangesDate#toString()
, no change toDate#toString.call({})
.(b)
#toString
is generic; invokesObject#toString
when given a non-instance. Changes bothDate#toString()
andDate#toString.call({})
.(c)
#toString
is generic; uses a "zero" value when given a non-instance. No change toDate#toString()
; changesDate#toString.call({})
.(d)
#toString
returns a "zero" value when given a prototype, throws TypeError otherwise. No change toDate#toString()
orDate#toString.call({})
.There is (e)
#toString
returns "[object Object]" when invoked on the (an)original Date.prototype (regardless of the Realm it came from). Otherwise,
it throws when invoked on a non-instance.
This is what Allen and me proposed.
Allen said:
Your proposed change would return ToDateString(NaN) for both cases. So that preserves the ES5 level result when applied to Date.prototype but changes the result for any other non-Date object.
With my proposed solution they would both produce "[object Object]" for both cases. So it changes the result of both cases, relative to ES5.
...which is option (b). (Allen, correct me if I'm reading that wrong!)
Oh, sorry, I missed that.
you're right.
Till, In the spec. we don't actually have a good way to identify any Date.prototype object from any Realm. We'd have to brand all Date.prototype objects in some way. It could be done, but it isn't something I would expect anybody to every bother to do for user defined classes. If such cross realm detection is actually important for Date why isn't it also important for the classes that a JS programmer defines.
Fair. (Ignoring the fact that all actual implementations probably do have a way to do this.) What about only special-casing Date.prototype from the current Realm, then? We have %DatePrototype% for that, and it probably covers the vast majority of the compatibility concerns.
I like this list. I prefer #c.
- We have previously succeeded at making previously non-generic methods generic. I think we could get away with #c.
- It is easier for a normal JS programmer to do the equivalent of #c for most of their classes.
- Doesn't requiring branding the builtin prototypes unnecessarily.
- Works fine across Realms.
On Thu, Jun 12, 2014 at 3:56 PM, Mark S. Miller <erights at google.com> wrote:
I like this list. I prefer #c.
- We have previously succeeded at making previously non-generic methods generic. I think we could get away with #c.
- It is easier for a normal JS programmer to do the equivalent of #c for most of their classes.
FWIW, if a change to the spec is needed, I also favor (c).
Both generic options (b) and (c) have consistent behavior that can
extend to #valueOf
. In option (b) you would presumably invoke
Object#valueOf
and for option (c) you'd return the "zero" value.
On Thu, Jun 12, 2014 at 10:42 PM, C. Scott Ananian <ecmascript at cscott.net>
wrote:
On Thu, Jun 12, 2014 at 3:56 PM, Mark S. Miller <erights at google.com> wrote:
I like this list. I prefer #c.
- We have previously succeeded at making previously non-generic methods generic. I think we could get away with #c.
- It is easier for a normal JS programmer to do the equivalent of #c for most of their classes.
FWIW, if a change to the spec is needed, I also favor (c).
Both generic options (b) and (c) have consistent behavior that can extend to
#valueOf
. In option (b) you would presumably invokeObject#valueOf
and for option (c) you'd return the "zero" value.
Is this close-ish to a consensus? If so, we'd implement and land it on Nightly to get an idea of its web compatibility. It doesn't sound like there's a meaningful danger of implementing something seriously incompatible with what the spec is going to say - if (c) is roughly adopted.
Trying #c on Nightly would provide us the information needed to bring #c to consensus.
On Jun 17, 2014, at 2:10 AM, Till Schneidereit wrote:
On Thu, Jun 12, 2014 at 10:42 PM, C. Scott Ananian <ecmascript at cscott.net> wrote: On Thu, Jun 12, 2014 at 3:56 PM, Mark S. Miller <erights at google.com> wrote:
I like this list. I prefer #c.
- We have previously succeeded at making previously non-generic methods generic. I think we could get away with #c.
- It is easier for a normal JS programmer to do the equivalent of #c for most of their classes.
FWIW, if a change to the spec is needed, I also favor (c).
Both generic options (b) and (c) have consistent behavior that can extend to
#valueOf
. In option (b) you would presumably invokeObject#valueOf
and for option (c) you'd return the "zero" value.Is this close-ish to a consensus? If so, we'd implement and land it on Nightly to get an idea of its web compatibility. It doesn't sound like there's a meaningful danger of implementing something seriously incompatible with what the spec is going to say - if (c) is roughly adopted.
I'm not sure who introduced the idea that the Date.prototype should have a "zero value", but that is inconsistent with ES3&5 where the TimeValue of Date.prototype is NaN: www.ecma-international.org/ecma-262/5.1/#sec-15.9.5 If we went the (c) route it should presumably be modified to use NaN and not 0.
I have yet seen any evidence that there is an issue with valueOf. The original premise that TC39 agreed to is that we would make prototypes ordinary objects except for cases (like Array and Function) where there were known dependency on the legacy instance behavior. Has anybody actually observed code that depends upon Date.prototype.valueOf() returning NaN?
I'm also, not sure that we've seen an actual real world case that depends upon Date.prototype.toString() not throwing but I'm very sympathetic to the argument that it should be possible to toString any object.
That suggests that the issue here is broader than just legacy compatability for Date.prototype.toString. We have lots of new built-in constructors (Map, for example) whose prototypes are not instance objects. As of now we haven't defined specialized toString methods for them, but we might in the future and it would be nice to establish a pattern that user defined classes could follow. The approach of falling back to Object.prototype.toString when presented the wrong "kind" of object seems like a good general pattern for extending toString. That is essentially alternative (b).
So, here's mky proposal:
If somebody has evidence that there are actual real world dependencies upon Date.prototype.toString() producing ToDateString(NaN) then we should go with the NaN version of (c). Otherwise we should apply (b) to Date.prototype.toString. In either case, we should make all the other built-in toString methods that depend upon a specific kind of object also follow (b).
How about we test (b) for Date on Nightly?
On Tue, Jun 17, 2014 at 11:33 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
I'm not sure who introduced the idea that the Date.prototype should have a "zero value", but that is inconsistent with ES3&5 where the TimeValue of Date.prototype is NaN: www.ecma-international.org/ecma-262/5.1/#sec-15.9.5 If we went the (c) route it should presumably be modified to use NaN and not 0.
Sorry, I was using quotes around "zero value" on purpose to mean, "the appropriate value for the given type" (which is not actually 0). Date should be NaN, boolean should be false, etc. I hope most of those reading understood this.
I am happy with #b as well, though I prefer #c. I also agree with C. Scott's interpretation of #c, to mean, appropriate degenerate value, which is generally the zero value, but is plausibly NaN for Date.
Whichever experiment Nightly tries first with a positive outcome, I expect that's what we'll do, since the difference between #b and #c is not large enough to be worth waiting for a second experiment.
On Tue, Jun 17, 2014 at 6:07 PM, Mark Miller <erights at gmail.com> wrote:
I am happy with #b as well, though I prefer #c. I also agree with C. Scott's interpretation of #c, to mean, appropriate degenerate value, which is generally the zero value, but is plausibly NaN for Date.
Whichever experiment Nightly tries first with a positive outcome, I expect that's what we'll do, since the difference between #b and #c is not large enough to be worth waiting for a second experiment.
I don't much care which one we choose, to be honest. I kinda doubt that #b will have more compatibility issues than #c[1], and find conceptually cleaner by a thin margin: a non-Date object isn't an invalid Date, it's not a Date. But then again, if you don't want an object to be treated as a Date, don't call Date's toString on it? Implementation-wise, it's pretty much a wash, at least.
[1]: and for all I know there might be content out there that relies on an exception being thrown if Date#toString is used on a non-Date object ...
On Jun 17, 2014, at 1:41 PM, Till Schneidereit wrote:
On Tue, Jun 17, 2014 at 6:07 PM, Mark Miller <erights at gmail.com> wrote: I am happy with #b as well, though I prefer #c. I also agree with C. Scott's interpretation of #c, to mean, appropriate degenerate value, which is generally the zero value, but is plausibly NaN for Date.
Whichever experiment Nightly tries first with a positive outcome, I expect that's what we'll do, since the difference between #b and #c is not large enough to be worth waiting for a second experiment.
I don't much care which one we choose, to be honest. I kinda doubt that #b will have more compatibility issues than #c[1], and find conceptually cleaner by a thin margin: a non-Date object isn't an invalid Date, it's not a Date. But then again, if you don't want an object to be treated as a Date, don't call Date's toString on it? Implementation-wise, it's pretty much a wash, at least.
I think the most important thing to test is changing Date.prototype to be a non-date (and for that matter changing all the other legacy built-ins that have changed to non-instances). That's the big bet and we should get feedback on.
As far as I can tell, toString being an issue is just a conjecture that hasn't been tested. So, you could even go ahead an implement the corresponding toString methods as currently stands in the ES6 spec. (throw for the wrong kind of object) which probably requires no change to their current implementation.
If we run into an actual toString issue we will have a better idea of which fix may be preferable.
On Tue, Jun 17, 2014 at 11:27 PM, Allen Wirfs-Brock <allen at wirfs-brock.com>
wrote:
On Jun 17, 2014, at 1:41 PM, Till Schneidereit wrote:
On Tue, Jun 17, 2014 at 6:07 PM, Mark Miller <erights at gmail.com> wrote:
I am happy with #b as well, though I prefer #c. I also agree with C. Scott's interpretation of #c, to mean, appropriate degenerate value, which is generally the zero value, but is plausibly NaN for Date.
Whichever experiment Nightly tries first with a positive outcome, I expect that's what we'll do, since the difference between #b and #c is not large enough to be worth waiting for a second experiment.
I don't much care which one we choose, to be honest. I kinda doubt that #b will have more compatibility issues than #c[1], and find conceptually cleaner by a thin margin: a non-Date object isn't an invalid Date, it's not a Date. But then again, if you don't want an object to be treated as a Date, don't call Date's toString on it? Implementation-wise, it's pretty much a wash, at least.
I think the most important thing to test is changing Date.prototype to be a non-date (and for that matter changing all the other legacy built-ins that have changed to non-instances). That's the big bet and we should get feedback on.
Agreed.
As far as I can tell, toString being an issue is just a conjecture that hasn't been tested. So, you could even go ahead an implement the corresponding toString methods as currently stands in the ES6 spec. (throw for the wrong kind of object) which probably requires no change to their current implementation.
I'm somewhat opposed to this for two reasons:
One is that I'm pretty sure that it won't be compatible, whereas I'm optimistic about making Date.prototype a non-Date. There are tens of thousands of people using Nightly as their default browser, and while that makes it a good first target for experiments like this, it also means that we shouldn't do experiments where we're not optimistic about the outcome.
The other is that I still think the standard library shouldn't contain objects that throw when they're string-ified or value-ified.
If we run into an actual toString issue we will have a better idea of which fix may be preferable.
How is that? The script-visible differences between #b and #c would mostly be that Date.prototype.toString would return "[Object Date]" for #b and (as it does now) "Invalid Date" for #c. It's not clear to me how testing the spec status quo would give us any more information about which one of these would be more compatible or preferable on other grounds.
I held back but can't any longer.
Till Schneidereit wrote:
As far as I can tell, toString being an issue is just a conjecture that hasn't been tested. So, you could even go ahead an implement the corresponding toString methods as currently stands in the ES6 spec. (throw for the wrong kind of object) which probably requires no change to their current implementation.
I'm somewhat opposed to this for two reasons:
One is that I'm pretty sure that it won't be compatible, whereas I'm optimistic about making Date.prototype a non-Date. There are tens of thousands of people using Nightly as their default browser, and while that makes it a good first target for experiments like this, it also means that we shouldn't do experiments where we're not optimistic about the outcome.
The other is that I still think the standard library shouldn't contain objects that throw when they're string-ified or value-ified.
Agreed on both points.
Changing Date.prototype from how it has stringified for (now) over 19 years [1] takes unknown probabiltiy times non-trivial cost risk, and Firefox Nightly won't be enough to find the content that breaks (sorry). Other engines would need to test and even put into release channels the change, and then we'd hope some smart site bug diag guru figures out the problem, if there is a problem.
Best way to avoid this is to avoid it. What's the profit in (b)?
If we run into an actual toString issue we will have a better idea of which fix may be preferable.
How is that? The script-visible differences between #b and #c would mostly be that Date.prototype.toString would return "[Object Date]"
(Lower-case "object" there.)
for #b and (as it does now) "Invalid Date" for #c. It's not clear to me how testing the spec status quo would give us any more information about which one of these would be more compatible or preferable on other grounds.
Just use "Invalid Date" and get on with more important work. My 2 cents.
/be
[1] SpiderMonkey session:
js> Date.prototype.toString() "Invalid Date" js> Date.prototype.toSource() "(new Date(NaN))"
On Wed, Jun 18, 2014 at 1:04 PM, Brendan Eich <brendan at mozilla.org> wrote:
Just use "Invalid Date" and get on with more important work. My 2 cents.
Endorse. Can we get that added to the spec as normative text? ;-)
On Jun 18, 2014, at 11:04 AM, Brendan Eich wrote:
I held back but can't any longer.
Till Schneidereit wrote:
As far as I can tell, toString being an issue is just a conjecture that hasn't been tested. So, you could even go ahead an implement the corresponding toString methods as currently stands in the ES6 spec. (throw for the wrong kind of object) which probably requires no change to their current implementation.
I'm somewhat opposed to this for two reasons:
One is that I'm pretty sure that it won't be compatible, whereas I'm optimistic about making Date.prototype a non-Date. There are tens of thousands of people using Nightly as their default browser, and while that makes it a good first target for experiments like this, it also means that we shouldn't do experiments where we're not optimistic about the outcome.
The other is that I still think the standard library shouldn't contain objects that throw when they're string-ified or value-ified.
Agreed on both points.
Changing Date.prototype from how it has stringified for (now) over 19 years [1] takes unknown probabiltiy times non-trivial cost risk, and Firefox Nightly won't be enough to find the content that breaks (sorry). Other engines would need to test and even put into release channels the change, and then we'd hope some smart site bug diag guru figures out the problem, if there is a problem.
Best way to avoid this is to avoid it. What's the profit in (b)?
Mostly about establishing the pattern for what should be done for other toString methods that are applied to the wrong kind of object or a non-instance prototype. Not every object that could get a custom toString has a natural "0-value".
For example, what should Symbol.prototype.toString() do? (b) sounds like a good choice for it. (c) is certainly ok, for Date.prototype but but we have other cases that need to be addressed.
Allen Wirfs-Brock wrote:
Mostly about establishing the pattern for what should be done for other toString methods that are applied to the wrong kind of object or a non-instance prototype. Not every object that could get a custom toString has a natural "0-value".
Don't let the tail wag the dog here.
That's on top of "avoid risk you can't size easily and don't need".
For example, what should Symbol.prototype.toString() do? (b) sounds like a good choice for it. (c) is certainly ok, for Date.prototype but but we have other cases that need to be addressed.
Symbol.prototype could even throw, it's such an outlier. Throw if no "zero" might go a long way, cover most of the dog.
On Wed, Jun 18, 2014 at 2:38 PM, Brendan Eich <brendan at mozilla.org> wrote:
[...] Throw if no "zero" might go a long way, cover most of the dog.
Is there a cartoonist in the house?
On Jun 18, 2014, at 2:38 PM, Brendan Eich wrote:
For example, what should Symbol.prototype.toString() do? (b) sounds like a good choice for it. (c) is certainly ok, for Date.prototype but but we have other cases that need to be addressed.
Symbol.prototype could even throw, it's such an outlier. Throw if no "zero" might go a long way, cover most of the dog.
The spec. current says throw for this Symbol.prototype case. The (reasonable) opposing view is that toString should never throw. Other than the protoype-is-not-an-instance it all about unlikely edge cases where toString methods are applied to the wrong kind of object.
Allen Wirfs-Brock wrote:
The spec. current says throw for this Symbol.prototype case. The (reasonable) opposing view is that toString should never throw. Other than the protoype-is-not-an-instance it all about unlikely edge cases where toString methods are applied to the wrong kind of object.
js> Object.create(null).toString()
typein:1:0 TypeError: Object.create(...).toString is not a function
It happens. Better to catch that error (Symbol.prototype flowing into an implicit conversion) early?
On Jun 18, 2014, at 4:12 PM, Brendan Eich wrote:
Allen Wirfs-Brock wrote:
The spec. current says throw for this Symbol.prototype case. The (reasonable) opposing view is that toString should never throw. Other than the protoype-is-not-an-instance it all about unlikely edge cases where toString methods are applied to the wrong kind of object.
js> Object.create(null).toString() typein:1:0 TypeError: Object.create(...).toString is not a function
It happens. Better to catch that error (Symbol.prototype flowing into an implicit conversion) early?
which is pretty much the approach the ES6 spec. has taken WRT toString up to now. Tension between catching invalid iplicit toString conversions and reliable toString for debugging.
At any rate, run time tools can really depend upon toString working, They probably should use something like:
function reliableToString(obj) { try {return obj.toString()} catch (e) { try {return {}.toString.call(obj)} catch (f) { return "[ object ???]"} } } }
Allen Wirfs-Brock wrote:
"[ object ???]"
"[object WTF]"
:
On Thu, Jun 19, 2014 at 1:39 AM, Allen Wirfs-Brock <allen at wirfs-brock.com>
wrote:
On Jun 18, 2014, at 4:12 PM, Brendan Eich wrote:
Allen Wirfs-Brock wrote:
The spec. current says throw for this Symbol.prototype case. The (reasonable) opposing view is that toString should never throw. Other than the protoype-is-not-an-instance it all about unlikely edge cases where toString methods are applied to the wrong kind of object.
js> Object.create(null).toString() typein:1:0 TypeError: Object.create(...).toString is not a function
It happens. Better to catch that error (Symbol.prototype flowing into an implicit conversion) early?
which is pretty much the approach the ES6 spec. has taken WRT toString up to now. Tension between catching invalid iplicit toString conversions and reliable toString for debugging.
I'd posit that there are three different cases for toString:
- (explictly or implicitly) calling foo.toString, where
foo
is any of the objects that, by default, exist in a JS global without any user code having run - (explicitly or implicitly) calling foo.toString, where
foo
is some content script-generated object - explicitly calling bar.toString.call(foo), where
foo
andbar
are arbitrary objects
The current (ES5) state of things is that 1) can never throw, while both 2) and 3) might. The current ES6 draft would move 1) over into the latter camp. I think that is the crucial thing to prevent. It's just wrong for the language to essentially say "here's a bunch of objects. They have methods. But don't call all of them. Finding out which ones aren't allowed to be called is left as an excercise to the reader."
Theoretically, Scott's #d still hold the most appeal to me. However, I don't think it works in a multi-Realm world, hence "theoretically". Given that, it's #b or #c, where, again, I don't much care which one it'll be.
At any rate, run time tools can really depend upon toString working, They probably should use something like:
function reliableToString(obj) { try {return obj.toString()} catch (e) { try {return {}.toString.call(obj)} catch (f) { return "[ object ???]"} } } }
True. It's not clear to me that the web as a whole knows this, though, and the current state of the spec might teach it the hard way.
Can i bring back the good old JScript "unknown"? :P
Sent from my Windows Phone From: Brendan Eich Sent: 6/18/2014 17:15 To: Allen Wirfs-Brock Cc: Mark S. Miller; Mark Miller; Erik Arvidsson; Jason Orendorff; es-discuss at mozilla.org list Subject: Re: Standard builtins' prototypes and toString Allen Wirfs-Brock wrote:
"[ object ???]"
"[object WTF]"
:
On Jun 19, 2014, at 3:18 AM, Till Schneidereit wrote:
On Thu, Jun 19, 2014 at 1:39 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
On Jun 18, 2014, at 4:12 PM, Brendan Eich wrote:
Allen Wirfs-Brock wrote:
The spec. current says throw for this Symbol.prototype case. The (reasonable) opposing view is that toString should never throw. Other than the protoype-is-not-an-instance it all about unlikely edge cases where toString methods are applied to the wrong kind of object.
js> Object.create(null).toString() typein:1:0 TypeError: Object.create(...).toString is not a function
It happens. Better to catch that error (Symbol.prototype flowing into an implicit conversion) early?
which is pretty much the approach the ES6 spec. has taken WRT toString up to now. Tension between catching invalid iplicit toString conversions and reliable toString for debugging.
I'd posit that there are three different cases for toString:
- (explictly or implicitly) calling foo.toString, where
foo
is any of the objects that, by default, exist in a JS global without any user code having run- (explicitly or implicitly) calling foo.toString, where
foo
is some content script-generated object- explicitly calling bar.toString.call(foo), where
foo
andbar
are arbitrary objectsThe current (ES5) state of things is that 1) can never throw, while both 2) and 3) might. The current ES6 draft would move 1) over into the latter camp. I think that is the crucial thing to prevent. It's just wrong for the language to essentially say "here's a bunch of objects. They have methods. But don't call all of them. Finding out which ones aren't allowed to be called is left as an excercise to the reader."
Theoretically, Scott's #d still hold the most appeal to me. However, I don't think it works in a multi-Realm world, hence "theoretically". Given that, it's #b or #c, where, again, I don't much care which one it'll be.
Or even in a single Realm world, it isn't clear how: (class extends Date {}).prototype.toString() would recognize that is is dealing with a prototype rather than a Date instance.
I basically agree with you conclusion. An the conserve thing to do is to try to minimize legacy compat. To me, that says that for Date, Number, Boolean, String, RegExp we should do (c). For new kinds of objects that don't exist in ES5 and which have internal state dependent toStrings we should do (b).
I'll update the spec. accordingly.
Note that for the (c) cases this introduces a different breaking change: ES5 specifies that String.prototype.toString.call({}) throws a TypeError but the (c) based change will return the empty string for that case. Hopefully, that is a change the web can live with.
While we're on the topic, shall we discuss valueOf()
? Should it get the
same treatment?
On Thu, Jun 19, 2014 at 7:46 PM, C. Scott Ananian <ecmascript at cscott.net>
wrote:
While we're on the topic, shall we discuss
valueOf()
? Should it get the same treatment? --scott
Oh, yes: I always considered the toString
discussion to really be about
valueOf
, too.
On Jun 19, 2014 1:28 PM, "Allen Wirfs-Brock" <allen at wirfs-brock.com> wrote:
On Jun 19, 2014, at 3:18 AM, Till Schneidereit wrote:
On Thu, Jun 19, 2014 at 1:39 AM, Allen Wirfs-Brock <allen at wirfs-brock.com
wrote:
On Jun 18, 2014, at 4:12 PM, Brendan Eich wrote:
Allen Wirfs-Brock wrote:
The spec. current says throw for this Symbol.prototype case. The (reasonable) opposing view is that toString should never throw. Other than the protoype-is-not-an-instance it all about unlikely edge cases where toString methods are applied to the wrong kind of object.
js> Object.create(null).toString() typein:1:0 TypeError: Object.create(...).toString is not a function
It happens. Better to catch that error (Symbol.prototype flowing into an implicit conversion) early?
which is pretty much the approach the ES6 spec. has taken WRT toString up to now. Tension between catching invalid iplicit toString conversions and reliable toString for debugging.
I'd posit that there are three different cases for toString:
- (explictly or implicitly) calling foo.toString, where
foo
is any of the objects that, by default, exist in a JS global without any user code having run- (explicitly or implicitly) calling foo.toString, where
foo
is some content script-generated object- explicitly calling bar.toString.call(foo), where
foo
andbar
are arbitrary objectsThe current (ES5) state of things is that 1) can never throw, while both 2) and 3) might. The current ES6 draft would move 1) over into the latter camp. I think that is the crucial thing to prevent. It's just wrong for the language to essentially say "here's a bunch of objects. They have methods. But don't call all of them. Finding out which ones aren't allowed to be called is left as an excercise to the reader."
Theoretically, Scott's #d still hold the most appeal to me. However, I don't think it works in a multi-Realm world, hence "theoretically". Given that, it's #b or #c, where, again, I don't much care which one it'll be.
Or even in a single Realm world, it isn't clear how: (class extends Date {}).prototype.toString() would recognize that is is dealing with a prototype rather than a Date instance.
I basically agree with you conclusion. An the conserve thing to do is to try to minimize legacy compat. To me, that says that for Date, Number, Boolean, String, RegExp we should do (c). For new kinds of objects that don't exist in ES5 and which have internal state dependent toStrings we should do (b).
I'll update the spec. accordingly.
Note that for the (c) cases this introduces a different breaking change: ES5 specifies that String.prototype.toString.call({}) throws a TypeError but the (c) based change will return the empty string for that case. Hopefully, that is a change the web can live with.
Same for Date.prototype.toString({}) as mentioned above, yes. I'm cautiously optimistic about these changes.
On Jun 19, 2014, at 10:46 AM, C. Scott Ananian wrote:
While we're on the topic, shall we discuss
valueOf()
? Should it get the same treatment? --scott
I don't think so. To me, toString is a special case because it is explicitly applied so often, particularly for debugging purposes.
But valueOf is primarily invoked for implicit conversions in expressions.
Like I described earlier in this thread, TC39's assumption when it agreed to this ES design change was that the web did not generally depend upon using these existing ES3 generation (or earlier) prototypes as instance objects. In the case where we know that wasn't true (Function.prototype) we intentionally did not change the prototype to being a non-instance object. We also agreed if we discovered other such dependencies we would change those prototypes back to being instance objects.
Discovering that one of these prototypes objects were being routinely used in expression contexts where valueOf was being implicitly applied would be evidence that that prototype needed to be handled in the legacy manner. But, we don't have any new evidence of that being the case. Until there is some evidence, there isn't any reason to second guess the original analysis and TC39's decision.
On Thu, Jun 19, 2014 at 8:16 PM, Allen Wirfs-Brock <allen at wirfs-brock.com>
wrote:
On Jun 19, 2014, at 10:46 AM, C. Scott Ananian wrote:
While we're on the topic, shall we discuss
valueOf()
? Should it get the same treatment? --scottI don't think so. To me, toString is a special case because it is explicitly applied so often, particularly for debugging purposes.
But valueOf is primarily invoked for implicit conversions in expressions.
Like I described earlier in this thread, TC39's assumption when it agreed to this ES design change was that the web did not generally depend upon using these existing ES3 generation (or earlier) prototypes as instance objects. In the case where we know that wasn't true (Function.prototype) we intentionally did not change the prototype to being a non-instance object. We also agreed if we discovered other such dependencies we would change those prototypes back to being instance objects.
Discovering that one of these prototypes objects were being routinely used in expression contexts where valueOf was being implicitly applied would be evidence that that prototype needed to be handled in the legacy manner. But, we don't have any new evidence of that being the case. Until there is some evidence, there isn't any reason to second guess the original analysis and TC39's decision.
Ok, we'll try this in Nightly for Date.prototype and see if the assumption holds. If it does, we'll continue with the rest.
I don't really know if Date.prototype is a good canary, but it's what I have a mostly-finished patch for, and it doesn't seem to make too much of a difference.
While working on changing Date.prototype to be a plain object in SpiderMonkey, we realized that there's an issue: the way things are specced now,
alert(Date.prototype)
will throw, becauseDate.prototype.toString
isn't generic. The same applies for all builtins with non-generictoString
prototype functions.To resolve this, I propose changing these
toString
to first check if thethis
value is %FooPrototype% (e.g., %DatePrototype% in the case at hand) and return the result of the equivalent of calling the original Object.prototype.toString.I'm not sure if that is enough to cover subclasses of these builtins. Will calling
toString
on the prototype ofclass MyDate extends Date{}
still throw? If so, that would at least not be a backwards-compatibility concern, but it's probably also not desirable.till