ES3.1-strict spec bug & proposed fix: method calls on primitives.

Mark S. Miller erights at google.com
Thu Jan 8 19:43:34 PST 2009


Currently, according to the draft ES3.1 spec

    String.prototype.reverse = function() {
      "use strict";
      if (this === '') { return this; }
      return this.substring(1).reverse() + this.charAt(0);
    };

    String.prototype.reverse.call("hello") // 1) works
    "hello".reverse.call("hello") // 2) works
    "hello".reverse() // 3) doesn't work -- infinite loop.

What's going on here is that ES3.1-nonstrict functions coerce their this
argument, but ES3.1-strict functions do not. Function.prototype.call does
not itself coerce its first argument -- to be bound to this in the invoked
function -- so that the function can coerce it or not according to the
function's strictness. This explains the behavior of #1 and #2.

However, #3 should that the current draft spec broke an important algebraic
regularity that holds in ES3 and ES3.1-nonstrict, to whit:

Given that Function.prototype.call and Function.prototype.apply have not
been overridden or replaced, that x is a variable, and that x.foo is a
function, then

    x.foo(a,b) ==== x.foo.call(x, a, b) ==== x.foo.apply(x, [a, b])

where "====" is meta-linguistic equivalence.

In ES3.1-nonstrict or ES3 as extended by several libraries such as
prototype, these are also equivalent to

    ... ==== x.foo.bind(x)(a, b) ==== x.foo.bind(x, a)(b) ==== x.foo.bind(x,
a, b)()

These algebraic properties are useful and should not be broken lightly. As
the advocate of the change causing this breakage, I can assure everyone that
this regularity wasn't broken intentionally. However, I am bouncing this off
the list first before filing a trac ticket in case there is any controversy
about whether this should indeed be considered a bug, or whether it should
be fixed in ES3.1. From the ES3.1 phone call where this was discussed, we
are especially concerned about the possible implementation of fixing this.
Pratap & Allen will check with the JScript team. If other ES3.1
implementation efforts, or anyone else, have any objections to the following
specification bug fix, please speak up now.



                                The Bug


The current ES3.1 draft spec evaluates "hello".reverse to a reference whose
property name is "reverse", and whose base is obtained from the operation
ToObject("hello"). In other words, the evaluation of a "." or index
expression to a reference needlessly coerces its base to an object (11.2.1).
As a result, even though strict functions avoid coercing their this-value,
it's too late. The coerced empty string bound to "this" is not === a
primitive empty string.



                                The Fix


As far as we can tell (both in the ES3.1 phone call and in a separate
conversation with Waldemar), we can simply fix the spec to not do this
coercion here. This of course changes the contract of GetBase(ref), since it
can now return a primitive. We must inspect all places where it's used in
the spec to see what adjustments we need to make. The relevant places are

8.7.1 GetValue,
8.7.2 PutValue,
11.2.3 Function Calls
11.4.1 The delete Operator

isUnresolvableReference() also needs to use GetBase(), but I can't find any
definition of isUnresolvableReference() in the 22dec08 draft spec. Is there
a trac ticket on this?


We'd change the spec of GetValue and delete to do a ToObject() on its base
in order to look up the property. However, this coercion of the base is
ephemeral. The wrapper cannot escape, and ToObject has no observable side
effects, so it is just an explanatory device equivalent to saying: "If the
base is a string, we lookup the property on String.prototype. If it's a
number, ..."

isUnresolvableReference() shouldn't care about the difference between a
primitive value and an object, so it can be defined however it would anyway
have been defined.

The current bug would then be fixed merely by not changing the definition of
Function Calls. The uncoerced base would be passed as the this-value of the
called function.



                     Another Wrinkle


Pratap pointed out the remaining issue of PutValue. In the current ES3.1
draft

    3.foo = 9

is generally a no op, with one exception: If Number.prototype.foo is an
accessor property with a setter. In that weird case, then the inherited
setter is invoked. As with method calls, the setter should be invoked with
the primitive 3, not a wrapped 3. Again, as with methods, if the setter is
non-strict, the 3 will be coerced anyway so none of this makes any
observable difference. But a strict setter should see the actual base value
on which the assignment happened.

Ok, what it there is no inherited accessor? In ES3.1-nonstrict, this should
remain a no op. However, there are two equivalent means of explaining this
no op behavior: 1) mutating an unobservable wrapper, or 2) a failed
assignment. Recall that nonstrict failed assignments are silent, so either
explanation suffices. However, if we use explanation #2, we give ourselves
license to have this fail with a thrown error in strict code.

I am in favor of both the basic fix + this wrinkle. But if this wrinkle
turns out to be objectionable, we can adopt the base fix without this
wrinkle.



                An Extra Dividend


I've just skimmed all the occurrences of ToObject in the spec. (Waldemar,
you were right. There aren't that many.) AFAICT, if we make the fixes above,
new programmers who program only in the strict language, and interact only
with strict code, will completely avoid implicit wrapping. They can of
course still wrap explicitly using the Object constructor. But all remaining
uses of ToObject in the strict language are explanatory devices, as above,
where the wrapper object does not escape. If it does not escape, it need not
actually be allocated, potentially providing a performance win as well.

If purely strict programmers don't want wrappers, they can stop worrying
about them. In Crock's taxonomy (see "JavaScript, The Good Parts"), this
demotes wrappers from awful parts to merely bad parts.

-- 
   Cheers,
   --MarkM
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20090108/8f5172bd/attachment.html>


More information about the Es-discuss mailing list