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