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

# Mark S. Miller (17 years ago)

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.

# Brendan Eich (17 years ago)

The fix of allowing primitive Reference base type is ok for
implementations such as SpiderMonkey, which already does this for the
built-in methods on String.prototype, Number.prototype, and
Boolean.prototype to avoid the overhead of wrappers when calling
native methods on string, number, and boolean primitive values.

Practical implementations do not use anything like a Reference type,
instead specializing lvalue and rvalue contexts to preserve reference
bases in VM registers.

FWIW, this is what ES4 did too. Types string and String shared a
prototype object; calls on a primitive |this| to methods inherited
from the shared prototype entailed no coercion or wrapping. ES4 did
not use Reference internal types with non-primitive bases to specify
this binding. So the fix is harmonious from the start.

On Jan 8, 2009, at 7:43 PM, Mark S. Miller wrote:

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, ..."

In the face of multiple global objects, the spec must say which
original value of String, e.g., is used, or really, how the right
global object is found. It should be found by following the scope
chain to the last object, the global for the currently active code
(not the same as the global for the caller or grand caller or top
level script that's being evaluated).

Web browser embedding example:

  • Window A script source:

String.prototype.trim = function () { return this.replace(/^\s*(\S*)\s* $/, "$1"); }

function wraptrim(s) { return s.trim(); }

  • Window B script source (assume window A is denoted by winA here):

alert(winA.wraptrim(" hello "));

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.

A strict error for attempting to set an ad-hoc property could be
helpful. All else equal, when using primitives and objects
interchangeably in generic code, the silent case is less likely to be
helpful in my experience.

Generic programming with primtiives more often wants to read
undefined when getting a non-existent property, and "object detect"
without having to try/catch. This common use case is not served by
strict mode in ES3.1, right? You'd have to change code to try/catch
around such probes.

# Mark S. Miller (17 years ago)

On Thu, Jan 8, 2009 at 9:14 PM, Brendan Eich <brendan at mozilla.com> wrote:

[lots of agreement]

Wonderful!

Generic programming with primtiives more often wants to read undefined when getting a non-existent property, and "object detect" without having to try/catch. This common use case is not served by strict mode in ES3.1, right? You'd have to change code to try/catch around such probes.

ES3.1-strict and nonstrict both return undefined from a failed read for exactly this reason, as does Caja. The feature testing pattern is pervasive and we dared not break it. The rationale is that a failed read is not silent n practice, and so is adequately non-hazardous for integrity. It returns an undefined, which is likely to be noticed, since the operation in question is, after all, a read.

This rationale does not apply to "delete" returning false, since in practice much code calls delete, assumes success, and does not check the return value. In the face of new user-defined non-configurable properties, failed deletes will be more common, so this hazard becomes worse. So a failed strict delete does throw. You do indeed have to place try/catches around strict deletes to simulate their current behavior.

# Brendan Eich (17 years ago)

On Jan 8, 2009, at 9:27 PM, Mark S. Miller wrote:

On Thu, Jan 8, 2009 at 9:14 PM, Brendan Eich <brendan at mozilla.com>
wrote: [lots of agreement]

Wonderful!

Generic programming with primtiives more often wants to read
undefined when getting a non-existent property, and "object detect"
without having to try/catch. This common use case is not served by
strict mode in ES3.1, right? You'd have to change code to try/catch
around such probes.

ES3.1-strict and nonstrict both return undefined from a failed read
for exactly this reason, as does Caja. The feature testing pattern
is pervasive and we dared not break it. The rationale is that a
failed read is not silent n practice, and so is adequately non- hazardous for integrity. It returns an undefined, which is likely to
be noticed, since the operation in question is, after all, a read.

Oh good -- I misremembered.

This rationale does not apply to "delete" returning false, since in
practice much code calls delete, assumes success, and does not check
the return value. In the face of new user-defined non-configurable
properties, failed deletes will be more common, so this hazard
becomes worse. So a failed strict delete does throw. You do indeed
have to place try/catches around strict deletes to simulate their
current behavior.

It's hard to care, frankly. The Netscape 2 JS1.0 implementation had,
if memory serves, delete as a statement, not an operator. :-P

The rationale "we dare not break" applies to other cases of making the
strict mode migration tax too high. This came up in the wiki just now,
and I've commented in-line at

meetings:minutes_dec_18_2008

Spin-off thread fodder, change of subject appropriate if you take my
bait ;-).