Mercurial > hg > openjdk > lambda > nashorn
view src/jdk/nashorn/internal/runtime/PropertyMap.java @ 650:d8aa87d292eb
8026858: Array length does not handle defined properties correctly
Reviewed-by: jlaskey
author | hannesw |
---|---|
date | Fri, 18 Oct 2013 22:42:41 +0200 |
parents | b7c04b3b01a7 |
children |
line wrap: on
line source
/* * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package jdk.nashorn.internal.runtime; import static jdk.nashorn.internal.runtime.PropertyHashMap.EMPTY_HASHMAP; import static jdk.nashorn.internal.runtime.arrays.ArrayIndex.getArrayIndex; import static jdk.nashorn.internal.runtime.arrays.ArrayIndex.isValidArrayIndex; import java.lang.invoke.SwitchPoint; import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.NoSuchElementException; import java.util.WeakHashMap; /** * Map of object properties. The PropertyMap is the "template" for JavaScript object * layouts. It contains a map with prototype names as keys and {@link Property} instances * as values. A PropertyMap is typically passed to the {@link ScriptObject} constructor * to form the seed map for the ScriptObject. * <p> * All property maps are immutable. If a property is added, modified or removed, the mutator * will return a new map. */ public final class PropertyMap implements Iterable<Object>, PropertyListener { /** Used for non extensible PropertyMaps, negative logic as the normal case is extensible. See {@link ScriptObject#preventExtensions()} */ public static final int NOT_EXTENSIBLE = 0b0000_0001; /** Does this map contain valid array keys? */ public static final int CONTAINS_ARRAY_KEYS = 0b0000_0010; /** This mask is used to preserve certain flags when cloning the PropertyMap. Others should not be copied */ private static final int CLONEABLE_FLAGS_MASK = 0b0000_1111; /** Has a listener been added to this property map. This flag is not copied when cloning a map. See {@link PropertyListener} */ public static final int IS_LISTENER_ADDED = 0b0001_0000; /** Is this process wide "shared" map?. This flag is not copied when cloning a map */ public static final int IS_SHARED = 0b0010_0000; /** Map status flags. */ private int flags; /** Map of properties. */ private final PropertyHashMap properties; /** Number of fields in use. */ private int fieldCount; /** Number of fields available. */ private int fieldMaximum; /** Length of spill in use. */ private int spillLength; /** {@link SwitchPoint}s for gets on inherited properties. */ private Map<String, SwitchPoint> protoGetSwitches; /** History of maps, used to limit map duplication. */ private HashMap<Property, PropertyMap> history; /** History of prototypes, used to limit map duplication. */ private WeakHashMap<ScriptObject, WeakReference<PropertyMap>> protoHistory; /** Cache for hashCode */ private int hashCode; /** * Constructor. * * @param properties A {@link PropertyHashMap} with initial contents. * @param fieldCount Number of fields in use. * @param fieldMaximum Number of fields available. * @param spillLength Number of spill slots used. * @param containsArrayKeys True if properties contain numeric keys */ private PropertyMap(final PropertyHashMap properties, final int fieldCount, final int fieldMaximum, final int spillLength, final boolean containsArrayKeys) { this.properties = properties; this.fieldCount = fieldCount; this.fieldMaximum = fieldMaximum; this.spillLength = spillLength; if (containsArrayKeys) { setContainsArrayKeys(); } if (Context.DEBUG) { count++; } } /** * Cloning constructor. * * @param propertyMap Existing property map. * @param properties A {@link PropertyHashMap} with a new set of properties. */ private PropertyMap(final PropertyMap propertyMap, final PropertyHashMap properties) { this.properties = properties; this.flags = propertyMap.getClonedFlags(); this.spillLength = propertyMap.spillLength; this.fieldCount = propertyMap.fieldCount; this.fieldMaximum = propertyMap.fieldMaximum; if (Context.DEBUG) { count++; clonedCount++; } } /** * Cloning constructor. * * @param propertyMap Existing property map. */ private PropertyMap(final PropertyMap propertyMap) { this(propertyMap, propertyMap.properties); } /** * Duplicates this PropertyMap instance. This is used to duplicate 'shared' * maps {@link PropertyMap} used as process wide singletons. Shared maps are * duplicated for every global scope object. That way listeners, proto and property * histories are scoped within a global scope. * * @return Duplicated {@link PropertyMap}. */ public PropertyMap duplicate() { if (Context.DEBUG) { duplicatedCount++; } return new PropertyMap(this.properties, 0, 0, 0, containsArrayKeys()); } /** * Public property map allocator. * * <p>It is the caller's responsibility to make sure that {@code properties} does not contain * properties with keys that are valid array indices.</p> * * @param properties Collection of initial properties. * @param fieldCount Number of fields in use. * @param fieldMaximum Number of fields available. * @param spillLength Number of used spill slots. * @return New {@link PropertyMap}. */ public static PropertyMap newMap(final Collection<Property> properties, final int fieldCount, final int fieldMaximum, final int spillLength) { PropertyHashMap newProperties = EMPTY_HASHMAP.immutableAdd(properties); return new PropertyMap(newProperties, fieldCount, fieldMaximum, spillLength, false); } /** * Public property map allocator. Used by nasgen generated code. * * <p>It is the caller's responsibility to make sure that {@code properties} does not contain * properties with keys that are valid array indices.</p> * * @param properties Collection of initial properties. * @return New {@link PropertyMap}. */ public static PropertyMap newMap(final Collection<Property> properties) { return (properties == null || properties.isEmpty())? newMap() : newMap(properties, 0, 0, 0); } /** * Return a sharable empty map. * * @return New empty {@link PropertyMap}. */ public static PropertyMap newMap() { return new PropertyMap(EMPTY_HASHMAP, 0, 0, 0, false); } /** * Return number of properties in the map. * * @return Number of properties. */ public int size() { return properties.size(); } /** * Return a SwitchPoint used to track changes of a property in a prototype. * * @param proto Object prototype. * @param key {@link Property} key. * * @return A shared {@link SwitchPoint} for the property. */ public SwitchPoint getProtoGetSwitchPoint(final ScriptObject proto, final String key) { assert !isShared() : "proto SwitchPoint from a shared PropertyMap"; if (proto == null) { return null; } if (protoGetSwitches == null) { protoGetSwitches = new HashMap<>(); if (! isListenerAdded()) { proto.addPropertyListener(this); setIsListenerAdded(); } } if (protoGetSwitches.containsKey(key)) { return protoGetSwitches.get(key); } final SwitchPoint switchPoint = new SwitchPoint(); protoGetSwitches.put(key, switchPoint); return switchPoint; } /** * Indicate that a prototype property has changed. * * @param property {@link Property} to invalidate. */ private void invalidateProtoGetSwitchPoint(final Property property) { assert !isShared() : "proto invalidation on a shared PropertyMap"; if (protoGetSwitches != null) { final String key = property.getKey(); final SwitchPoint sp = protoGetSwitches.get(key); if (sp != null) { protoGetSwitches.put(key, new SwitchPoint()); if (Context.DEBUG) { protoInvalidations++; } SwitchPoint.invalidateAll(new SwitchPoint[] { sp }); } } } /** * Indicate that proto itself has changed in hierachy somewhere. */ private void invalidateAllProtoGetSwitchPoints() { assert !isShared() : "proto invalidation on a shared PropertyMap"; if (protoGetSwitches != null) { final Collection<SwitchPoint> sws = protoGetSwitches.values(); SwitchPoint.invalidateAll(sws.toArray(new SwitchPoint[sws.size()])); } } /** * Add a property to the map, re-binding its getters and setters, * if available, to a given receiver. This is typically the global scope. See * {@link ScriptObject#addBoundProperties(ScriptObject)} * * @param property {@link Property} being added. * @param bindTo Object to bind to. * * @return New {@link PropertyMap} with {@link Property} added. */ PropertyMap addPropertyBind(final AccessorProperty property, final Object bindTo) { return addProperty(new AccessorProperty(property, bindTo)); } /** * Add a property to the map. Cloning or using an existing map if available. * * @param property {@link Property} being added. * * @return New {@link PropertyMap} with {@link Property} added. */ public PropertyMap addProperty(final Property property) { PropertyMap newMap = checkHistory(property); if (newMap == null) { final PropertyHashMap newProperties = properties.immutableAdd(property); newMap = new PropertyMap(this, newProperties); addToHistory(property, newMap); if(!property.isSpill()) { newMap.fieldCount = Math.max(newMap.fieldCount, property.getSlot() + 1); } if (isValidArrayIndex(getArrayIndex(property.getKey()))) { newMap.setContainsArrayKeys(); } newMap.spillLength += property.getSpillCount(); } return newMap; } /** * Remove a property from a map. Cloning or using an existing map if available. * * @param property {@link Property} being removed. * * @return New {@link PropertyMap} with {@link Property} removed or {@code null} if not found. */ public PropertyMap deleteProperty(final Property property) { PropertyMap newMap = checkHistory(property); final String key = property.getKey(); if (newMap == null && properties.containsKey(key)) { final PropertyHashMap newProperties = properties.immutableRemove(key); newMap = new PropertyMap(this, newProperties); addToHistory(property, newMap); } return newMap; } /** * Replace an existing property with a new one. * * @param oldProperty Property to replace. * @param newProperty New {@link Property}. * * @return New {@link PropertyMap} with {@link Property} replaced. */ PropertyMap replaceProperty(final Property oldProperty, final Property newProperty) { // Add replaces existing property. final PropertyHashMap newProperties = properties.immutableAdd(newProperty); final PropertyMap newMap = new PropertyMap(this, newProperties); /* * See ScriptObject.modifyProperty and ScriptObject.setUserAccessors methods. * * This replaceProperty method is called only for the following three cases: * * 1. To change flags OR TYPE of an old (cloned) property. We use the same spill slots. * 2. To change one UserAccessor property with another - user getter or setter changed via * Object.defineProperty function. Again, same spill slots are re-used. * 3. Via ScriptObject.setUserAccessors method to set user getter and setter functions * replacing the dummy AccessorProperty with null method handles (added during map init). * * In case (1) and case(2), the property type of old and new property is same. For case (3), * the old property is an AccessorProperty and the new one is a UserAccessorProperty property. */ final boolean sameType = (oldProperty.getClass() == newProperty.getClass()); assert sameType || (oldProperty instanceof AccessorProperty && newProperty instanceof UserAccessorProperty) : "arbitrary replaceProperty attempted"; newMap.flags = getClonedFlags(); /* * spillLength remains same in case (1) and (2) because of slot reuse. Only for case (3), we need * to add spill count of the newly added UserAccessorProperty property. */ newMap.spillLength = spillLength + (sameType? 0 : newProperty.getSpillCount()); return newMap; } /** * Make a new UserAccessorProperty property. getter and setter functions are stored in * this ScriptObject and slot values are used in property object. Note that slots * are assigned speculatively and should be added to map before adding other * properties. * * @param key the property name * @param propertyFlags attribute flags of the property * @return the newly created UserAccessorProperty */ public UserAccessorProperty newUserAccessors(final String key, final int propertyFlags) { int oldSpillLength = spillLength; final int getterSlot = oldSpillLength++; final int setterSlot = oldSpillLength++; return new UserAccessorProperty(key, propertyFlags, getterSlot, setterSlot); } /** * Find a property in the map. * * @param key Key to search for. * * @return {@link Property} matching key. */ public Property findProperty(final String key) { return properties.find(key); } /** * Adds all map properties from another map. * * @param other The source of properties. * * @return New {@link PropertyMap} with added properties. */ public PropertyMap addAll(final PropertyMap other) { assert this != other : "adding property map to itself"; final Property[] otherProperties = other.properties.getProperties(); final PropertyHashMap newProperties = properties.immutableAdd(otherProperties); final PropertyMap newMap = new PropertyMap(this, newProperties); for (final Property property : otherProperties) { if (isValidArrayIndex(getArrayIndex(property.getKey()))) { newMap.setContainsArrayKeys(); } newMap.spillLength += property.getSpillCount(); } return newMap; } /** * Return an array of all properties. * * @return Properties as an array. */ public Property[] getProperties() { return properties.getProperties(); } /** * Prevents the map from having additional properties. * * @return New map with {@link #NOT_EXTENSIBLE} flag set. */ PropertyMap preventExtensions() { final PropertyMap newMap = new PropertyMap(this); newMap.flags |= NOT_EXTENSIBLE; return newMap; } /** * Prevents properties in map from being modified. * * @return New map with {@link #NOT_EXTENSIBLE} flag set and properties with * {@link Property#NOT_CONFIGURABLE} set. */ PropertyMap seal() { PropertyHashMap newProperties = EMPTY_HASHMAP; for (final Property oldProperty : properties.getProperties()) { newProperties = newProperties.immutableAdd(oldProperty.addFlags(Property.NOT_CONFIGURABLE)); } final PropertyMap newMap = new PropertyMap(this, newProperties); newMap.flags |= NOT_EXTENSIBLE; return newMap; } /** * Prevents properties in map from being modified or written to. * * @return New map with {@link #NOT_EXTENSIBLE} flag set and properties with * {@link Property#NOT_CONFIGURABLE} and {@link Property#NOT_WRITABLE} set. */ PropertyMap freeze() { PropertyHashMap newProperties = EMPTY_HASHMAP; for (Property oldProperty : properties.getProperties()) { int propertyFlags = Property.NOT_CONFIGURABLE; if (!(oldProperty instanceof UserAccessorProperty)) { propertyFlags |= Property.NOT_WRITABLE; } newProperties = newProperties.immutableAdd(oldProperty.addFlags(propertyFlags)); } final PropertyMap newMap = new PropertyMap(this, newProperties); newMap.flags |= NOT_EXTENSIBLE; return newMap; } /** * Make this property map 'shared' one. Shared property map instances are * process wide singleton objects. A shaped map should never be added as a listener * to a proto object. Nor it should have history or proto history. A shared map * is just a template that is meant to be duplicated before use. All nasgen initialized * property maps are shared. * * @return this map after making it as shared */ public PropertyMap setIsShared() { assert !isListenerAdded() : "making PropertyMap shared after listener added"; assert protoHistory == null : "making PropertyMap shared after associating a proto with it"; if (Context.DEBUG) { sharedCount++; } flags |= IS_SHARED; // clear any history on this PropertyMap, won't be used. history = null; return this; } /** * Check for any configurable properties. * * @return {@code true} if any configurable. */ private boolean anyConfigurable() { for (final Property property : properties.getProperties()) { if (property.isConfigurable()) { return true; } } return false; } /** * Check if all properties are frozen. * * @return {@code true} if all are frozen. */ private boolean allFrozen() { for (final Property property : properties.getProperties()) { // check if it is a data descriptor if (!(property instanceof UserAccessorProperty)) { if (property.isWritable()) { return false; } } if (property.isConfigurable()) { return false; } } return true; } /** * Check prototype history for an existing property map with specified prototype. * * @param newProto New prototype object. * * @return Existing {@link PropertyMap} or {@code null} if not found. */ private PropertyMap checkProtoHistory(final ScriptObject newProto) { final PropertyMap cachedMap; if (protoHistory != null) { final WeakReference<PropertyMap> weakMap = protoHistory.get(newProto); cachedMap = (weakMap != null ? weakMap.get() : null); } else { cachedMap = null; } if (Context.DEBUG && cachedMap != null) { protoHistoryHit++; } return cachedMap; } /** * Add a map to the prototype history. * * @param newProto Prototype to add (key.) * @param newMap {@link PropertyMap} associated with prototype. */ private void addToProtoHistory(final ScriptObject newProto, final PropertyMap newMap) { assert !isShared() : "proto history modified on a shared PropertyMap"; if (protoHistory == null) { protoHistory = new WeakHashMap<>(); } protoHistory.put(newProto, new WeakReference<>(newMap)); } /** * Track the modification of the map. * * @param property Mapping property. * @param newMap Modified {@link PropertyMap}. */ private void addToHistory(final Property property, final PropertyMap newMap) { assert !isShared() : "history modified on a shared PropertyMap"; if (!properties.isEmpty()) { if (history == null) { history = new LinkedHashMap<>(); } history.put(property, newMap); } } /** * Check the history for a map that already has the given property added. * * @param property {@link Property} to add. * * @return Existing map or {@code null} if not found. */ private PropertyMap checkHistory(final Property property) { if (history != null) { PropertyMap historicMap = history.get(property); if (historicMap != null) { if (Context.DEBUG) { historyHit++; } return historicMap; } } return null; } /** * Calculate the hash code for the map. * * @return Computed hash code. */ private int computeHashCode() { int hash = 0; for (final Property property : getProperties()) { hash = hash << 7 ^ hash >> 7; hash ^= property.hashCode(); } return hash; } @Override public int hashCode() { if (hashCode == 0 && !properties.isEmpty()) { hashCode = computeHashCode(); } return hashCode; } @Override public boolean equals(final Object other) { if (!(other instanceof PropertyMap)) { return false; } final PropertyMap otherMap = (PropertyMap)other; if (properties.size() != otherMap.properties.size()) { return false; } final Iterator<Property> iter = properties.values().iterator(); final Iterator<Property> otherIter = otherMap.properties.values().iterator(); while (iter.hasNext() && otherIter.hasNext()) { if (!iter.next().equals(otherIter.next())) { return false; } } return true; } @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append(" ["); boolean isFirst = true; for (final Property property : properties.values()) { if (!isFirst) { sb.append(", "); } isFirst = false; sb.append(ScriptRuntime.safeToString(property.getKey())); final Class<?> ctype = property.getCurrentType(); sb.append(" <"). append(property.getClass().getSimpleName()). append(':'). append(ctype == null ? "undefined" : ctype.getSimpleName()). append('>'); } sb.append(']'); return sb.toString(); } @Override public Iterator<Object> iterator() { return new PropertyMapIterator(this); } /** * Check if this map contains properties with valid array keys * * @return {@code true} if this map contains properties with valid array keys */ public final boolean containsArrayKeys() { return (flags & CONTAINS_ARRAY_KEYS) != 0; } /** * Flag this object as having array keys in defined properties */ private void setContainsArrayKeys() { flags |= CONTAINS_ARRAY_KEYS; } /** * Check whether a {@link PropertyListener} has been added to this map. * * @return {@code true} if {@link PropertyListener} exists */ public boolean isListenerAdded() { return (flags & IS_LISTENER_ADDED) != 0; } /** * Check if this map shared or not. * * @return true if this map is shared. */ public boolean isShared() { return (flags & IS_SHARED) != 0; } /** * Test to see if {@link PropertyMap} is extensible. * * @return {@code true} if {@link PropertyMap} can be added to. */ boolean isExtensible() { return (flags & NOT_EXTENSIBLE) == 0; } /** * Test to see if {@link PropertyMap} is not extensible or any properties * can not be modified. * * @return {@code true} if {@link PropertyMap} is sealed. */ boolean isSealed() { return !isExtensible() && !anyConfigurable(); } /** * Test to see if {@link PropertyMap} is not extensible or all properties * can not be modified. * * @return {@code true} if {@link PropertyMap} is frozen. */ boolean isFrozen() { return !isExtensible() && allFrozen(); } /** * Get the number of fields allocated for this {@link PropertyMap}. * * @return Number of fields allocated. */ int getFieldCount() { return fieldCount; } /** * Get maximum number of fields available for this {@link PropertyMap}. * * @return Number of fields available. */ int getFieldMaximum() { return fieldMaximum; } /** * Get length of spill area associated with this {@link PropertyMap}. * * @return Length of spill area. */ int getSpillLength() { return spillLength; } /** * Change the prototype of objects associated with this {@link PropertyMap}. * * @param oldProto Current prototype object. * @param newProto New prototype object to replace oldProto. * * @return New {@link PropertyMap} with prototype changed. */ PropertyMap changeProto(final ScriptObject oldProto, final ScriptObject newProto) { assert !isShared() : "proto associated with a shared PropertyMap"; if (oldProto == newProto) { return this; } final PropertyMap nextMap = checkProtoHistory(newProto); if (nextMap != null) { return nextMap; } if (Context.DEBUG) { incrementSetProtoNewMapCount(); } final PropertyMap newMap = new PropertyMap(this); addToProtoHistory(newProto, newMap); return newMap; } /** * Indicate that the map has listeners. */ private void setIsListenerAdded() { flags |= IS_LISTENER_ADDED; } /** * Return only the flags that should be copied during cloning. * * @return Subset of flags that should be copied. */ private int getClonedFlags() { return flags & CLONEABLE_FLAGS_MASK; } /** * {@link PropertyMap} iterator. */ private static class PropertyMapIterator implements Iterator<Object> { /** Property iterator. */ final Iterator<Property> iter; /** Current Property. */ Property property; /** * Constructor. * * @param propertyMap {@link PropertyMap} to iterate over. */ PropertyMapIterator(final PropertyMap propertyMap) { iter = Arrays.asList(propertyMap.properties.getProperties()).iterator(); property = iter.hasNext() ? iter.next() : null; skipNotEnumerable(); } /** * Ignore properties that are not enumerable. */ private void skipNotEnumerable() { while (property != null && !property.isEnumerable()) { property = iter.hasNext() ? iter.next() : null; } } @Override public boolean hasNext() { return property != null; } @Override public Object next() { if (property == null) { throw new NoSuchElementException(); } final Object key = property.getKey(); property = iter.next(); skipNotEnumerable(); return key; } @Override public void remove() { throw new UnsupportedOperationException(); } } /* * PropertyListener implementation. */ @Override public void propertyAdded(final ScriptObject object, final Property prop) { invalidateProtoGetSwitchPoint(prop); } @Override public void propertyDeleted(final ScriptObject object, final Property prop) { invalidateProtoGetSwitchPoint(prop); } @Override public void propertyModified(final ScriptObject object, final Property oldProp, final Property newProp) { invalidateProtoGetSwitchPoint(oldProp); } @Override public void protoChanged(final ScriptObject object, final ScriptObject oldProto, final ScriptObject newProto) { // We may walk and invalidate SwitchPoints for properties inherited // from 'object' or it's old proto chain. But, it may not be worth it. // For example, a new proto may have a user defined getter/setter for // a data property down the chain. So, invalidating all is better. invalidateAllProtoGetSwitchPoints(); } /* * Debugging and statistics. */ // counters updated only in debug mode private static int count; private static int clonedCount; private static int sharedCount; private static int duplicatedCount; private static int historyHit; private static int protoInvalidations; private static int protoHistoryHit; private static int setProtoNewMapCount; /** * @return Total number of maps. */ public static int getCount() { return count; } /** * @return The number of maps that were cloned. */ public static int getClonedCount() { return clonedCount; } /** * @return The number of maps that are shared. */ public static int getSharedCount() { return sharedCount; } /** * @return The number of maps that are duplicated. */ public static int getDuplicatedCount() { return duplicatedCount; } /** * @return The number of times history was successfully used. */ public static int getHistoryHit() { return historyHit; } /** * @return The number of times prototype changes caused invalidation. */ public static int getProtoInvalidations() { return protoInvalidations; } /** * @return The number of times proto history was successfully used. */ public static int getProtoHistoryHit() { return protoHistoryHit; } /** * @return The number of times prototypes were modified. */ public static int getSetProtoNewMapCount() { return setProtoNewMapCount; } /** * Increment the prototype set count. */ private static void incrementSetProtoNewMapCount() { setProtoNewMapCount++; } }