Skip to content

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 a PutValue() 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 as Boolean, Number, and String.)

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):

  1. Let orig be O. (Remember the uncoerced original for a possible setter call.)
  2. Call CheckObjectCoercible with O as argument. In practice: if O is null or undefined, throw a TypeError. (Note: this is unconditional.)
  3. Let P be ToString(P). (This may have side effects if P is an object.)
  4. If O is not an object, let coerced be true, else let coerced be false.
  5. Let O be ToObject(O). (This is side effect free.)
  6. Let curr be O.
  7. NEXT: Let desc be the result of calling the [[GetOwnProperty]] internal method of curr with property name P.
  8. If desc is undefined: a. Let curr be the value of the [[Prototype]] internal property of curr. b. If curr is not null, goto NEXT. c. If coerced is true, Reject. d. If O.[[Extensible]] is false, Reject. e. Let newDesc be a property descriptor with values: - [[Value]]: V - [[Writable]]: true - [[Enumerable]]: true - [[Configurable]]: true} f. Call O.[[DefineOwnProperty]](P, newDesc, Throw). g. Return.
  9. If IsDataDescriptor(desc): a. If coerced is true, Reject. b. If curr != O (property is an inherited data property): (Note: assumes there are no prototype loops.) 1. If O.[[Extensible] is false, Reject. 2. If desc.[[Writable]] is false, Reject. 3. Let newDesc be a property descriptor with values: - [[Value]]: V - [[Writable]]: true - [[Enumerable]]: true - [[Configurable]]: true} 4. Call O.[[DefineOwnProperty]](P, newDesc, Throw). c. Else (property is an own data property): 1. If desc.[[Writable]] is false, Reject. 2. Let valueDesc be { [[Value]]: V }. 3. Call O.[[DefineOwnProperty]](P, valueDesc, Throw).
  10. Else (property is an accessor): a. If desc.[[Set]] is undefined, Reject. b. Call the [[Call]] internal method of desc.[[Set]] providing orig as the this value and providing V as the sole argument. (Note: the difference to a basic [[Put]] is that the setter this binding is the original, uncoerced object.)
  11. 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 the String 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 not orig is an object.

  • Since curr is used for prototype chain walking, we don't need to store orig (O can be used for that instead).

