1 /*
   2  * Copyright (c) 2010, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.nashorn.internal.runtime;
  27 
  28 import static jdk.nashorn.internal.runtime.PropertyHashMap.EMPTY_HASHMAP;
  29 import static jdk.nashorn.internal.runtime.arrays.ArrayIndex.getArrayIndex;
  30 import static jdk.nashorn.internal.runtime.arrays.ArrayIndex.isValidArrayIndex;
  31 
  32 import java.io.IOException;
  33 import java.io.ObjectInputStream;
  34 import java.io.ObjectOutputStream;
  35 import java.io.Serializable;
  36 import java.lang.invoke.SwitchPoint;
  37 import java.lang.ref.Reference;
  38 import java.lang.ref.SoftReference;
  39 import java.lang.ref.WeakReference;
  40 import java.util.Arrays;
  41 import java.util.BitSet;
  42 import java.util.Collection;
  43 import java.util.Iterator;
  44 import java.util.NoSuchElementException;
  45 import java.util.Set;
  46 import java.util.WeakHashMap;
  47 import java.util.concurrent.atomic.LongAdder;
  48 import jdk.nashorn.internal.runtime.options.Options;
  49 import jdk.nashorn.internal.scripts.JO;
  50 
  51 /**
  52  * Map of object properties. The PropertyMap is the "template" for JavaScript object
  53  * layouts. It contains a map with prototype names as keys and {@link Property} instances
  54  * as values. A PropertyMap is typically passed to the {@link ScriptObject} constructor
  55  * to form the seed map for the ScriptObject.
  56  * <p>
  57  * All property maps are immutable. If a property is added, modified or removed, the mutator
  58  * will return a new map.
  59  */
  60 public class PropertyMap implements Iterable<Object>, Serializable {
  61     private static final int INITIAL_SOFT_REFERENCE_DERIVATION_LIMIT =
  62             Math.max(0, Options.getIntProperty("nashorn.propertyMap.softReferenceDerivationLimit", 32));
  63 
  64     /** Used for non extensible PropertyMaps, negative logic as the normal case is extensible. See {@link ScriptObject#preventExtensions()} */
  65     private static final int NOT_EXTENSIBLE         = 0b0000_0001;
  66     /** Does this map contain valid array keys? */
  67     private static final int CONTAINS_ARRAY_KEYS    = 0b0000_0010;
  68 
  69     /** Map status flags. */
  70     private final int flags;
  71 
  72     /** Map of properties. */
  73     private transient PropertyHashMap properties;
  74 
  75     /** Number of fields in use. */
  76     private final int fieldCount;
  77 
  78     /** Number of fields available. */
  79     private final int fieldMaximum;
  80 
  81     /** Length of spill in use. */
  82     private final int spillLength;
  83 
  84     /** Structure class name */
  85     private final String className;
  86 
  87     /**
  88      * Countdown of number of times this property map has been derived from another property map. When it
  89      * reaches zero, the property map will start using weak references instead of soft references to hold on
  90      * to its history elements.
  91      */
  92     private final int softReferenceDerivationLimit;
  93 
  94     /** A reference to the expected shared prototype property map. If this is set this
  95      * property map should only be used if it the same as the actual prototype map. */
  96     private transient SharedPropertyMap sharedProtoMap;
  97 
  98     /** History of maps, used to limit map duplication. */
  99     private transient WeakHashMap<Property, Reference<PropertyMap>> history;
 100 
 101     /** History of prototypes, used to limit map duplication. */
 102     private transient WeakHashMap<ScriptObject, SoftReference<PropertyMap>> protoHistory;
 103 
 104     /** SwitchPoints for properties inherited form this map */
 105     private transient PropertySwitchPoints propertySwitchPoints;
 106 
 107     private transient BitSet freeSlots;
 108 
 109     private static final long serialVersionUID = -7041836752008732533L;
 110 
 111     /**
 112      * Constructs a new property map.
 113      *
 114      * @param properties   A {@link PropertyHashMap} with initial contents.
 115      * @param fieldCount   Number of fields in use.
 116      * @param fieldMaximum Number of fields available.
 117      * @param spillLength  Number of spill slots used.
 118      */
 119     private PropertyMap(final PropertyHashMap properties, final int flags, final String className,
 120                         final int fieldCount, final int fieldMaximum, final int spillLength) {
 121         this.properties   = properties;
 122         this.className    = className;
 123         this.fieldCount   = fieldCount;
 124         this.fieldMaximum = fieldMaximum;
 125         this.spillLength  = spillLength;
 126         this.flags        = flags;
 127         this.softReferenceDerivationLimit = INITIAL_SOFT_REFERENCE_DERIVATION_LIMIT;
 128 
 129         if (Context.DEBUG) {
 130             count.increment();
 131         }
 132     }
 133 
 134     /**
 135      * Constructs a clone of {@code propertyMap} with changed properties, flags, or boundaries.
 136      *
 137      * @param propertyMap Existing property map.
 138      * @param properties  A {@link PropertyHashMap} with a new set of properties.
 139      */
 140     private PropertyMap(final PropertyMap propertyMap, final PropertyHashMap properties, final int flags, final int fieldCount, final int spillLength, final int softReferenceDerivationLimit) {
 141         this.properties   = properties;
 142         this.flags        = flags;
 143         this.spillLength  = spillLength;
 144         this.fieldCount   = fieldCount;
 145         this.fieldMaximum = propertyMap.fieldMaximum;
 146         this.className    = propertyMap.className;
 147         // We inherit the parent property propertySwitchPoints instance. It will be cloned when a new listener is added.
 148         this.propertySwitchPoints = propertyMap.propertySwitchPoints;
 149         this.freeSlots    = propertyMap.freeSlots;
 150         this.sharedProtoMap = propertyMap.sharedProtoMap;
 151         this.softReferenceDerivationLimit = softReferenceDerivationLimit;
 152 
 153         if (Context.DEBUG) {
 154             count.increment();
 155             clonedCount.increment();
 156         }
 157     }
 158 
 159     /**
 160      * Constructs an exact clone of {@code propertyMap}.
 161      *
 162      * @param propertyMap Existing property map.
 163       */
 164     protected PropertyMap(final PropertyMap propertyMap) {
 165         this(propertyMap, propertyMap.properties, propertyMap.flags, propertyMap.fieldCount, propertyMap.spillLength, propertyMap.softReferenceDerivationLimit);
 166     }
 167 
 168     private void writeObject(final ObjectOutputStream out) throws IOException {
 169         out.defaultWriteObject();
 170         out.writeObject(properties.getProperties());
 171     }
 172 
 173     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
 174         in.defaultReadObject();
 175 
 176         final Property[] props = (Property[]) in.readObject();
 177         this.properties = EMPTY_HASHMAP.immutableAdd(props);
 178 
 179         assert className != null;
 180         final Class<?> structure = Context.forStructureClass(className);
 181         for (final Property prop : props) {
 182             prop.initMethodHandles(structure);
 183         }
 184     }
 185 
 186     /**
 187      * Public property map allocator.
 188      *
 189      * <p>It is the caller's responsibility to make sure that {@code properties} does not contain
 190      * properties with keys that are valid array indices.</p>
 191      *
 192      * @param properties   Collection of initial properties.
 193      * @param className    class name
 194      * @param fieldCount   Number of fields in use.
 195      * @param fieldMaximum Number of fields available.
 196      * @param spillLength  Number of used spill slots.
 197      * @return New {@link PropertyMap}.
 198      */
 199     public static PropertyMap newMap(final Collection<Property> properties, final String className, final int fieldCount, final int fieldMaximum,  final int spillLength) {
 200         final PropertyHashMap newProperties = EMPTY_HASHMAP.immutableAdd(properties);
 201         return new PropertyMap(newProperties, 0, className, fieldCount, fieldMaximum, spillLength);
 202     }
 203 
 204     /**
 205      * Public property map allocator. Used by nasgen generated code.
 206      *
 207      * <p>It is the caller's responsibility to make sure that {@code properties} does not contain
 208      * properties with keys that are valid array indices.</p>
 209      *
 210      * @param properties Collection of initial properties.
 211      * @return New {@link PropertyMap}.
 212      */
 213     public static PropertyMap newMap(final Collection<Property> properties) {
 214         return properties == null || properties.isEmpty()? newMap() : newMap(properties, JO.class.getName(), 0, 0, 0);
 215     }
 216 
 217     /**
 218      * Return a sharable empty map for the given object class.
 219      * @param clazz the base object class
 220      * @return New empty {@link PropertyMap}.
 221      */
 222     public static PropertyMap newMap(final Class<? extends ScriptObject> clazz) {
 223         return new PropertyMap(EMPTY_HASHMAP, 0, clazz.getName(), 0, 0, 0);
 224     }
 225 
 226     /**
 227      * Return a sharable empty map.
 228      *
 229      * @return New empty {@link PropertyMap}.
 230      */
 231     public static PropertyMap newMap() {
 232         return newMap(JO.class);
 233     }
 234 
 235     /**
 236      * Return number of properties in the map.
 237      *
 238      * @return Number of properties.
 239      */
 240     public int size() {
 241         return properties.size();
 242     }
 243 
 244     /**
 245      * Get the number of property SwitchPoints of this map
 246      *
 247      * @return the number of property SwitchPoints
 248      */
 249     public int getSwitchPointCount() {
 250         return propertySwitchPoints == null ? 0 : propertySwitchPoints.getSwitchPointCount();
 251     }
 252 
 253     /**
 254      * Add a property switchpoint to this property map for the given {@code key}.
 255      *
 256      * @param key the property name
 257      * @param switchPoint the switchpoint
 258      */
 259     public void addSwitchPoint(final String key, final SwitchPoint switchPoint) {
 260         // We need to clone listener instance when adding a new listener since we share
 261         // the propertySwitchPoints instance with our parent maps that don't need to see the new listener.
 262         propertySwitchPoints = PropertySwitchPoints.addSwitchPoint(propertySwitchPoints, key, switchPoint);
 263     }
 264 
 265     /**
 266      * Method called when a property of an object using this property map is being created,
 267      * modified, or deleted. If a switchpoint for the property exists it will be invalidated.
 268      *
 269      * @param property The changed property.
 270      */
 271     public void propertyChanged(final Property property) {
 272         if (propertySwitchPoints != null) {
 273             propertySwitchPoints.invalidateProperty(property);
 274         }
 275     }
 276 
 277     /**
 278      * Method called when the prototype of an object using this property map is changed.
 279      */
 280     void protoChanged() {
 281         if (sharedProtoMap != null) {
 282             sharedProtoMap.invalidateSwitchPoint();
 283         }
 284         if (propertySwitchPoints != null) {
 285             propertySwitchPoints.invalidateInheritedProperties(this);
 286         }
 287     }
 288 
 289     /**
 290      * Returns a SwitchPoint for use with a property inherited from this or a parent map.
 291      * If such a switchpoint exists, it will be invalidated when the property is modified
 292      * in an object using this map. This method returns {@code null} if no swichpoint exists
 293      * for the property.
 294      *
 295      * @param key Property key.
 296      * @return A {@link SwitchPoint} for the property, or null.
 297      */
 298     public synchronized SwitchPoint getSwitchPoint(final String key) {
 299         if (propertySwitchPoints != null) {
 300             final Set<SwitchPoint> existingSwitchPoints = propertySwitchPoints.getSwitchPoints(key);
 301             for (final SwitchPoint switchPoint : existingSwitchPoints) {
 302                 if (switchPoint != null && !switchPoint.hasBeenInvalidated()) {
 303                     return switchPoint;
 304                 }
 305             }
 306         }
 307 
 308         return null;
 309     }
 310 
 311     /**
 312      * Add a property to the map, re-binding its getters and setters,
 313      * if available, to a given receiver. This is typically the global scope. See
 314      * {@link ScriptObject#addBoundProperties(ScriptObject)}
 315      *
 316      * @param property {@link Property} being added.
 317      * @param bindTo   Object to bind to.
 318      *
 319      * @return New {@link PropertyMap} with {@link Property} added.
 320      */
 321     PropertyMap addPropertyBind(final AccessorProperty property, final Object bindTo) {
 322         // We must not store bound property in the history as bound properties can't be reused.
 323         return addPropertyNoHistory(new AccessorProperty(property, bindTo));
 324     }
 325 
 326     // Get a logical slot index for a property, with spill slot 0 starting at fieldMaximum.
 327     private int logicalSlotIndex(final Property property) {
 328         final int slot = property.getSlot();
 329         if (slot < 0) {
 330             return -1;
 331         }
 332         return property.isSpill() ? slot + fieldMaximum : slot;
 333     }
 334 
 335     private int newSpillLength(final Property newProperty) {
 336         return newProperty.isSpill() ? Math.max(spillLength, newProperty.getSlot() + 1) : spillLength;
 337     }
 338 
 339     private int newFieldCount(final Property newProperty) {
 340         return !newProperty.isSpill() ? Math.max(fieldCount, newProperty.getSlot() + 1) : fieldCount;
 341     }
 342 
 343     private int newFlags(final Property newProperty) {
 344         return isValidArrayIndex(getArrayIndex(newProperty.getKey())) ? flags | CONTAINS_ARRAY_KEYS : flags;
 345     }
 346 
 347     // Update the free slots bitmap for a property that has been deleted and/or added. This method is not synchronized
 348     // as it is always invoked on a newly created instance.
 349     private void updateFreeSlots(final Property oldProperty, final Property newProperty) {
 350         // Free slots bitset is possibly shared with parent map, so we must clone it before making modifications.
 351         boolean freeSlotsCloned = false;
 352         if (oldProperty != null) {
 353             final int slotIndex = logicalSlotIndex(oldProperty);
 354             if (slotIndex >= 0) {
 355                 final BitSet newFreeSlots = freeSlots == null ? new BitSet() : (BitSet)freeSlots.clone();
 356                 assert !newFreeSlots.get(slotIndex);
 357                 newFreeSlots.set(slotIndex);
 358                 freeSlots = newFreeSlots;
 359                 freeSlotsCloned = true;
 360             }
 361         }
 362         if (freeSlots != null && newProperty != null) {
 363             final int slotIndex = logicalSlotIndex(newProperty);
 364             if (slotIndex > -1 && freeSlots.get(slotIndex)) {
 365                 final BitSet newFreeSlots = freeSlotsCloned ? freeSlots : ((BitSet)freeSlots.clone());
 366                 newFreeSlots.clear(slotIndex);
 367                 freeSlots = newFreeSlots.isEmpty() ? null : newFreeSlots;
 368             }
 369         }
 370     }
 371 
 372     /**
 373      * Add a property to the map without adding it to the history. This should be used for properties that
 374      * can't be shared such as bound properties, or properties that are expected to be added only once.
 375      *
 376      * @param property {@link Property} being added.
 377      * @return New {@link PropertyMap} with {@link Property} added.
 378      */
 379     public final PropertyMap addPropertyNoHistory(final Property property) {
 380         propertyChanged(property);
 381         return addPropertyInternal(property);
 382     }
 383 
 384     /**
 385      * Add a property to the map.  Cloning or using an existing map if available.
 386      *
 387      * @param property {@link Property} being added.
 388      *
 389      * @return New {@link PropertyMap} with {@link Property} added.
 390      */
 391     public final synchronized PropertyMap addProperty(final Property property) {
 392         propertyChanged(property);
 393         PropertyMap newMap = checkHistory(property);
 394 
 395         if (newMap == null) {
 396             newMap = addPropertyInternal(property);
 397             addToHistory(property, newMap);
 398         }
 399 
 400         return newMap;
 401     }
 402 
 403     private PropertyMap deriveMap(final PropertyHashMap newProperties, final int newFlags, final int newFieldCount, final int newSpillLength) {
 404         return new PropertyMap(this, newProperties, newFlags, newFieldCount, newSpillLength, softReferenceDerivationLimit == 0 ? 0 : softReferenceDerivationLimit - 1);
 405     }
 406 
 407     private PropertyMap addPropertyInternal(final Property property) {
 408         final PropertyHashMap newProperties = properties.immutableAdd(property);
 409         final PropertyMap newMap = deriveMap(newProperties, newFlags(property), newFieldCount(property), newSpillLength(property));
 410         newMap.updateFreeSlots(null, property);
 411         return newMap;
 412     }
 413 
 414     /**
 415      * Remove a property from a map. Cloning or using an existing map if available.
 416      *
 417      * @param property {@link Property} being removed.
 418      *
 419      * @return New {@link PropertyMap} with {@link Property} removed or {@code null} if not found.
 420      */
 421     public final synchronized PropertyMap deleteProperty(final Property property) {
 422         propertyChanged(property);
 423         PropertyMap newMap = checkHistory(property);
 424         final Object key = property.getKey();
 425 
 426         if (newMap == null && properties.containsKey(key)) {
 427             final PropertyHashMap newProperties = properties.immutableRemove(key);
 428             final boolean isSpill = property.isSpill();
 429             final int slot = property.getSlot();
 430             // If deleted property was last field or spill slot we can make it reusable by reducing field/slot count.
 431             // Otherwise mark it as free in free slots bitset.
 432             if (isSpill && slot >= 0 && slot == spillLength - 1) {
 433                 newMap = deriveMap(newProperties, flags, fieldCount, spillLength - 1);
 434                 newMap.freeSlots = freeSlots;
 435             } else if (!isSpill && slot >= 0 && slot == fieldCount - 1) {
 436                 newMap = deriveMap(newProperties, flags, fieldCount - 1, spillLength);
 437                 newMap.freeSlots = freeSlots;
 438             } else {
 439                 newMap = deriveMap(newProperties, flags, fieldCount, spillLength);
 440                 newMap.updateFreeSlots(property, null);
 441             }
 442             addToHistory(property, newMap);
 443         }
 444 
 445         return newMap;
 446     }
 447 
 448     /**
 449      * Replace an existing property with a new one.
 450      *
 451      * @param oldProperty Property to replace.
 452      * @param newProperty New {@link Property}.
 453      *
 454      * @return New {@link PropertyMap} with {@link Property} replaced.
 455      */
 456     public final PropertyMap replaceProperty(final Property oldProperty, final Property newProperty) {
 457         propertyChanged(oldProperty);
 458         /*
 459          * See ScriptObject.modifyProperty and ScriptObject.setUserAccessors methods.
 460          *
 461          * This replaceProperty method is called only for the following three cases:
 462          *
 463          *   1. To change flags OR TYPE of an old (cloned) property. We use the same spill slots.
 464          *   2. To change one UserAccessor property with another - user getter or setter changed via
 465          *      Object.defineProperty function. Again, same spill slots are re-used.
 466          *   3. Via ScriptObject.setUserAccessors method to set user getter and setter functions
 467          *      replacing the dummy AccessorProperty with null method handles (added during map init).
 468          *
 469          * In case (1) and case(2), the property type of old and new property is same. For case (3),
 470          * the old property is an AccessorProperty and the new one is a UserAccessorProperty property.
 471          */
 472 
 473         final boolean sameType = oldProperty.getClass() == newProperty.getClass();
 474         assert sameType ||
 475                 oldProperty instanceof AccessorProperty &&
 476                 newProperty instanceof UserAccessorProperty :
 477             "arbitrary replaceProperty attempted " + sameType + " oldProperty=" + oldProperty.getClass() + " newProperty=" + newProperty.getClass() + " [" + oldProperty.getLocalType() + " => " + newProperty.getLocalType() + "]";
 478 
 479         /*
 480          * spillLength remains same in case (1) and (2) because of slot reuse. Only for case (3), we need
 481          * to add spill count of the newly added UserAccessorProperty property.
 482          */
 483         final int newSpillLength = sameType ? spillLength : Math.max(spillLength, newProperty.getSlot() + 1);
 484 
 485         // Add replaces existing property.
 486         final PropertyHashMap newProperties = properties.immutableReplace(oldProperty, newProperty);
 487         final PropertyMap newMap = deriveMap(newProperties, flags, fieldCount, newSpillLength);
 488 
 489         if (!sameType) {
 490             newMap.updateFreeSlots(oldProperty, newProperty);
 491         }
 492         return newMap;
 493     }
 494 
 495     /**
 496      * Make a new UserAccessorProperty property. getter and setter functions are stored in
 497      * this ScriptObject and slot values are used in property object. Note that slots
 498      * are assigned speculatively and should be added to map before adding other
 499      * properties.
 500      *
 501      * @param key the property name
 502      * @param propertyFlags attribute flags of the property
 503      * @return the newly created UserAccessorProperty
 504      */
 505     public final UserAccessorProperty newUserAccessors(final Object key, final int propertyFlags) {
 506         return new UserAccessorProperty(key, propertyFlags, getFreeSpillSlot());
 507     }
 508 
 509     /**
 510      * Find a property in the map.
 511      *
 512      * @param key Key to search for.
 513      *
 514      * @return {@link Property} matching key.
 515      */
 516     public final Property findProperty(final Object key) {
 517         return properties.find(key);
 518     }
 519 
 520     /**
 521      * Adds all map properties from another map.
 522      *
 523      * @param other The source of properties.
 524      *
 525      * @return New {@link PropertyMap} with added properties.
 526      */
 527     public final PropertyMap addAll(final PropertyMap other) {
 528         assert this != other : "adding property map to itself";
 529         final Property[] otherProperties = other.properties.getProperties();
 530         final PropertyHashMap newProperties = properties.immutableAdd(otherProperties);
 531 
 532         final PropertyMap newMap = deriveMap(newProperties, flags, fieldCount, spillLength);
 533         for (final Property property : otherProperties) {
 534             // This method is only safe to use with non-slotted, native getter/setter properties
 535             assert property.getSlot() == -1;
 536             assert !(isValidArrayIndex(getArrayIndex(property.getKey())));
 537         }
 538 
 539         return newMap;
 540     }
 541 
 542     /**
 543      * Return an array of all properties.
 544      *
 545      * @return Properties as an array.
 546      */
 547     public final Property[] getProperties() {
 548         return properties.getProperties();
 549     }
 550 
 551     /**
 552      * Return the name of the class of objects using this property map.
 553      *
 554      * @return class name of owner objects.
 555      */
 556     public final String getClassName() {
 557         return className;
 558     }
 559 
 560     /**
 561      * Prevents the map from having additional properties.
 562      *
 563      * @return New map with {@link #NOT_EXTENSIBLE} flag set.
 564      */
 565     PropertyMap preventExtensions() {
 566         return deriveMap(properties, flags | NOT_EXTENSIBLE, fieldCount, spillLength);
 567     }
 568 
 569     /**
 570      * Prevents properties in map from being modified.
 571      *
 572      * @return New map with {@link #NOT_EXTENSIBLE} flag set and properties with
 573      * {@link Property#NOT_CONFIGURABLE} set.
 574      */
 575     PropertyMap seal() {
 576         PropertyHashMap newProperties = EMPTY_HASHMAP;
 577 
 578         for (final Property oldProperty :  properties.getProperties()) {
 579             newProperties = newProperties.immutableAdd(oldProperty.addFlags(Property.NOT_CONFIGURABLE));
 580         }
 581 
 582         return deriveMap(newProperties, flags | NOT_EXTENSIBLE, fieldCount, spillLength);
 583     }
 584 
 585     /**
 586      * Prevents properties in map from being modified or written to.
 587      *
 588      * @return New map with {@link #NOT_EXTENSIBLE} flag set and properties with
 589      * {@link Property#NOT_CONFIGURABLE} and {@link Property#NOT_WRITABLE} set.
 590      */
 591     PropertyMap freeze() {
 592         PropertyHashMap newProperties = EMPTY_HASHMAP;
 593 
 594         for (final Property oldProperty : properties.getProperties()) {
 595             int propertyFlags = Property.NOT_CONFIGURABLE;
 596 
 597             if (!(oldProperty instanceof UserAccessorProperty)) {
 598                 propertyFlags |= Property.NOT_WRITABLE;
 599             }
 600 
 601             newProperties = newProperties.immutableAdd(oldProperty.addFlags(propertyFlags));
 602         }
 603 
 604         return deriveMap(newProperties, flags | NOT_EXTENSIBLE, fieldCount, spillLength);
 605     }
 606 
 607     /**
 608      * Check for any configurable properties.
 609      *
 610      * @return {@code true} if any configurable.
 611      */
 612     private boolean anyConfigurable() {
 613         for (final Property property : properties.getProperties()) {
 614             if (property.isConfigurable()) {
 615                return true;
 616             }
 617         }
 618 
 619         return false;
 620     }
 621 
 622     /**
 623      * Check if all properties are frozen.
 624      *
 625      * @return {@code true} if all are frozen.
 626      */
 627     private boolean allFrozen() {
 628         for (final Property property : properties.getProperties()) {
 629             // check if it is a data descriptor
 630             if (!property.isAccessorProperty() && property.isWritable()) {
 631                 return false;
 632             }
 633             if (property.isConfigurable()) {
 634                return false;
 635             }
 636         }
 637 
 638         return true;
 639     }
 640 
 641     /**
 642      * Check prototype history for an existing property map with specified prototype.
 643      *
 644      * @param proto New prototype object.
 645      *
 646      * @return Existing {@link PropertyMap} or {@code null} if not found.
 647      */
 648     private PropertyMap checkProtoHistory(final ScriptObject proto) {
 649         final PropertyMap cachedMap;
 650         if (protoHistory != null) {
 651             final SoftReference<PropertyMap> weakMap = protoHistory.get(proto);
 652             cachedMap = (weakMap != null ? weakMap.get() : null);
 653         } else {
 654             cachedMap = null;
 655         }
 656 
 657         if (Context.DEBUG && cachedMap != null) {
 658             protoHistoryHit.increment();
 659         }
 660 
 661         return cachedMap;
 662     }
 663 
 664     /**
 665      * Add a map to the prototype history.
 666      *
 667      * @param newProto Prototype to add (key.)
 668      * @param newMap   {@link PropertyMap} associated with prototype.
 669      */
 670     private void addToProtoHistory(final ScriptObject newProto, final PropertyMap newMap) {
 671         if (protoHistory == null) {
 672             protoHistory = new WeakHashMap<>();
 673         }
 674 
 675         protoHistory.put(newProto, new SoftReference<>(newMap));
 676     }
 677 
 678     /**
 679      * Track the modification of the map.
 680      *
 681      * @param property Mapping property.
 682      * @param newMap   Modified {@link PropertyMap}.
 683      */
 684     private void addToHistory(final Property property, final PropertyMap newMap) {
 685         if (history == null) {
 686             history = new WeakHashMap<>();
 687         }
 688 
 689         history.put(property, softReferenceDerivationLimit == 0 ? new WeakReference<>(newMap) : new SoftReference<>(newMap));
 690     }
 691 
 692     /**
 693      * Check the history for a map that already has the given property added.
 694      *
 695      * @param property {@link Property} to add.
 696      *
 697      * @return Existing map or {@code null} if not found.
 698      */
 699     private PropertyMap checkHistory(final Property property) {
 700 
 701         if (history != null) {
 702             final Reference<PropertyMap> ref = history.get(property);
 703             final PropertyMap historicMap = ref == null ? null : ref.get();
 704 
 705             if (historicMap != null) {
 706                 if (Context.DEBUG) {
 707                     historyHit.increment();
 708                 }
 709 
 710                 return historicMap;
 711             }
 712         }
 713 
 714         return null;
 715     }
 716 
 717     /**
 718      * Returns true if the two maps have identical properties in the same order, but allows the properties to differ in
 719      * their types. This method is mostly useful for tests.
 720      * @param otherMap the other map
 721      * @return true if this map has identical properties in the same order as the other map, allowing the properties to
 722      * differ in type.
 723      */
 724     public boolean equalsWithoutType(final PropertyMap otherMap) {
 725         if (properties.size() != otherMap.properties.size()) {
 726             return false;
 727         }
 728 
 729         final Iterator<Property> iter      = properties.values().iterator();
 730         final Iterator<Property> otherIter = otherMap.properties.values().iterator();
 731 
 732         while (iter.hasNext() && otherIter.hasNext()) {
 733             if (!iter.next().equalsWithoutType(otherIter.next())) {
 734                 return false;
 735             }
 736         }
 737 
 738         return true;
 739     }
 740 
 741     @Override
 742     public String toString() {
 743         final StringBuilder sb = new StringBuilder();
 744 
 745         sb.append(Debug.id(this));
 746         sb.append(" = {\n");
 747 
 748         for (final Property property : getProperties()) {
 749             sb.append('\t');
 750             sb.append(property);
 751             sb.append('\n');
 752         }
 753 
 754         sb.append('}');
 755 
 756         return sb.toString();
 757     }
 758 
 759     @Override
 760     public Iterator<Object> iterator() {
 761         return new PropertyMapIterator(this);
 762     }
 763 
 764     /**
 765      * Check if this map contains properties with valid array keys
 766      *
 767      * @return {@code true} if this map contains properties with valid array keys
 768      */
 769     public final boolean containsArrayKeys() {
 770         return (flags & CONTAINS_ARRAY_KEYS) != 0;
 771     }
 772 
 773     /**
 774      * Test to see if {@link PropertyMap} is extensible.
 775      *
 776      * @return {@code true} if {@link PropertyMap} can be added to.
 777      */
 778     boolean isExtensible() {
 779         return (flags & NOT_EXTENSIBLE) == 0;
 780     }
 781 
 782     /**
 783      * Test to see if {@link PropertyMap} is not extensible or any properties
 784      * can not be modified.
 785      *
 786      * @return {@code true} if {@link PropertyMap} is sealed.
 787      */
 788     boolean isSealed() {
 789         return !isExtensible() && !anyConfigurable();
 790     }
 791 
 792     /**
 793      * Test to see if {@link PropertyMap} is not extensible or all properties
 794      * can not be modified.
 795      *
 796      * @return {@code true} if {@link PropertyMap} is frozen.
 797      */
 798     boolean isFrozen() {
 799         return !isExtensible() && allFrozen();
 800     }
 801 
 802     /**
 803      * Return a free field slot for this map, or {@code -1} if none is available.
 804      *
 805      * @return free field slot or -1
 806      */
 807     int getFreeFieldSlot() {
 808         if (freeSlots != null) {
 809             final int freeSlot = freeSlots.nextSetBit(0);
 810             if (freeSlot > -1 && freeSlot < fieldMaximum) {
 811                 return freeSlot;
 812             }
 813         }
 814         if (fieldCount < fieldMaximum) {
 815             return fieldCount;
 816         }
 817         return -1;
 818     }
 819 
 820     /**
 821      * Get a free spill slot for this map.
 822      *
 823      * @return free spill slot
 824      */
 825     int getFreeSpillSlot() {
 826         if (freeSlots != null) {
 827             final int freeSlot = freeSlots.nextSetBit(fieldMaximum);
 828             if (freeSlot > -1) {
 829                 return freeSlot - fieldMaximum;
 830             }
 831         }
 832         return spillLength;
 833     }
 834 
 835     /**
 836      * Return a property map with the same layout that is associated with the new prototype object.
 837      *
 838      * @param newProto New prototype object to replace oldProto.
 839      * @return New {@link PropertyMap} with prototype changed.
 840      */
 841     public synchronized PropertyMap changeProto(final ScriptObject newProto) {
 842         final PropertyMap nextMap = checkProtoHistory(newProto);
 843         if (nextMap != null) {
 844             return nextMap;
 845         }
 846 
 847         if (Context.DEBUG) {
 848             setProtoNewMapCount.increment();
 849         }
 850 
 851         final PropertyMap newMap = makeUnsharedCopy();
 852         addToProtoHistory(newProto, newMap);
 853 
 854         return newMap;
 855     }
 856 
 857     /**
 858      * Make a copy of this property map with the shared prototype field set to null. Note that this is
 859      * only necessary for shared maps of top-level objects. Shared prototype maps represented by
 860      * {@link SharedPropertyMap} are automatically converted to plain property maps when they evolve.
 861      *
 862      * @return a copy with the shared proto map unset
 863      */
 864     PropertyMap makeUnsharedCopy() {
 865         final PropertyMap newMap = new PropertyMap(this);
 866         newMap.sharedProtoMap = null;
 867         return newMap;
 868     }
 869 
 870     /**
 871      * Set a reference to the expected parent prototype map. This is used for class-like
 872      * structures where we only want to use a top-level property map if all of the
 873      * prototype property maps have not been modified.
 874      *
 875      * @param protoMap weak reference to the prototype property map
 876      */
 877     void setSharedProtoMap(final SharedPropertyMap protoMap) {
 878         sharedProtoMap = protoMap;
 879     }
 880 
 881     /**
 882      * Get the expected prototype property map if it is known, or null.
 883      *
 884      * @return parent map or null
 885      */
 886     public PropertyMap getSharedProtoMap() {
 887         return sharedProtoMap;
 888     }
 889 
 890     /**
 891      * Returns {@code true} if this map has been used as a shared prototype map (i.e. as a prototype
 892      * for a JavaScript constructor function) and has not had properties added, deleted or replaced since then.
 893      * @return true if this is a valid shared prototype map
 894      */
 895     boolean isValidSharedProtoMap() {
 896         return false;
 897     }
 898 
 899     /**
 900      * Returns the shared prototype switch point, or null if this is not a shared prototype map.
 901      * @return the shared prototype switch point, or null
 902      */
 903     SwitchPoint getSharedProtoSwitchPoint() {
 904         return null;
 905     }
 906 
 907     /**
 908      * Return true if this map has a shared prototype map which has either been invalidated or does
 909      * not match the map of {@code proto}.
 910      * @param prototype the prototype object
 911      * @return true if this is an invalid shared map for {@code prototype}
 912      */
 913     boolean isInvalidSharedMapFor(final ScriptObject prototype) {
 914         return sharedProtoMap != null
 915                 && (!sharedProtoMap.isValidSharedProtoMap() || prototype == null || sharedProtoMap != prototype.getMap());
 916     }
 917 
 918     /**
 919      * {@link PropertyMap} iterator.
 920      */
 921     private static class PropertyMapIterator implements Iterator<Object> {
 922         /** Property iterator. */
 923         final Iterator<Property> iter;
 924 
 925         /** Current Property. */
 926         Property property;
 927 
 928         /**
 929          * Constructor.
 930          *
 931          * @param propertyMap {@link PropertyMap} to iterate over.
 932          */
 933         PropertyMapIterator(final PropertyMap propertyMap) {
 934             iter = Arrays.asList(propertyMap.properties.getProperties()).iterator();
 935             property = iter.hasNext() ? iter.next() : null;
 936             skipNotEnumerable();
 937         }
 938 
 939         /**
 940          * Ignore properties that are not enumerable.
 941          */
 942         private void skipNotEnumerable() {
 943             while (property != null && !property.isEnumerable()) {
 944                 property = iter.hasNext() ? iter.next() : null;
 945             }
 946         }
 947 
 948         @Override
 949         public boolean hasNext() {
 950             return property != null;
 951         }
 952 
 953         @Override
 954         public Object next() {
 955             if (property == null) {
 956                 throw new NoSuchElementException();
 957             }
 958 
 959             final Object key = property.getKey();
 960             property = iter.hasNext() ? iter.next() : null;
 961             skipNotEnumerable();
 962 
 963             return key;
 964         }
 965 
 966         @Override
 967         public void remove() {
 968             throw new UnsupportedOperationException("remove");
 969         }
 970     }
 971 
 972     /*
 973      * Debugging and statistics.
 974      */
 975 
 976     /**
 977      * Debug helper function that returns the diff of two property maps, only
 978      * displaying the information that is different and in which map it exists
 979      * compared to the other map. Can be used to e.g. debug map guards and
 980      * investigate why they fail, causing relink
 981      *
 982      * @param map0 the first property map
 983      * @param map1 the second property map
 984      *
 985      * @return property map diff as string
 986      */
 987     public static String diff(final PropertyMap map0, final PropertyMap map1) {
 988         final StringBuilder sb = new StringBuilder();
 989 
 990         if (map0 != map1) {
 991            sb.append(">>> START: Map diff");
 992            boolean found = false;
 993 
 994            for (final Property p : map0.getProperties()) {
 995                final Property p2 = map1.findProperty(p.getKey());
 996                if (p2 == null) {
 997                    sb.append("FIRST ONLY : [").append(p).append("]");
 998                    found = true;
 999                } else if (p2 != p) {
1000                    sb.append("DIFFERENT  : [").append(p).append("] != [").append(p2).append("]");
1001                    found = true;
1002                }
1003            }
1004 
1005            for (final Property p2 : map1.getProperties()) {
1006                final Property p1 = map0.findProperty(p2.getKey());
1007                if (p1 == null) {
1008                    sb.append("SECOND ONLY: [").append(p2).append("]");
1009                    found = true;
1010                }
1011            }
1012 
1013            //assert found;
1014 
1015            if (!found) {
1016                 sb.append(map0).
1017                     append("!=").
1018                     append(map1);
1019            }
1020 
1021            sb.append("<<< END: Map diff\n");
1022         }
1023 
1024         return sb.toString();
1025     }
1026 
1027     // counters updated only in debug mode
1028     private static LongAdder count;
1029     private static LongAdder clonedCount;
1030     private static LongAdder historyHit;
1031     private static LongAdder protoInvalidations;
1032     private static LongAdder protoHistoryHit;
1033     private static LongAdder setProtoNewMapCount;
1034     static {
1035         if (Context.DEBUG) {
1036             count = new LongAdder();
1037             clonedCount = new LongAdder();
1038             historyHit = new LongAdder();
1039             protoInvalidations = new LongAdder();
1040             protoHistoryHit = new LongAdder();
1041             setProtoNewMapCount = new LongAdder();
1042         }
1043     }
1044 
1045     /**
1046      * @return Total number of maps.
1047      */
1048     public static long getCount() {
1049         return count.longValue();
1050     }
1051 
1052     /**
1053      * @return The number of maps that were cloned.
1054      */
1055     public static long getClonedCount() {
1056         return clonedCount.longValue();
1057     }
1058 
1059     /**
1060      * @return The number of times history was successfully used.
1061      */
1062     public static long getHistoryHit() {
1063         return historyHit.longValue();
1064     }
1065 
1066     /**
1067      * @return The number of times prototype changes caused invalidation.
1068      */
1069     public static long getProtoInvalidations() {
1070         return protoInvalidations.longValue();
1071     }
1072 
1073     /**
1074      * @return The number of times proto history was successfully used.
1075      */
1076     public static long getProtoHistoryHit() {
1077         return protoHistoryHit.longValue();
1078     }
1079 
1080     /**
1081      * @return The number of times prototypes were modified.
1082      */
1083     public static long getSetProtoNewMapCount() {
1084         return setProtoNewMapCount.longValue();
1085     }
1086 }