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
thisbinding 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
origbeO. (Remember the uncoerced original for a possible setter call.) - Call
CheckObjectCoerciblewithOas argument. In practice: ifOisnullorundefined, throw aTypeError. (Note: this is unconditional.) - Let
PbeToString(P). (This may have side effects ifPis an object.) - If
Ois not an object, letcoercedbetrue, else letcoercedbefalse. - Let
ObeToObject(O). (This is side effect free.) - Let
currbeO. - NEXT: Let
descbe the result of calling the[[GetOwnProperty]]internal method ofcurrwith property nameP. - If
descisundefined: a. Letcurrbe the value of the[[Prototype]]internal property ofcurr. b. Ifcurris notnull, goto NEXT. c. Ifcoercedistrue, Reject. d. IfO.[[Extensible]]isfalse, Reject. e. LetnewDescbe 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. Ifcoercedistrue, 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. LetnewDescbe 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. LetvalueDescbe{ [[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]]providingorigas thethisvalue and providingVas the sole argument. (Note: the difference to a basic[[Put]]is that the setterthisbinding 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
thisbinding (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 theStringobject, 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
coercedflag is not needed: we can simply check whether or notorigis an object.Since
curris used for prototype chain walking, we don't need to storeorig(Ocan be used for that instead).
Cleaning up
- Call
CheckObjectCoerciblewithOas argument. In practice: ifOisnullorundefined, throw aTypeError. (Note: this is unconditional.) - Let
currbeToObject(O). (This is side effect free.) - Let
PbeToString(P). (This may have side effects ifPis an object.) - NEXT: Let
descbe the result of calling the[[GetOwnProperty]]internal method ofcurrwith property nameP. - If
descisundefined: a. Letcurrbe the value of the[[Prototype]]internal property ofcurr. b. Ifcurris notnull, goto NEXT. c. IfOis not an object (was coerced), Reject. d. IfO.[[Extensible]]isfalse, Reject. e. LetnewDescbe 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. IfOis 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. LetnewDescbe 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. LetvalueDescbe{ [[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]]providingOas thethisvalue and providingVas the sole argument. (Note: the difference to a basic[[Put]]is that the setterthisbinding 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
CheckObjectCoerciblewithOas argument. In practice: ifOisnullorundefined, throw aTypeError. (Note: this is unconditional.) - Let
currbeToObject(O). (This is side effect free.) - Let
PbeToString(P). (This may have side effects ifPis an object.) - NEXT: Let
descbe the result of calling the[[GetOwnProperty]]internal method ofcurrwith property nameP. - If
descisundefined: a. Letcurrbe the value of the[[Prototype]]internal property ofcurr. b. Ifcurris notnull, goto NEXT. c. IfOis not an object (was coerced), goto REJECT. d. IfO.[[Extensible]]isfalse, goto REJECT. e. Goto NEWPROP. - If
IsDataDescriptor(desc): a. IfOis 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]]providingOas thethisvalue and providingVas the sole argument. (Note: the difference to a basic[[Put]]is that the setterthisbinding is the original, uncoerced object.) c. Return. - UPDATEPROP: (Inlined
[[DefineOwnProperty]]call for existing property.) IfOis anArrayobject, andPis"length", then: a. LetnewLenbeToUint32(V). b. IfnewLenis not equal toToNumber(V), goto REJECTRANGE. c. LetoldLenDescbe the result of calling the[[GetOwnProperty]]internal method ofOpassing"length"as the argument. The result will never beundefinedor an accessor descriptor becauseArrayobjects are created with alengthdata property that cannot be deleted or reconfigured. d. LetoldLenbeoldLenDesc.[[Value]]. (Note thatoldLenis guaranteed to be a unsigned 32-bit integer.) e. IfnewLen<oldLen, then: 1. LetshortenSucceeded,finalLenbe the result of calling the internal helperShortenArray()witholdLenandnewLen. 2. Update the property ("length") value tofinalLen. 3. Goto REJECT, ifshortenSucceededisfalse. 4. Return. f. Update the property ("length") value tonewLen. g. Return. - Set the
[[Value]]attribute of the property namedPof objectOtoV. (Since it is side effect free to update the value with the same value, no check for that case is needed.) - If
Ois an arguments object which has a[[ParameterMap]]internal property: a. Letmapbe the value of the[[ParameterMap]]internal property of the arguments object. b. If the result of calling the[[GetOwnProperty]]internal method ofmappassingPas the argument is notundefined, then: 1. Call the[[Put]]internal method ofmappassingP,V, andThrowas the arguments. (This updates the bound variable value.) - Return.
- NEWPROP: (Inlined
[[DefineOwnProperty]]call for new property.) IfOis anArrayobject andPis an array index (E5 Section 15.4), then: a. LetoldLenDescbe the result of calling the[[GetOwnProperty]]internal method ofOpassing"length"as the argument. The result will never beundefinedor an accessor descriptor becauseArrayobjects are created with a length data property that cannot be deleted or reconfigured. b. LetoldLenbeoldLenDesc.[[Value]]. (Note thatoldLenis guaranteed to be a unsigned 32-bit integer.) c. LetindexbeToUint32(P). d. Ifindex>=oldLen: 1. Goto REJECToldLenDesc.[[Writable]]isfalse. 2. Update the"length"property ofOto the valueindex + 1. This always succeeds. - Create an own data property named
Pof objectOwhose attributes are:[[Value]]: V[[Writable]]: true[[Enumerable]]: true[[Configurable]]: true
- Return.
- REJECT: If
Throwistrue, then throw aTypeErrorexception, otherwise return. - REJECTRANGE: Throw a
RangeErrorexception. (This is unconditional.)
Notes:
- In step 8, we don't need to check for array index updates: the property already exists, so array
lengthwill 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
lengthexotic behavior: thelengthproperty 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
Oas follows: a. IfOisnullorundefined, throw aTypeError. (This is theCheckObjectCoerciblepart; the throw is unconditional.) b. IfOis a boolean: setcurrto the built-inBooleanprototype object (skip creation of temporary) c. Else ifOis a number: setcurrto the built-inNumberprototype object (skip creation of temporary) d. Else ifOis a string: 1. SetPtoToString(P). (This may have side effects ifPis an object.) 2. IfPislength, goto REJECT. 3. IfPis a valid array index within the string length, goto REJECT. 4. Setcurrto the built-inStringprototype object (skip creation of temporary) 5. Goto NEXT. (Avoid double coercion ofP.) e. Else ifOis an object: setcurrtoO. 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
PbeToString(P). (This may have side effects ifPis an object.) - NEXT: Let
descbe the result of calling the[[GetOwnProperty]]internal method ofcurrwith property nameP. - If
descisundefined: a. Letcurrbe the value of the[[Prototype]]internal property ofcurr. b. Ifcurris notnull, goto NEXT. c. IfOis not an object (was coerced), goto REJECT. d. IfO.[[Extensible]]isfalse, goto REJECT. e. Goto NEWPROP. - If
IsDataDescriptor(desc): a. IfOis 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]]providingOas thethisvalue and providingVas the sole argument. (Note: the difference to a basic[[Put]]is that the setterthisbinding is the original, uncoerced object.) c. Return. - UPDATEPROP: (Inlined
[[DefineOwnProperty]]call for existing property.) IfOis anArrayobject, andPis"length", then: a. LetnewLenbeToUint32(V). b. IfnewLenis not equal toToNumber(V), goto REJECTRANGE. c. LetoldLenDescbe the result of calling the[[GetOwnProperty]]internal method ofOpassing"length"as the argument. The result will never beundefinedor an accessor descriptor becauseArrayobjects are created with alengthdata property that cannot be deleted or reconfigured. d. LetoldLenbeoldLenDesc.[[Value]]. (Note thatoldLenis guaranteed to be a unsigned 32-bit integer.) e. IfnewLen<oldLen, then: 1. LetshortenSucceeded,finalLenbe the result of calling the internal helperShortenArray()witholdLenandnewLen. 2. Update the property ("length") value tofinalLen. 3. Goto REJECT, ifshortenSucceededisfalse. 4. Return. f. Update the property ("length") value tonewLen. g. Return. - Set the
[[Value]]attribute of the property namedPof objectOtoV. (Since it is side effect free to update the value with the same value, no check for that case is needed.) - If
Ois an arguments object which has a[[ParameterMap]]internal property: a. Letmapbe the value of the[[ParameterMap]]internal property of the arguments object. b. If the result of calling the[[GetOwnProperty]]internal method ofmappassingPas the argument is notundefined, then: 1. Call the[[Put]]internal method ofmappassingP,V, andThrowas the arguments. (This updates the bound variable value.) - Return.
- NEWPROP: (Inlined
[[DefineOwnProperty]]call for new property.) IfOis anArrayobject andPis an array index (E5 Section 15.4), then: a. LetoldLenDescbe the result of calling the[[GetOwnProperty]]internal method ofOpassing"length"as the argument. The result will never beundefinedor an accessor descriptor becauseArrayobjects are created with a length data property that cannot be deleted or reconfigured. b. LetoldLenbeoldLenDesc.[[Value]]. (Note thatoldLenis guaranteed to be a unsigned 32-bit integer.) c. LetindexbeToUint32(P). d. Ifindex>=oldLen: 1. Goto REJECToldLenDesc.[[Writable]]isfalse. 2. Update the"length"property ofOto the valueindex + 1. This always succeeds. - Create an own data property named
Pof objectOwhose attributes are:[[Value]]: V[[Writable]]: true[[Enumerable]]: true[[Configurable]]: true
- Return.
- REJECT: If
Throwistrue, then throw aTypeErrorexception, otherwise return.
Notes:
- Step 7: if array exotic behavior exists, we can return right after processing the
lengthupdate; 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 thelengthupdated but the element missing.
Minor improvements
Addressing the array length issue:
- Check and/or coerce
Oas follows: a. IfOisnullorundefined, throw aTypeError. (This is theCheckObjectCoerciblepart; the throw is unconditional.) b. IfOis a boolean: setcurrto the built-inBooleanprototype object (skip creation of temporary) c. Else ifOis a number: setcurrto the built-inNumberprototype object (skip creation of temporary) d. Else ifOis a string: 1. SetPtoToString(P). (This may have side effects ifPis an object.) 2. IfPislength, goto REJECT. 3. IfPis a valid array index within the string length, goto REJECT. 4. Setcurrto the built-inStringprototype object (skip creation of temporary) 5. Goto NEXT. (Avoid double coercion ofP.) e. Else ifOis an object: setcurrtoO. 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
PbeToString(P). (This may have side effects ifPis an object.) - NEXT: Let
descbe the result of calling the[[GetOwnProperty]]internal method ofcurrwith property nameP. - If
descisundefined: a. Letcurrbe the value of the[[Prototype]]internal property ofcurr. b. Ifcurris notnull, goto NEXT. c. IfOis not an object (was coerced), goto REJECT. d. IfO.[[Extensible]]isfalse, goto REJECT. e. Goto NEWPROP. - If
IsDataDescriptor(desc): a. IfOis 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]]providingOas thethisvalue and providingVas the sole argument. (Note: the difference to a basic[[Put]]is that the setterthisbinding is the original, uncoerced object.) c. Return. - UPDATEPROP: (Inlined
[[DefineOwnProperty]]call for existing property.) IfOis anArrayobject, andPis"length", then: a. LetnewLenbeToUint32(V). b. IfnewLenis not equal toToNumber(V), goto REJECTRANGE. c. LetoldLenDescbe the result of calling the[[GetOwnProperty]]internal method ofOpassing"length"as the argument. The result will never beundefinedor an accessor descriptor becauseArrayobjects are created with alengthdata property that cannot be deleted or reconfigured. d. LetoldLenbeoldLenDesc.[[Value]]. (Note thatoldLenis guaranteed to be a unsigned 32-bit integer.) e. IfnewLen<oldLen, then: 1. LetshortenSucceeded,finalLenbe the result of calling the internal helperShortenArray()witholdLenandnewLen. 2. Update the property ("length") value tofinalLen. 3. Goto REJECT, ifshortenSucceededisfalse. 4. Return. f. Update the property ("length") value tonewLen. g. Return. - Set the
[[Value]]attribute of the property namedPof objectOtoV. (Since it is side effect free to update the value with the same value, no check for that case is needed.) - If
Ois an arguments object which has a[[ParameterMap]]internal property: a. Letmapbe the value of the[[ParameterMap]]internal property of the arguments object. b. If the result of calling the[[GetOwnProperty]]internal method ofmappassingPas the argument is notundefined, then: 1. Call the[[Put]]internal method ofmappassingP,V, andThrowas the arguments. (This updates the bound variable value.) - Return.
- NEWPROP: (Inlined
[[DefineOwnProperty]]call for new property.) LetpendingLengthbe 0 (zero). - If
Ois anArrayobject andPis an array index (E5 Section 15.4), then: a. LetoldLenDescbe the result of calling the[[GetOwnProperty]]internal method ofOpassing"length"as the argument. The result will never beundefinedor an accessor descriptor becauseArrayobjects are created with a length data property that cannot be deleted or reconfigured. b. LetoldLenbeoldLenDesc.[[Value]]. (Note thatoldLenis guaranteed to be a unsigned 32-bit integer.) c. LetindexbeToUint32(P). d. Ifindex>=oldLen: 1. Goto REJECToldLenDesc.[[Writable]]isfalse. 2. LetpendingLengthbeindex + 1(always non-zero). - Create an own data property named
Pof objectOwhose attributes are:[[Value]]: V[[Writable]]: true[[Enumerable]]: true[[Configurable]]: true
- If
pendingLength>0: a. Update the"length"property ofOto the valuependingLength. This always succeeds. (Note: this can only happen for anArrayobject, and thelengthproperty must exist and has already been checked to be writable.) - Return.
- REJECT: If
Throwistrue, then throw aTypeErrorexception, 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 succeedslengthshould be updated (and should never fail):
- For NEWPROP, if the property written is an array index which updates array
Final version
(See above.)