Cleaning up

  1. Call CheckObjectCoercible with O as argument. In practice: if O is null or undefined, throw a TypeError. (Note: this is unconditional.)
  2. Let curr be ToObject(O). (This is side effect free.)
  3. Let P be ToString(P). (This may have side effects if P is an object.)
  4. NEXT: Let desc be the result of calling the [[GetOwnProperty]] internal method of curr with property name P.
  5. If desc is undefined: a. Let curr be the value of the [[Prototype]] internal property of curr. b. If curr is not null, goto NEXT. c. If O is not an object (was coerced), Reject. d. If O.[[Extensible]] is false, Reject. e. Let newDesc be a property descriptor with values: - [[Value]]: V - [[Writable]]: true - [[Enumerable]]: true - [[Configurable]]: true} f. Call O.[[DefineOwnProperty]](P, newDesc, Throw). g. Return.
  6. If IsDataDescriptor(desc): a. If O is not an object (was coerced), Reject. b. If curr != O (property is an inherited data property): (Note: assumes there are no prototype loops.) 1. If O.[[Extensible] is false, Reject. 2. If desc.[[Writable]] is false, Reject. 3. Let newDesc be a property descriptor with values: - [[Value]]: V - [[Writable]]: true - [[Enumerable]]: true - [[Configurable]]: true} 4. Call O.[[DefineOwnProperty]](P, newDesc, Throw). c. Else (property is an own data property): 1. If desc.[[Writable]] is false, Reject. 2. Let valueDesc be { [[Value]]: V }. 3. Call O.[[DefineOwnProperty]](P, valueDesc, Throw).
  7. Else (property is an accessor): a. If desc.[[Set]] is undefined, Reject. b. Call the [[Call]] internal method of desc.[[Set]] providing O as the this value and providing V as the sole argument. (Note: the difference to a basic [[Put]] is that the setter this binding is the original, uncoerced object.)
  8. 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:

  1. Call CheckObjectCoercible with O as argument. In practice: if O is null or undefined, throw a TypeError. (Note: this is unconditional.)
  2. Let curr be ToObject(O). (This is side effect free.)
  3. Let P be ToString(P). (This may have side effects if P is an object.)
  4. NEXT: Let desc be the result of calling the [[GetOwnProperty]] internal method of curr with property name P.
  5. If desc is undefined: a. Let curr be the value of the [[Prototype]] internal property of curr. b. If curr is not null, goto NEXT. c. If O is not an object (was coerced), goto REJECT. d. If O.[[Extensible]] is false, goto REJECT. e. Goto NEWPROP.
  6. If IsDataDescriptor(desc): a. If O is not an object (was coerced), goto REJECT. b. If curr != O (property is an inherited data property): (Note: assumes there are no prototype loops.) 1. If O.[[Extensible] is false, goto REJECT. 2. If desc.[[Writable]] is false, goto REJECT. 3. Goto NEWPROP. c. Else (property is an own data property): 1. If desc.[[Writable]] is false, goto REJECT. 2. Goto UPDATEPROP.
  7. Else (property is an accessor): a. If desc.[[Set]] is undefined, goto REJECT. b. Call the [[Call]] internal method of desc.[[Set]] providing O as the this value and providing V as the sole argument. (Note: the difference to a basic [[Put]] is that the setter this binding is the original, uncoerced object.) c. Return.
  8. UPDATEPROP: (Inlined [[DefineOwnProperty]] call for existing property.) If O is an Array object, and P is "length", then: a. Let newLen be ToUint32(V). b. If newLen is not equal to ToNumber(V), goto REJECTRANGE. c. Let oldLenDesc be the result of calling the [[GetOwnProperty]] internal method of O passing "length" as the argument. The result will never be undefined or an accessor descriptor because Array objects are created with a length data property that cannot be deleted or reconfigured. d. Let oldLen be oldLenDesc.[[Value]]. (Note that oldLen is guaranteed to be a unsigned 32-bit integer.) e. If newLen < oldLen, then: 1. Let shortenSucceeded, finalLen be the result of calling the internal helper ShortenArray() with oldLen and newLen. 2. Update the property ("length") value to finalLen. 3. Goto REJECT, if shortenSucceeded is false. 4. Return. f. Update the property ("length") value to newLen. g. Return.
  9. Set the [[Value]] attribute of the property named P of object O to V. (Since it is side effect free to update the value with the same value, no check for that case is needed.)
  10. If O is an arguments object which has a [[ParameterMap]] internal property: a. Let map be the value of the [[ParameterMap]] internal property of the arguments object. b. If the result of calling the [[GetOwnProperty]] internal method of map passing P as the argument is not undefined, then: 1. Call the [[Put]] internal method of map passing P, V, and Throw as the arguments. (This updates the bound variable value.)
  11. Return.
  12. NEWPROP: (Inlined [[DefineOwnProperty]] call for new property.) If O is an Array object and P is an array index (E5 Section 15.4), then: a. Let oldLenDesc be the result of calling the [[GetOwnProperty]] internal method of O passing "length" as the argument. The result will never be undefined or an accessor descriptor because Array objects are created with a length data property that cannot be deleted or reconfigured. b. Let oldLen be oldLenDesc.[[Value]]. (Note that oldLen is guaranteed to be a unsigned 32-bit integer.) c. Let index be ToUint32(P). d. If index >= oldLen: 1. Goto REJECT oldLenDesc.[[Writable]] is false. 2. Update the "length" property of O to the value index + 1. This always succeeds.
  13. Create an own data property named P of object O whose attributes are:
    • [[Value]]: V
    • [[Writable]]: true
    • [[Enumerable]]: true
    • [[Configurable]]: true
  14. Return.
  15. REJECT: If Throw is true, then throw a TypeError exception, otherwise return.
  16. 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: the length 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: none
  • Number: none
  • String: "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:

  1. Check and/or coerce O as follows: a. If O is null or undefined, throw a TypeError. (This is the CheckObjectCoercible part; the throw is unconditional.) b. If O is a boolean: set curr to the built-in Boolean prototype object (skip creation of temporary) c. Else if O is a number: set curr to the built-in Number prototype object (skip creation of temporary) d. Else if O is a string: 1. Set P to ToString(P). (This may have side effects if P is an object.) 2. If P is length, goto REJECT. 3. If P is a valid array index within the string length, goto REJECT. 4. Set curr to the built-in String prototype object (skip creation of temporary) 5. Goto NEXT. (Avoid double coercion of P.) e. Else if O is an object: set curr to O. f. Else, Throw a TypeError. (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.)
  2. Let P be ToString(P). (This may have side effects if P is an object.)
  3. NEXT: Let desc be the result of calling the [[GetOwnProperty]] internal method of curr with property name P.
  4. If desc is undefined: a. Let curr be the value of the [[Prototype]] internal property of curr. b. If curr is not null, goto NEXT. c. If O is not an object (was coerced), goto REJECT. d. If O.[[Extensible]] is false, goto REJECT. e. Goto NEWPROP.
  5. If IsDataDescriptor(desc): a. If O is not an object (was coerced), goto REJECT. b. If curr != O (property is an inherited data property): (Note: assumes there are no prototype loops.) 1. If O.[[Extensible] is false, goto REJECT. 2. If desc.[[Writable]] is false, goto REJECT. 3. Goto NEWPROP. c. Else (property is an own data property): 1. If desc.[[Writable]] is false, goto REJECT. 2. Goto UPDATEPROP.
  6. Else (property is an accessor): a. If desc.[[Set]] is undefined, goto REJECT. b. Call the [[Call]] internal method of desc.[[Set]] providing O as the this value and providing V as the sole argument. (Note: the difference to a basic [[Put]] is that the setter this binding is the original, uncoerced object.) c. Return.
  7. UPDATEPROP: (Inlined [[DefineOwnProperty]] call for existing property.) If O is an Array object, and P is "length", then: a. Let newLen be ToUint32(V). b. If newLen is not equal to ToNumber(V), goto REJECTRANGE. c. Let oldLenDesc be the result of calling the [[GetOwnProperty]] internal method of O passing "length" as the argument. The result will never be undefined or an accessor descriptor because Array objects are created with a length data property that cannot be deleted or reconfigured. d. Let oldLen be oldLenDesc.[[Value]]. (Note that oldLen is guaranteed to be a unsigned 32-bit integer.) e. If newLen < oldLen, then: 1. Let shortenSucceeded, finalLen be the result of calling the internal helper ShortenArray() with oldLen and newLen. 2. Update the property ("length") value to finalLen. 3. Goto REJECT, if shortenSucceeded is false. 4. Return. f. Update the property ("length") value to newLen. g. Return.
  8. Set the [[Value]] attribute of the property named P of object O to V. (Since it is side effect free to update the value with the same value, no check for that case is needed.)
  9. If O is an arguments object which has a [[ParameterMap]] internal property: a. Let map be the value of the [[ParameterMap]] internal property of the arguments object. b. If the result of calling the [[GetOwnProperty]] internal method of map passing P as the argument is not undefined, then: 1. Call the [[Put]] internal method of map passing P, V, and Throw as the arguments. (This updates the bound variable value.)
  10. Return.
  11. NEWPROP: (Inlined [[DefineOwnProperty]] call for new property.) If O is an Array object and P is an array index (E5 Section 15.4), then: a. Let oldLenDesc be the result of calling the [[GetOwnProperty]] internal method of O passing "length" as the argument. The result will never be undefined or an accessor descriptor because Array objects are created with a length data property that cannot be deleted or reconfigured. b. Let oldLen be oldLenDesc.[[Value]]. (Note that oldLen is guaranteed to be a unsigned 32-bit integer.) c. Let index be ToUint32(P). d. If index >= oldLen: 1. Goto REJECT oldLenDesc.[[Writable]] is false. 2. Update the "length" property of O to the value index + 1. This always succeeds.
  12. Create an own data property named P of object O whose attributes are:
    • [[Value]]: V
    • [[Writable]]: true
    • [[Enumerable]]: true
    • [[Configurable]]: true
  13. Return.
  14. REJECT: If Throw is true, then throw a TypeError 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 the length updated but the element missing.

Minor improvements

Addressing the array length issue:

  1. Check and/or coerce O as follows: a. If O is null or undefined, throw a TypeError. (This is the CheckObjectCoercible part; the throw is unconditional.) b. If O is a boolean: set curr to the built-in Boolean prototype object (skip creation of temporary) c. Else if O is a number: set curr to the built-in Number prototype object (skip creation of temporary) d. Else if O is a string: 1. Set P to ToString(P). (This may have side effects if P is an object.) 2. If P is length, goto REJECT. 3. If P is a valid array index within the string length, goto REJECT. 4. Set curr to the built-in String prototype object (skip creation of temporary) 5. Goto NEXT. (Avoid double coercion of P.) e. Else if O is an object: set curr to O. f. Else, Throw a TypeError. (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.)
  2. Let P be ToString(P). (This may have side effects if P is an object.)
  3. NEXT: Let desc be the result of calling the [[GetOwnProperty]] internal method of curr with property name P.
  4. If desc is undefined: a. Let curr be the value of the [[Prototype]] internal property of curr. b. If curr is not null, goto NEXT. c. If O is not an object (was coerced), goto REJECT. d. If O.[[Extensible]] is false, goto REJECT. e. Goto NEWPROP.
  5. If IsDataDescriptor(desc): a. If O is not an object (was coerced), goto REJECT. b. If curr != O (property is an inherited data property): (Note: assumes there are no prototype loops.) 1. If O.[[Extensible] is false, goto REJECT. 2. If desc.[[Writable]] is false, goto REJECT. 3. Goto NEWPROP. c. Else (property is an own data property): 1. If desc.[[Writable]] is false, goto REJECT. 2. Goto UPDATEPROP.
  6. Else (property is an accessor): a. If desc.[[Set]] is undefined, goto REJECT. b. Call the [[Call]] internal method of desc.[[Set]] providing O as the this value and providing V as the sole argument. (Note: the difference to a basic [[Put]] is that the setter this binding is the original, uncoerced object.) c. Return.
  7. UPDATEPROP: (Inlined [[DefineOwnProperty]] call for existing property.) If O is an Array object, and P is "length", then: a. Let newLen be ToUint32(V). b. If newLen is not equal to ToNumber(V), goto REJECTRANGE. c. Let oldLenDesc be the result of calling the [[GetOwnProperty]] internal method of O passing "length" as the argument. The result will never be undefined or an accessor descriptor because Array objects are created with a length data property that cannot be deleted or reconfigured. d. Let oldLen be oldLenDesc.[[Value]]. (Note that oldLen is guaranteed to be a unsigned 32-bit integer.) e. If newLen < oldLen, then: 1. Let shortenSucceeded, finalLen be the result of calling the internal helper ShortenArray() with oldLen and newLen. 2. Update the property ("length") value to finalLen. 3. Goto REJECT, if shortenSucceeded is false. 4. Return. f. Update the property ("length") value to newLen. g. Return.
  8. Set the [[Value]] attribute of the property named P of object O to V. (Since it is side effect free to update the value with the same value, no check for that case is needed.)
  9. If O is an arguments object which has a [[ParameterMap]] internal property: a. Let map be the value of the [[ParameterMap]] internal property of the arguments object. b. If the result of calling the [[GetOwnProperty]] internal method of map passing P as the argument is not undefined, then: 1. Call the [[Put]] internal method of map passing P, V, and Throw as the arguments. (This updates the bound variable value.)
  10. Return.
  11. NEWPROP: (Inlined [[DefineOwnProperty]] call for new property.) Let pendingLength be 0 (zero).
  12. If O is an Array object and P is an array index (E5 Section 15.4), then: a. Let oldLenDesc be the result of calling the [[GetOwnProperty]] internal method of O passing "length" as the argument. The result will never be undefined or an accessor descriptor because Array objects are created with a length data property that cannot be deleted or reconfigured. b. Let oldLen be oldLenDesc.[[Value]]. (Note that oldLen is guaranteed to be a unsigned 32-bit integer.) c. Let index be ToUint32(P). d. If index >= oldLen: 1. Goto REJECT oldLenDesc.[[Writable]] is false. 2. Let pendingLength be index + 1 (always non-zero).
  13. Create an own data property named P of object O whose attributes are:
    • [[Value]]: V
    • [[Writable]]: true
    • [[Enumerable]]: true
    • [[Configurable]]: true
  14. If pendingLength > 0: a. Update the "length" property of O to the value pendingLength. This always succeeds. (Note: this can only happen for an Array object, and the length property must exist and has already been checked to be writable.)
  15. Return.
  16. REJECT: If Throw is true, then throw a TypeError 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 succeeds length should be updated (and should never fail):

Final version

(See above.)