PUTPROP: exposed property put algorithm
Background
Properties are written in ECMAScript code in many contexts, e.g.:
foo.bar = "quux";
A property put expression in ECMAScript code involves:
- A property accessor reference (E5 Section 11.2.1)
- A
PutValue()
call (E5 Section 8.7.2) - A
[[Put]]
call (or aPutValue()
specific variant)
The property accessor coercions are the same as for GetValue
:
- The base reference is checked with
CheckObjectCoercible()
- The property name is coerced to a string
The PutValue()
call is simple:
- If the base reference is primitive, it is coerced to an object, and a exotic variant of
[[Put]]
is used. - Otherwise, standard
[[Put]]
is used.
The variant [[Put]]
for a primitive base value differs from the standard [[Put]]
as follows:
- If the coerced temporary object has a matching own data property, the put is explicitly rejected (steps 3-4 of the variant algorithm), regardless of the property attributes (especially, writability). Compare this to the standard
[[Put]]
behavior in E5 Section 8.12.5, steps 2-3 which simply attempts to update the data property, provided that the property is writable. - If the property is found (either in the temporary object or its ancestors) and is a setter, the setter call
this
binding is the primitive value, not the coerced value. (An own accessor property should never be found in practice, as the only possible coerced object types asBoolean
,Number
, andString
.)
Like GetValue()
, we could skip creation of the coerced object, but don't take advantage of this now.
Note: if the base reference is a primitive value, the coerced object is temporary and never exposed to user code. Some implementations (like V8) omit a property write entirely if the base value is primitive. This can be observed by lack of side effects, e.g. no setter call occurs when it should:
// add test getter
Object.defineProperty(String.prototype, 'test', {
get: function() { print(typeof this); },
set: function(x) { print(typeof this); },
});
"foo".test = "bar"; // prints 'string'
V8 will print nothing, while Rhino and Smjs print 'object' (which is also not correct).
First draft
The relevant part begins after that in steps 5-8, which first perform some coercions and then create a property accessor. The accessor is then acted upon by PutValue()
, and ultimately [[Put]]
or its variant.
Combining all of these, we get the first draft (for base value O
and property name value P
):
- Let
orig
beO
. (Remember the uncoerced original for a possible setter call.) - Call
CheckObjectCoercible
withO
as argument. In practice: ifO
isnull
orundefined
, throw aTypeError
. (Note: this is unconditional.) - Let
P
beToString(P)
. (This may have side effects ifP
is an object.) - If
O
is not an object, letcoerced
betrue
, else letcoerced
befalse
. - Let
O
beToObject(O)
. (This is side effect free.) - Let
curr
beO
. - NEXT: Let
desc
be the result of calling the[[GetOwnProperty]]
internal method ofcurr
with property nameP
. - If
desc
isundefined
: a. Letcurr
be the value of the[[Prototype]]
internal property ofcurr
. b. Ifcurr
is notnull
, goto NEXT. c. Ifcoerced
istrue
, Reject. d. IfO.[[Extensible]]
isfalse
, Reject. e. LetnewDesc
be a property descriptor with values: -[[Value]]: V
-[[Writable]]: true
-[[Enumerable]]: true
-[[Configurable]]: true}
f. CallO.[[DefineOwnProperty]](P, newDesc, Throw)
. g. Return. - If
IsDataDescriptor(desc)
: a. Ifcoerced
istrue
, Reject. b. Ifcurr
!=O
(property is an inherited data property): (Note: assumes there are no prototype loops.) 1. IfO.[[Extensible]
isfalse
, Reject. 2. Ifdesc.[[Writable]]
isfalse
, Reject. 3. LetnewDesc
be a property descriptor with values: -[[Value]]: V
-[[Writable]]: true
-[[Enumerable]]: true
-[[Configurable]]: true}
4. CallO.[[DefineOwnProperty]](P, newDesc, Throw)
. c. Else (property is an own data property): 1. Ifdesc.[[Writable]]
isfalse
, Reject. 2. LetvalueDesc
be{ [[Value]]: V }
. 3. CallO.[[DefineOwnProperty]](P, valueDesc, Throw)
. - Else (property is an accessor): a. If
desc.[[Set]]
isundefined
, Reject. b. Call the[[Call]]
internal method ofdesc.[[Set]]
providingorig
as thethis
value and providingV
as the sole argument. (Note: the difference to a basic[[Put]]
is that the setterthis
binding is the original, uncoerced object.) - Return.
Notes:
Steps 2-3 come from the property accessor evaluation rules in E5 Section 11.2.1. In particular,
CheckObjectCoercible()
is called before the key is coerced to a string. Since the key string coercion may have side effects, the order of evaluation matters.Note that
ToObject()
has no side effects (this can be seen from a case by case inspection), so steps 3 and 4-5 can be reversed.Step 10.b uses the original object (not the coerced object) as the setter
this
binding (E5 Section 8.7.2, step 6 of the variant[[Put]]
algorithm).Steps 8.c and 9.a reject attempt to update or create a data property on a temporary object (E5 Section 8.7.2, steps 4 and 7 of the variant
[[Put]]
algorithm). Note that the "coerced" check is not actually needed to guard step 9.c (step 4 of the variant[[Put]]
) because the only coerced object with own properties is theString
object, and all its own properties are non-writable and thus caught by step 9.c.1 anyway. This might of course change in a future version, or be untrue for some out-of-spec coercion behavior for custom types. The pre-check is needed to avoid creating a new property on the temporary object, though.An explicit
coerced
flag is not needed: we can simply check whether or notorig
is an object.Since
curr
is used for prototype chain walking, we don't need to storeorig
(O
can be used for that instead).
Cleaning up
- Call
CheckObjectCoercible
withO
as argument. In practice: ifO
isnull
orundefined
, throw aTypeError
. (Note: this is unconditional.) - Let
curr
beToObject(O)
. (This is side effect free.) - Let
P
beToString(P)
. (This may have side effects ifP
is an object.) - NEXT: Let
desc
be the result of calling the[[GetOwnProperty]]
internal method ofcurr
with property nameP
. - If
desc
isundefined
: a. Letcurr
be the value of the[[Prototype]]
internal property ofcurr
. b. Ifcurr
is notnull
, goto NEXT. c. IfO
is not an object (was coerced), Reject. d. IfO.[[Extensible]]
isfalse
, Reject. e. LetnewDesc
be a property descriptor with values: -[[Value]]: V
-[[Writable]]: true
-[[Enumerable]]: true
-[[Configurable]]: true}
f. CallO.[[DefineOwnProperty]](P, newDesc, Throw)
. g. Return. - If
IsDataDescriptor(desc)
: a. IfO
is not an object (was coerced), Reject. b. Ifcurr
!=O
(property is an inherited data property): (Note: assumes there are no prototype loops.) 1. IfO.[[Extensible]
isfalse
, Reject. 2. Ifdesc.[[Writable]]
isfalse
, Reject. 3. LetnewDesc
be a property descriptor with values: -[[Value]]: V
-[[Writable]]: true
-[[Enumerable]]: true
-[[Configurable]]: true}
4. CallO.[[DefineOwnProperty]](P, newDesc, Throw)
. c. Else (property is an own data property): 1. Ifdesc.[[Writable]]
isfalse
, Reject. 2. LetvalueDesc
be{ [[Value]]: V }
. 3. CallO.[[DefineOwnProperty]](P, valueDesc, Throw)
. - Else (property is an accessor): a. If
desc.[[Set]]
isundefined
, Reject. b. Call the[[Call]]
internal method ofdesc.[[Set]]
providingO
as thethis
value and providingV
as the sole argument. (Note: the difference to a basic[[Put]]
is that the setterthis
binding is the original, uncoerced object.) - Return.
Inlining DefineOwnProperty calls
The [[Put]]
uses two different calls to [[DefineOwnProperty]]
: one to update an existing property [[Value]]
and another to create a brand new data property. These can be inlined into the algorithm as follows (see the section on preliminary algorithm work).
Before inlining, the cases for "update old property" and "create new property" are isolated into goto labels (as there are two places where a new property is created). The [[DefineOwnProperty]]
calls with exotic behaviors inlined are then substituted. "Reject" is also made an explicit label.
The resulting algorithm is:
- Call
CheckObjectCoercible
withO
as argument. In practice: ifO
isnull
orundefined
, throw aTypeError
. (Note: this is unconditional.) - Let
curr
beToObject(O)
. (This is side effect free.) - Let
P
beToString(P)
. (This may have side effects ifP
is an object.) - NEXT: Let
desc
be the result of calling the[[GetOwnProperty]]
internal method ofcurr
with property nameP
. - If
desc
isundefined
: a. Letcurr
be the value of the[[Prototype]]
internal property ofcurr
. b. Ifcurr
is notnull
, goto NEXT. c. IfO
is not an object (was coerced), goto REJECT. d. IfO.[[Extensible]]
isfalse
, goto REJECT. e. Goto NEWPROP. - If
IsDataDescriptor(desc)
: a. IfO
is not an object (was coerced), goto REJECT. b. Ifcurr
!=O
(property is an inherited data property): (Note: assumes there are no prototype loops.) 1. IfO.[[Extensible]
isfalse
, goto REJECT. 2. Ifdesc.[[Writable]]
isfalse
, goto REJECT. 3. Goto NEWPROP. c. Else (property is an own data property): 1. Ifdesc.[[Writable]]
isfalse
, goto REJECT. 2. Goto UPDATEPROP. - Else (property is an accessor): a. If
desc.[[Set]]
isundefined
, goto REJECT. b. Call the[[Call]]
internal method ofdesc.[[Set]]
providingO
as thethis
value and providingV
as the sole argument. (Note: the difference to a basic[[Put]]
is that the setterthis
binding is the original, uncoerced object.) c. Return. - UPDATEPROP: (Inlined
[[DefineOwnProperty]]
call for existing property.) IfO
is anArray
object, andP
is"length"
, then: a. LetnewLen
beToUint32(V)
. b. IfnewLen
is not equal toToNumber(V)
, goto REJECTRANGE. c. LetoldLenDesc
be the result of calling the[[GetOwnProperty]]
internal method ofO
passing"length"
as the argument. The result will never beundefined
or an accessor descriptor becauseArray
objects are created with alength
data property that cannot be deleted or reconfigured. d. LetoldLen
beoldLenDesc.[[Value]]
. (Note thatoldLen
is guaranteed to be a unsigned 32-bit integer.) e. IfnewLen
<oldLen
, then: 1. LetshortenSucceeded
,finalLen
be the result of calling the internal helperShortenArray()
witholdLen
andnewLen
. 2. Update the property ("length"
) value tofinalLen
. 3. Goto REJECT, ifshortenSucceeded
isfalse
. 4. Return. f. Update the property ("length"
) value tonewLen
. g. Return. - Set the
[[Value]]
attribute of the property namedP
of objectO
toV
. (Since it is side effect free to update the value with the same value, no check for that case is needed.) - If
O
is an arguments object which has a[[ParameterMap]]
internal property: a. Letmap
be the value of the[[ParameterMap]]
internal property of the arguments object. b. If the result of calling the[[GetOwnProperty]]
internal method ofmap
passingP
as the argument is notundefined
, then: 1. Call the[[Put]]
internal method ofmap
passingP
,V
, andThrow
as the arguments. (This updates the bound variable value.) - Return.
- NEWPROP: (Inlined
[[DefineOwnProperty]]
call for new property.) IfO
is anArray
object andP
is an array index (E5 Section 15.4), then: a. LetoldLenDesc
be the result of calling the[[GetOwnProperty]]
internal method ofO
passing"length"
as the argument. The result will never beundefined
or an accessor descriptor becauseArray
objects are created with a length data property that cannot be deleted or reconfigured. b. LetoldLen
beoldLenDesc.[[Value]]
. (Note thatoldLen
is guaranteed to be a unsigned 32-bit integer.) c. Letindex
beToUint32(P)
. d. Ifindex
>=oldLen
: 1. Goto REJECToldLenDesc.[[Writable]]
isfalse
. 2. Update the"length"
property ofO
to the valueindex + 1
. This always succeeds. - Create an own data property named
P
of objectO
whose attributes are:[[Value]]: V
[[Writable]]: true
[[Enumerable]]: true
[[Configurable]]: true
- Return.
- REJECT: If
Throw
istrue
, then throw aTypeError
exception, otherwise return. - REJECTRANGE: Throw a
RangeError
exception. (This is unconditional.)
Notes:
- In step 8, we don't need to check for array index updates: the property already exists, so array
length
will not need an update. - In step 8, the original
[[DefineOwnProperty]]
exotic behavior is split into a pre-step and a post-step because the"length"
write may fail. However, because we've inlined[[CanPut]]
, we know that the write will succeed, so both the pre- and post-behaviors can be handled in step 8 internally. - In step 8, we don't need to check for arguments exotic behavior, as only number-like indices have magic bindings (not
"length"
). - In steps 12-14, we don't need to check for arguments exotic behavior: any "magically bound" property must always be present in the arguments object. If a bound property is deleted, the binding is also deleted from the argument parameter map.
- In step 12, we don't need to check for
length
exotic behavior: thelength
property always exists for arrays so we cannot get here with arrays.
Avoiding temporary objects
As for GetValue()
the only cases where temporary objects are created are for Boolean
, Number
, and String
. The PutValue()
algorithm rejects a property write on a temporary object if a new data property were to be created or an existing one updated.
For the possible coerced values, the own properties are:
Boolean
: noneNumber
: noneString
:"length"
and index properties for string characters
These can be checked explicitly when coercing (and reject the attempt before going forwards). However, PutValue()
does allow a property write if an ancestor contains a setter which "captures" the write so that the temporary object would not be written to. Although the built-in prototype chains do not contain such setters, they can be added by user code at run time, so they do need to be checked for.
Avoiding temporaries altogether:
- Check and/or coerce
O
as follows: a. IfO
isnull
orundefined
, throw aTypeError
. (This is theCheckObjectCoercible
part; the throw is unconditional.) b. IfO
is a boolean: setcurr
to the built-inBoolean
prototype object (skip creation of temporary) c. Else ifO
is a number: setcurr
to the built-inNumber
prototype object (skip creation of temporary) d. Else ifO
is a string: 1. SetP
toToString(P)
. (This may have side effects ifP
is an object.) 2. IfP
islength
, goto REJECT. 3. IfP
is a valid array index within the string length, goto REJECT. 4. Setcurr
to the built-inString
prototype object (skip creation of temporary) 5. Goto NEXT. (Avoid double coercion ofP
.) e. Else ifO
is an object: setcurr
toO
. f. Else, Throw aTypeError
. (Note that this case should not happen, as steps a-e are exhaustive. However, this step is useful as a fallback, and for handling any internal types.) - Let
P
beToString(P)
. (This may have side effects ifP
is an object.) - NEXT: Let
desc
be the result of calling the[[GetOwnProperty]]
internal method ofcurr
with property nameP
. - If
desc
isundefined
: a. Letcurr
be the value of the[[Prototype]]
internal property ofcurr
. b. Ifcurr
is notnull
, goto NEXT. c. IfO
is not an object (was coerced), goto REJECT. d. IfO.[[Extensible]]
isfalse
, goto REJECT. e. Goto NEWPROP. - If
IsDataDescriptor(desc)
: a. IfO
is not an object (was coerced), goto REJECT. b. Ifcurr
!=O
(property is an inherited data property): (Note: assumes there are no prototype loops.) 1. IfO.[[Extensible]
isfalse
, goto REJECT. 2. Ifdesc.[[Writable]]
isfalse
, goto REJECT. 3. Goto NEWPROP. c. Else (property is an own data property): 1. Ifdesc.[[Writable]]
isfalse
, goto REJECT. 2. Goto UPDATEPROP. - Else (property is an accessor): a. If
desc.[[Set]]
isundefined
, goto REJECT. b. Call the[[Call]]
internal method ofdesc.[[Set]]
providingO
as thethis
value and providingV
as the sole argument. (Note: the difference to a basic[[Put]]
is that the setterthis
binding is the original, uncoerced object.) c. Return. - UPDATEPROP: (Inlined
[[DefineOwnProperty]]
call for existing property.) IfO
is anArray
object, andP
is"length"
, then: a. LetnewLen
beToUint32(V)
. b. IfnewLen
is not equal toToNumber(V)
, goto REJECTRANGE. c. LetoldLenDesc
be the result of calling the[[GetOwnProperty]]
internal method ofO
passing"length"
as the argument. The result will never beundefined
or an accessor descriptor becauseArray
objects are created with alength
data property that cannot be deleted or reconfigured. d. LetoldLen
beoldLenDesc.[[Value]]
. (Note thatoldLen
is guaranteed to be a unsigned 32-bit integer.) e. IfnewLen
<oldLen
, then: 1. LetshortenSucceeded
,finalLen
be the result of calling the internal helperShortenArray()
witholdLen
andnewLen
. 2. Update the property ("length"
) value tofinalLen
. 3. Goto REJECT, ifshortenSucceeded
isfalse
. 4. Return. f. Update the property ("length"
) value tonewLen
. g. Return. - Set the
[[Value]]
attribute of the property namedP
of objectO
toV
. (Since it is side effect free to update the value with the same value, no check for that case is needed.) - If
O
is an arguments object which has a[[ParameterMap]]
internal property: a. Letmap
be the value of the[[ParameterMap]]
internal property of the arguments object. b. If the result of calling the[[GetOwnProperty]]
internal method ofmap
passingP
as the argument is notundefined
, then: 1. Call the[[Put]]
internal method ofmap
passingP
,V
, andThrow
as the arguments. (This updates the bound variable value.) - Return.
- NEWPROP: (Inlined
[[DefineOwnProperty]]
call for new property.) IfO
is anArray
object andP
is an array index (E5 Section 15.4), then: a. LetoldLenDesc
be the result of calling the[[GetOwnProperty]]
internal method ofO
passing"length"
as the argument. The result will never beundefined
or an accessor descriptor becauseArray
objects are created with a length data property that cannot be deleted or reconfigured. b. LetoldLen
beoldLenDesc.[[Value]]
. (Note thatoldLen
is guaranteed to be a unsigned 32-bit integer.) c. Letindex
beToUint32(P)
. d. Ifindex
>=oldLen
: 1. Goto REJECToldLenDesc.[[Writable]]
isfalse
. 2. Update the"length"
property ofO
to the valueindex + 1
. This always succeeds. - Create an own data property named
P
of objectO
whose attributes are:[[Value]]: V
[[Writable]]: true
[[Enumerable]]: true
[[Configurable]]: true
- Return.
- REJECT: If
Throw
istrue
, then throw aTypeError
exception, otherwise return.
Notes:
- Step 7: if array exotic behavior exists, we can return right after processing the
length
update; in particular, step 9 is not necessary as an object cannot be simultaneously an array and an arguments object. - Step 11.d.2 (updating
length
) is a bit dangerous because it happens before step 12. Step 12 may fail due to an out-of-memory or other internal condition, which leaves thelength
updated but the element missing.
Minor improvements
Addressing the array length
issue:
- Check and/or coerce
O
as follows: a. IfO
isnull
orundefined
, throw aTypeError
. (This is theCheckObjectCoercible
part; the throw is unconditional.) b. IfO
is a boolean: setcurr
to the built-inBoolean
prototype object (skip creation of temporary) c. Else ifO
is a number: setcurr
to the built-inNumber
prototype object (skip creation of temporary) d. Else ifO
is a string: 1. SetP
toToString(P)
. (This may have side effects ifP
is an object.) 2. IfP
islength
, goto REJECT. 3. IfP
is a valid array index within the string length, goto REJECT. 4. Setcurr
to the built-inString
prototype object (skip creation of temporary) 5. Goto NEXT. (Avoid double coercion ofP
.) e. Else ifO
is an object: setcurr
toO
. f. Else, Throw aTypeError
. (Note that this case should not happen, as steps a-e are exhaustive. However, this step is useful as a fallback, and for handling any internal types.) - Let
P
beToString(P)
. (This may have side effects ifP
is an object.) - NEXT: Let
desc
be the result of calling the[[GetOwnProperty]]
internal method ofcurr
with property nameP
. - If
desc
isundefined
: a. Letcurr
be the value of the[[Prototype]]
internal property ofcurr
. b. Ifcurr
is notnull
, goto NEXT. c. IfO
is not an object (was coerced), goto REJECT. d. IfO.[[Extensible]]
isfalse
, goto REJECT. e. Goto NEWPROP. - If
IsDataDescriptor(desc)
: a. IfO
is not an object (was coerced), goto REJECT. b. Ifcurr
!=O
(property is an inherited data property): (Note: assumes there are no prototype loops.) 1. IfO.[[Extensible]
isfalse
, goto REJECT. 2. Ifdesc.[[Writable]]
isfalse
, goto REJECT. 3. Goto NEWPROP. c. Else (property is an own data property): 1. Ifdesc.[[Writable]]
isfalse
, goto REJECT. 2. Goto UPDATEPROP. - Else (property is an accessor): a. If
desc.[[Set]]
isundefined
, goto REJECT. b. Call the[[Call]]
internal method ofdesc.[[Set]]
providingO
as thethis
value and providingV
as the sole argument. (Note: the difference to a basic[[Put]]
is that the setterthis
binding is the original, uncoerced object.) c. Return. - UPDATEPROP: (Inlined
[[DefineOwnProperty]]
call for existing property.) IfO
is anArray
object, andP
is"length"
, then: a. LetnewLen
beToUint32(V)
. b. IfnewLen
is not equal toToNumber(V)
, goto REJECTRANGE. c. LetoldLenDesc
be the result of calling the[[GetOwnProperty]]
internal method ofO
passing"length"
as the argument. The result will never beundefined
or an accessor descriptor becauseArray
objects are created with alength
data property that cannot be deleted or reconfigured. d. LetoldLen
beoldLenDesc.[[Value]]
. (Note thatoldLen
is guaranteed to be a unsigned 32-bit integer.) e. IfnewLen
<oldLen
, then: 1. LetshortenSucceeded
,finalLen
be the result of calling the internal helperShortenArray()
witholdLen
andnewLen
. 2. Update the property ("length"
) value tofinalLen
. 3. Goto REJECT, ifshortenSucceeded
isfalse
. 4. Return. f. Update the property ("length"
) value tonewLen
. g. Return. - Set the
[[Value]]
attribute of the property namedP
of objectO
toV
. (Since it is side effect free to update the value with the same value, no check for that case is needed.) - If
O
is an arguments object which has a[[ParameterMap]]
internal property: a. Letmap
be the value of the[[ParameterMap]]
internal property of the arguments object. b. If the result of calling the[[GetOwnProperty]]
internal method ofmap
passingP
as the argument is notundefined
, then: 1. Call the[[Put]]
internal method ofmap
passingP
,V
, andThrow
as the arguments. (This updates the bound variable value.) - Return.
- NEWPROP: (Inlined
[[DefineOwnProperty]]
call for new property.) LetpendingLength
be 0 (zero). - If
O
is anArray
object andP
is an array index (E5 Section 15.4), then: a. LetoldLenDesc
be the result of calling the[[GetOwnProperty]]
internal method ofO
passing"length"
as the argument. The result will never beundefined
or an accessor descriptor becauseArray
objects are created with a length data property that cannot be deleted or reconfigured. b. LetoldLen
beoldLenDesc.[[Value]]
. (Note thatoldLen
is guaranteed to be a unsigned 32-bit integer.) c. Letindex
beToUint32(P)
. d. Ifindex
>=oldLen
: 1. Goto REJECToldLenDesc.[[Writable]]
isfalse
. 2. LetpendingLength
beindex + 1
(always non-zero). - Create an own data property named
P
of objectO
whose attributes are:[[Value]]: V
[[Writable]]: true
[[Enumerable]]: true
[[Configurable]]: true
- If
pendingLength
>0
: a. Update the"length"
property ofO
to the valuependingLength
. This always succeeds. (Note: this can only happen for anArray
object, and thelength
property must exist and has already been checked to be writable.) - Return.
- REJECT: If
Throw
istrue
, then throw aTypeError
exception, otherwise return.
Fast path for array indices
There is currently no fast path for array indices in the implementation.
This is primarily because to implement [[Put]
properly, the prototype chain needs to be walked when creating new properties, as an ancestor property may prevent or capture the write. The current implementation cannot walk the prototype chain without coercing the key to a string first. A fast path could be easily added for writing to existing array entries, though, but it's probably better to solve the problem a bit more comprehensively.
Implementation notes
- Property writes may fail for out of memory or other internal reasons. In such cases the algorithm should just throw an error and avoid making any updates to the object state. This is easy for normal properties, but there are some subtle issues when dealing with exotic behaviors which link multiple properties together and should be updated either atomically or in some consistent manner. In particular:
- For NEWPROP, if the property written is an array index which updates array
length
, the property write should be performed first. If the property write succeedslength
should be updated (and should never fail):
- For NEWPROP, if the property written is an array index which updates array
Final version
(See above.)